Class: Pluggy::PluggyObject

Inherits:
Object
  • Object
show all
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

APIResource

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 monthYear and billForecastDate ("2024-03") as Strings, and installmentPeriodicity ("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

Class Method Summary collapse

Instance Method Summary collapse

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

#clientObject (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_fieldsObject



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_typesObject

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_jsonObject

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_pairObject



146
# File 'lib/pluggy/pluggy_object.rb', line 146

def each_pair(&) = @values.each_pair(&)

#hashObject



172
# File 'lib/pluggy/pluggy_object.rb', line 172

def hash = to_h.hash

#inspectObject



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

Returns:

  • (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

#keysObject



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

Returns:

  • (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_hObject 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

#valuesObject



145
# File 'lib/pluggy/pluggy_object.rb', line 145

def values = @values.values