Module: SmilyCli::QueryOptions

Defined in:
lib/smily_cli/query_options.rb

Overview

Turns user-facing flags (--filter status=booked, --query per_page=100, --fields id,name, --include availability) into the flat query hash the API v3 expects. v3 filtering is plain query parameters, so --filter and --query share one mechanism; --filter simply reads as intent.

Constant Summary collapse

OPERATORS =

Comparison operators the API v3 filter DSL accepts (mirrors the operators reported per attribute by the resource_schema / api_v3_resource_schema tool). Used to recognize the attribute[op]=value filter syntax.

%w[eq not_eq gt gteq lt lteq in matches does_not_match].freeze

Class Method Summary collapse

Class Method Details

.as_operator(condition) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



130
131
132
# File 'lib/smily_cli/query_options.rb', line 130

def as_operator(condition)
  condition.is_a?(Hash) ? condition : { "op" => "eq", "value" => condition }
end

.build(fields: nil, include: nil, filter: [], query: []) ⇒ Hash{String=>Object}

Assemble the final query hash for a request from the standard flags.

Parameters:

  • fields (Array<String>, nil) (defaults to: nil)

    sparse fieldset

  • include (Array<String>, String, nil) (defaults to: nil)

    associations to sideload

  • filter (Array<String>) (defaults to: [])

    key=value filter pairs

  • query (Array<String>) (defaults to: [])

    key=value passthrough params

Returns:

  • (Hash{String=>Object})


71
72
73
74
75
76
77
78
79
80
# File 'lib/smily_cli/query_options.rb', line 71

def build(fields: nil, include: nil, filter: [], query: [])
  result = {}
  result.merge!(parse_pairs(query))
  result.merge!(parse_pairs(filter))
  if (inc = normalize_list(include))
    result["include"] = inc.join(",")
  end
  result["fields"] = fields.join(",") if fields && !fields.empty?
  result
end

.coerce_filter_value(operator, value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

in takes a comma-separated set; every other operator a single value.



136
137
138
139
140
# File 'lib/smily_cli/query_options.rb', line 136

def coerce_filter_value(operator, value)
  return value.split(",").map { |v| coerce_scalar(v.strip) } if operator == "in"

  coerce_scalar(value)
end

.coerce_scalar(value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Coerce a raw string into the natural JSON scalar so numeric/boolean attributes compare as themselves; anything else (incl. ISO 8601) stays a string.



146
147
148
149
150
151
152
153
154
# File 'lib/smily_cli/query_options.rb', line 146

def coerce_scalar(value)
  case value
  when /\A-?\d+\z/ then value.to_i
  when /\A-?\d+\.\d+\z/ then Float(value)
  when "true" then true
  when "false" then false
  else value
  end
end

.condition_for(operator, value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

A single filter condition: an { op, value } object when an operator was given, else the bare coerced equality value.



94
95
96
97
98
# File 'lib/smily_cli/query_options.rb', line 94

def condition_for(operator, value)
  return coerce_scalar(value) unless operator

  { "op" => operator, "value" => coerce_filter_value(operator, value) }
end

.merge_condition(filter, attribute, condition) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Add one condition to the filter, arraying (AND) when the attribute already has one. A bare equality value is promoted to an { op: "eq" } object when it must share an array with operator conditions, keeping the array uniform.



119
120
121
122
123
124
125
126
127
# File 'lib/smily_cli/query_options.rb', line 119

def merge_condition(filter, attribute, condition)
  if filter.key?(attribute)
    existing = filter[attribute]
    list = existing.is_a?(Array) ? existing : [as_operator(existing)]
    filter[attribute] = list << as_operator(condition)
  else
    filter[attribute] = condition
  end
end

.normalize_list(value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



157
158
159
160
161
162
# File 'lib/smily_cli/query_options.rb', line 157

def normalize_list(value)
  return nil if value.nil?

  list = Array(value).flat_map { |v| v.to_s.split(",") }.map(&:strip).reject(&:empty?)
  list.empty? ? nil : list
end

.parse_filter(pairs) ⇒ Hash{String=>Object}

Parse key=value / key[op]=value filter pairs into the structured filter object the API v3 list tool accepts (via MCP api_v3_list):

* a bare (coerced) value for plain equality,
* an `{ "op", "value" }` object for a single operator, and
* an array of such objects when one attribute carries more than one
condition (AND) — e.g. a numeric or date range.

Examples:

["currency=EUR"]                      => { "currency" => "EUR" }
["adults[gteq]=2"]                    => { "adults" => { "op" => "gteq", "value" => 2 } }
["final_price[gteq]=600",
"final_price[lt]=800"]               => { "final_price" =>
                                         [ { "op" => "gteq", "value" => 600 },
                                           { "op" => "lt",   "value" => 800 } ] }
["reference[in]=A,B"]                 => { "reference" => { "op" => "in", "value" => %w[A B] } }

Values are coerced (integer/float/boolean) so numeric attributes compare as numbers rather than strings; ISO 8601 timestamps stay strings.

Parameters:

  • pairs (Array<String>)

Returns:

  • (Hash{String=>Object})

Raises:

  • (UsageError)

    on a missing = or an unknown operator



56
57
58
59
60
61
62
# File 'lib/smily_cli/query_options.rb', line 56

def parse_filter(pairs)
  Array(pairs).each_with_object({}) do |pair, acc|
    key, value = split_pair(pair)
    attribute, operator = parse_operator(key)
    merge_condition(acc, attribute, condition_for(operator, value))
  end
end

.parse_operator(key) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Split an attribute[op] key into [attribute, op], validating the operator; a plain attribute returns [attribute, nil] (equality).



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/smily_cli/query_options.rb', line 103

def parse_operator(key)
  match = key.match(/\A(?<attribute>.+?)\[(?<op>[a-z_]+)\]\z/)
  return [key, nil] unless match

  operator = match[:op]
  unless OPERATORS.include?(operator)
    raise UsageError,
          "Unknown filter operator #{operator.inspect} in #{key.inspect}. Valid: #{OPERATORS.join(', ')}."
  end
  [match[:attribute], operator]
end

.parse_pairs(pairs) ⇒ Hash{String=>Object}

Parse key=value strings into a hash. A repeated key accumulates into an array (so --filter status=booked --filter status=tentative sends both).

Parameters:

  • pairs (Array<String>)

Returns:

  • (Hash{String=>Object})

Raises:



22
23
24
25
26
27
28
29
30
31
# File 'lib/smily_cli/query_options.rb', line 22

def parse_pairs(pairs)
  Array(pairs).each_with_object({}) do |pair, acc|
    key, value = split_pair(pair)
    acc[key] = if acc.key?(key)
                 Array(acc[key]) << value
               else
                 value
               end
  end
end

.split_pair(pair) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Raises:



83
84
85
86
87
88
89
# File 'lib/smily_cli/query_options.rb', line 83

def split_pair(pair)
  str = pair.to_s
  key, sep, value = str.partition("=")
  raise UsageError, "Invalid key=value pair #{pair.inspect} (expected e.g. status=booked)" if sep.empty?

  [key.strip, value]
end