Module: Pago::Serde
- Defined in:
- lib/pago/serde.rb
Overview
JSON conversion helpers shared by every generated model and service.
Class Method Summary collapse
-
.array(value) ⇒ Object
Map a JSON array through the given block, preserving nil.
-
.best_variant(data, variants) ⇒ Class?
The variant whose declared keys best match the payload.
-
.candidates(data, variants, seen = []) ⇒ Array<Class>
A tagged union whose tag the payload carries but this release does not know contributes nothing: the payload states it is a variant we cannot name, so scoring it against the variants we do know would mislabel it.
-
.dispatch_variant(data, union_module) ⇒ Class, ...
The variant a nested union's discriminator selects.
-
.dump(value) ⇒ Object
Recursively convert models, arrays and hashes into JSON-ready values.
-
.map(value) ⇒ Object
Map the values of a JSON object through the given block, preserving nil.
-
.object(data) ⇒ Hash?
The value when it is a JSON object, nil otherwise.
-
.path(template, params = {}) ⇒ Object
Interpolate
{name}placeholders of a path template. -
.query(params = {}) ⇒ Object
Flatten query parameters into
[name, value]pairs. -
.scalar(value) ⇒ String
The query string representation of a scalar value.
-
.tagged?(data, union_module) ⇒ Boolean
Whether the payload carries this union's discriminator.
-
.union(data, discriminator: nil, mapping: nil, variants: []) ⇒ Object
Resolve a union payload to one of its variants.
-
.union_module?(variant) ⇒ Boolean
Whether the variant is a generated union factory module.
Class Method Details
.array(value) ⇒ Object
Map a JSON array through the given block, preserving nil.
26 27 28 29 30 31 |
# File 'lib/pago/serde.rb', line 26 def array(value) return nil if value.nil? return value unless value.is_a?(Array) value.map { |item| yield(item) } end |
.best_variant(data, variants) ⇒ Class?
Returns the variant whose declared keys best match the payload.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/pago/serde.rb', line 110 def best_variant(data, variants) keys = data.keys.map(&:to_s) best = nil best_score = -1 variants.each do |variant| next unless variant.respond_to?(:json_keys) && variant.respond_to?(:required_json_keys) next unless (variant.required_json_keys - keys).empty? score = (variant.json_keys.values & keys).size if score > best_score best = variant best_score = score end end best end |
.candidates(data, variants, seen = []) ⇒ Array<Class>
A tagged union whose tag the payload carries but this release does not know contributes nothing: the payload states it is a variant we cannot name, so scoring it against the variants we do know would mislabel it.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/pago/serde.rb', line 83 def candidates(data, variants, seen = []) variants.flat_map do |variant| if union_module?(variant) next [] if seen.include?(variant) nested = dispatch_variant(data, variant) next candidates(data, [nested], seen + [variant]) if nested next [] if tagged?(data, variant) candidates(data, variant.variants, seen + [variant]) elsif variant.respond_to?(:json_keys) [variant] else [] end end end |
.dispatch_variant(data, union_module) ⇒ Class, ...
Returns the variant a nested union's discriminator selects.
134 135 136 137 138 139 140 141 142 |
# File 'lib/pago/serde.rb', line 134 def dispatch_variant(data, union_module) return nil unless union_module.const_defined?(:DISCRIMINATOR, false) discriminator = union_module.const_get(:DISCRIMINATOR) mapping = union_module.mapping return nil if discriminator.nil? || mapping.nil? mapping[data[discriminator]] end |
.dump(value) ⇒ Object
Recursively convert models, arrays and hashes into JSON-ready values.
42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/pago/serde.rb', line 42 def dump(value) case value when ::Pago::Model then value.to_json_hash when Array then value.map { |item| dump(item) } when Hash value.each_with_object({}) do |(key, item), result| result[key.is_a?(Symbol) ? key.to_s : key] = dump(item) end when Symbol then value.to_s else value end end |
.map(value) ⇒ Object
Map the values of a JSON object through the given block, preserving nil.
34 35 36 37 38 39 |
# File 'lib/pago/serde.rb', line 34 def map(value) return nil if value.nil? return value unless value.is_a?(Hash) value.each_with_object({}) { |(key, item), result| result[key] = yield(item) } end |
.object(data) ⇒ Hash?
Returns the value when it is a JSON object, nil otherwise.
19 20 21 22 23 |
# File 'lib/pago/serde.rb', line 19 def object(data) return nil if data.nil? data.is_a?(Hash) ? data : nil end |
.path(template, params = {}) ⇒ Object
Interpolate {name} placeholders of a path template.
145 146 147 148 149 150 151 152 153 |
# File 'lib/pago/serde.rb', line 145 def path(template, params = {}) template.gsub(/\{(\w+)\}/) do name = Regexp.last_match(1) value = params[name] raise ::Pago::Error, "Missing value for path parameter #{name.inspect}" if value.nil? ::URI.encode_www_form_component(scalar(value)).gsub("+", "%20") end end |
.query(params = {}) ⇒ Object
Flatten query parameters into [name, value] pairs.
Nil values are dropped, arrays repeat the parameter and hashes are encoded
as name[key]=value, matching the deep-object style used by the API.
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/pago/serde.rb', line 159 def query(params = {}) pairs = [] params.each do |name, value| next if value.nil? case value when Hash value.each do |key, nested| pairs << ["#{name}[#{key}]", scalar(nested)] unless nested.nil? end when Array value.each { |item| pairs << [name.to_s, scalar(item)] unless item.nil? } else pairs << [name.to_s, scalar(value)] end end pairs end |
.scalar(value) ⇒ String
Returns the query string representation of a scalar value.
179 180 181 182 183 184 |
# File 'lib/pago/serde.rb', line 179 def scalar(value) case value when Time then value.utc.strftime("%Y-%m-%dT%H:%M:%SZ") else value.to_s end end |
.tagged?(data, union_module) ⇒ Boolean
Returns whether the payload carries this union's discriminator.
102 103 104 105 106 107 |
# File 'lib/pago/serde.rb', line 102 def tagged?(data, union_module) return false unless union_module.const_defined?(:DISCRIMINATOR, false) discriminator = union_module.const_get(:DISCRIMINATOR) !discriminator.nil? && !union_module.mapping.nil? && data.key?(discriminator) end |
.union(data, discriminator: nil, mapping: nil, variants: []) ⇒ Object
Resolve a union payload to one of its variants.
A discriminated union dispatches on its discriminator property. Otherwise the candidate models are scored against the payload: a candidate is eligible only when every key its schema requires is present, and the one matching the most keys wins. A union whose variants are themselves unions contributes the variant its own discriminator selects, falling back to all of them. When nothing matches, the raw payload is returned untouched rather than raising, so a variant added server side never breaks a client.
64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/pago/serde.rb', line 64 def union(data, discriminator: nil, mapping: nil, variants: []) return nil if data.nil? return data unless data.is_a?(Hash) if discriminator && mapping variant = mapping[data[discriminator]] return variant.from_json(data) if variant.respond_to?(:from_json) return data if data.key?(discriminator) end variant = best_variant(data, candidates(data, variants)) variant ? variant.from_json(data) : data end |
.union_module?(variant) ⇒ Boolean
Returns whether the variant is a generated union factory module.
128 129 130 131 |
# File 'lib/pago/serde.rb', line 128 def union_module?(variant) variant.is_a?(Module) && !variant.is_a?(Class) && variant.respond_to?(:variants) && variant.respond_to?(:mapping) end |