Class: Pluggy::PluggyObject
- Inherits:
-
Object
- Object
- Pluggy::PluggyObject
- Defined in:
- lib/pluggy/pluggy_object.rb
Overview
Base value object wrapping an API JSON payload.
The access contract, which is the one thing worth memorising:
txn.date / txn[:date] => Time (coerced Ruby view)
txn["date"] / txn["createdAt"] => String (verbatim wire value)
txn.to_h => wire values, nested objects flattened
Symbol keys and readers give you the coerced Ruby value; String keys read the original payload and accept the original camelCase spelling.
Fields are declared once per class with the fields DSL rather than
defined per instance (Stripe's approach), because a 500-transaction page
would otherwise mean ~11,500 define_method calls per response. Undeclared
keys are still kept and still reachable -- via method_missing and via []
-- so fields Pluggy adds after this gem ships are not lost.
Deliberately does NOT include Enumerable: iterating a Transaction and getting its own field values is a confusing API, and it would collide with ICountResponse#count. Only the list objects are Enumerable.
Direct Known Subclasses
Defined Under Namespace
Classes: RawNumber
Constant Summary collapse
- RESERVED =
Overriding any of these would break the object itself.
Set.new(%w[ class send __send__ public_send object_id __id__ method methods respond_to? respond_to_missing? instance_variable_get instance_variable_set instance_variables singleton_class is_a? kind_of? instance_of? nil? tap then itself extend display equal? to_h to_hash to_s to_json as_json inspect keys values each_pair [] == eql? hash dup clone freeze frozen? initialize method_missing client read key? ]).freeze
- TEMPORAL_KEYS =
Time coercion is keyed on the wire field name, guarded by the value's shape. A blanket ISO-8601 sniff would be dangerous -- a descriptionRaw reading "2020-10-15" would silently become a Time.
Set.new(%w[ date createdAt updatedAt lastUpdatedAt nextAutoSyncAt consentExpiresAt expiresAt dueDate billClosingDate contractDate settlementDate firstInstallmentDueDate paymentDate purchaseDate balanceCloseDate balanceDueDate updateDateTime issueDate expirationDate paidDate ]).freeze
- ISO8601 =
Anchored, and requires a full date. This is what keeps
monthYearandbillForecastDate("2024-03") as Strings, andinstallmentPeriodicity("MES") untouched. /\A\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?\z/
Instance Attribute Summary collapse
-
#client ⇒ Object
readonly
Returns the value of attribute client.
Class Method Summary collapse
- .declared_fields ⇒ Object
- .define_field(wire) ⇒ Object
-
.fields(*names) ⇒ Object
fields :id, :descriptionRaw, "CET".
-
.nested(map) ⇒ Object
nested connector: Connector, financeCharges: BillFinanceCharge.
-
.nested_types ⇒ Object
Wire field name => nested class, inherited by subclasses.
Instance Method Summary collapse
- #==(other) ⇒ Object (also: #eql?)
-
#[](key) ⇒ Object
String key => verbatim wire value (accepts the camelCase spelling).
-
#as_json ⇒ Object
JSON.generate serialises a BigDecimal as a quoted string ('"amount":"-0"amount":"-0.21245e3"'), which would break round-tripping.
- #each_pair ⇒ Object
- #hash ⇒ Object
-
#initialize(values = {}, client: nil, **extra) ⇒ PluggyObject
constructor
Accepts the payload either braced or as a bare trailing hash.
- #inspect ⇒ Object
- #key?(key) ⇒ Boolean
- #keys ⇒ Object
-
#method_missing(name, *args) ⇒ Object
Forward-compat path for fields Pluggy adds after this gem ships, and for the undeclared-but-real ones (Bill#accountId, Item#clientUserId, Connector#isSandbox).
-
#read(wire) ⇒ Object
Coerced read, memoised.
- #respond_to_missing?(name, include_private = false) ⇒ Boolean
-
#to_h ⇒ Object
(also: #to_hash)
Wire-shaped hash: nested objects flattened back to plain hashes.
- #to_json(*args) ⇒ Object
- #values ⇒ Object
Constructor Details
#initialize(values = {}, client: nil, **extra) ⇒ PluggyObject
Accepts the payload either braced or as a bare trailing hash. Ruby routes
an unbraced hash into **extra (String keys included), so
Transaction.new("id" => "x") and Transaction.new({"id" => "x"}) and
Transaction.new(payload, client: c) all work.
Keys are wire names -- "createdAt", not :created_at.
106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/pluggy/pluggy_object.rb', line 106 def initialize(values = {}, client: nil, **extra) @client = client @values = {} @coerced = {} source = values.nil? || values.empty? ? extra : values.merge(extra) source.each do |key, value| k = key.to_s @values[k] = convert(k, value) end end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(name, *args) ⇒ Object
Forward-compat path for fields Pluggy adds after this gem ships, and for the undeclared-but-real ones (Bill#accountId, Item#clientUserId, Connector#isSandbox).
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/pluggy/pluggy_object.rb', line 182 def method_missing(name, *args) n = name.to_s return super if n.end_with?("=", "!") || !args.empty? if n.end_with?("?") base = n.delete_suffix("?") wire = resolve_wire(base) return !read(wire).nil? && read(wire) != false if wire end wire = resolve_wire(n) return read(wire) if wire super end |
Instance Attribute Details
#client ⇒ Object (readonly)
Returns the value of attribute client.
98 99 100 |
# File 'lib/pluggy/pluggy_object.rb', line 98 def client @client end |
Class Method Details
.declared_fields ⇒ Object
61 62 63 64 |
# File 'lib/pluggy/pluggy_object.rb', line 61 def declared_fields @declared_fields ||= superclass.respond_to?(:declared_fields) ? superclass.declared_fields.dup : Set.new end |
.define_field(wire) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/pluggy/pluggy_object.rb', line 83 def define_field(wire) declared_fields << wire ruby = Util.snake_case(wire) # Genuinely unusable names ("2fa", "foo-bar") stay reachable via []. return unless ruby.match?(/\A[a-z_][a-zA-Z0-9_]*\z/) return if RESERVED.include?(ruby) define_method(ruby) { read(wire) } # camelCase alias, e.g. loan.CET alongside loan.cet define_method(wire) { read(wire) } unless wire == ruby || RESERVED.include?(wire) end |
.fields(*names) ⇒ Object
fields :id, :descriptionRaw, "CET"
Pass wire names (camelCase or otherwise); readers are generated in snake_case, with a camelCase alias so code transliterated from Pluggy's own docs also works.
71 72 73 |
# File 'lib/pluggy/pluggy_object.rb', line 71 def fields(*names) names.flatten.each { |n| define_field(n.to_s) } end |
.nested(map) ⇒ Object
nested connector: Connector, financeCharges: BillFinanceCharge
76 77 78 79 80 81 |
# File 'lib/pluggy/pluggy_object.rb', line 76 def nested(map) map.each do |wire, klass| nested_types[Util.wire_key(wire)] = klass define_field(Util.wire_key(wire)) end end |
.nested_types ⇒ Object
Wire field name => nested class, inherited by subclasses.
57 58 59 |
# File 'lib/pluggy/pluggy_object.rb', line 57 def nested_types @nested_types ||= superclass.respond_to?(:nested_types) ? superclass.nested_types.dup : {} end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
167 168 169 |
# File 'lib/pluggy/pluggy_object.rb', line 167 def ==(other) other.is_a?(self.class) && other.to_h == to_h end |
#[](key) ⇒ Object
String key => verbatim wire value (accepts the camelCase spelling). Symbol key => coerced value, same as the reader.
129 130 131 132 133 134 135 136 137 |
# File 'lib/pluggy/pluggy_object.rb', line 129 def [](key) if key.is_a?(Symbol) wire = @values.key?(key.to_s) ? key.to_s : Util.camel_case(key) read(wire) else k = key.to_s @values.key?(k) ? @values[k] : @values[Util.camel_case(k)] end end |
#as_json ⇒ Object
JSON.generate serialises a BigDecimal as a quoted string ('Pluggy::PluggyObject."amount":"-0"amount":"-0.21245e3"'), which would break round-tripping. Wrap them so they render as unquoted numbers matching the original wire literal.
159 160 161 |
# File 'lib/pluggy/pluggy_object.rb', line 159 def as_json(*) deep_render(to_h) end |
#each_pair ⇒ Object
146 |
# File 'lib/pluggy/pluggy_object.rb', line 146 def each_pair(&) = @values.each_pair(&) |
#hash ⇒ Object
172 |
# File 'lib/pluggy/pluggy_object.rb', line 172 def hash = to_h.hash |
#inspect ⇒ Object
174 175 176 177 |
# File 'lib/pluggy/pluggy_object.rb', line 174 def inspect id = @values["id"] "#<#{self.class.name}#{":#{id}" if id} #{@values.keys.join(" ")}>" end |
#key?(key) ⇒ Boolean
139 140 141 142 |
# File 'lib/pluggy/pluggy_object.rb', line 139 def key?(key) k = key.to_s @values.key?(k) || @values.key?(Util.camel_case(k)) end |
#keys ⇒ Object
144 |
# File 'lib/pluggy/pluggy_object.rb', line 144 def keys = @values.keys |
#read(wire) ⇒ Object
Coerced read, memoised. Used by every generated accessor.
120 121 122 123 124 125 |
# File 'lib/pluggy/pluggy_object.rb', line 120 def read(wire) return @coerced[wire] if @coerced.key?(wire) raw = @values[wire] @coerced[wire] = TEMPORAL_KEYS.include?(wire) ? coerce_time(raw) : raw end |
#respond_to_missing?(name, include_private = false) ⇒ Boolean
198 199 200 201 |
# File 'lib/pluggy/pluggy_object.rb', line 198 def respond_to_missing?(name, include_private = false) n = name.to_s.sub(/[?]\z/, "") !resolve_wire(n).nil? || super end |
#to_h ⇒ Object Also known as: to_hash
Wire-shaped hash: nested objects flattened back to plain hashes. Values
are the parsed ones (so amounts are BigDecimal), which is what as_json
then renders correctly.
151 152 153 |
# File 'lib/pluggy/pluggy_object.rb', line 151 def to_h @values.transform_values { |v| unwrap(v) } end |
#to_json(*args) ⇒ Object
163 164 165 |
# File 'lib/pluggy/pluggy_object.rb', line 163 def to_json(*args) as_json.to_json(*args) end |
#values ⇒ Object
145 |
# File 'lib/pluggy/pluggy_object.rb', line 145 def values = @values.values |