Class: XRPL::Model

Inherits:
Object
  • Object
show all
Defined in:
lib/xrpl/model.rb

Overview

Common ground for the objects definitions.json describes field by field: transactions (TRANSACTION_FORMATS) and ledger entries (LEDGER_ENTRY_FORMATS).

A family - XRPL::Transaction, XRPL::LedgerEntry - is a subclass that names the type field ("TransactionType", "LedgerEntryType") and calls .define_types! with the right tables. That creates one class per entry in the table, with an accessor for every field the type accepts and a constant for every flag it defines. None of them is written by hand, so syncing definitions.json updates them all and they cannot drift.

Fields are written in snake_case and stored under the ledger's own PascalCase names, so #to_h hands the binary codec exactly what it expects.

Direct Known Subclasses

LedgerEntry, Transaction

Defined Under Namespace

Classes: ValidationError

Constant Summary collapse

REQUIRED =

rippled's SOEStyle: what the format says about a field.

0
OPTIONAL =
1
DEFAULT =
2
DEFINITIONS =
BinaryCodec::Definitions.instance.raw
FIELD_TO_ACCESSOR =
DEFINITIONS['FIELDS'].to_h { |name, _| [name, underscore(name)] }.freeze
ACCESSOR_TO_FIELD =
FIELD_TO_ACCESSOR.invert.freeze

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(fields = {}) ⇒ Model

Returns a new instance of Model.



140
141
142
143
# File 'lib/xrpl/model.rb', line 140

def initialize(fields = {})
  @fields = {}
  fields.each { |name, value| self[name] = value }
end

Class Attribute Details

.flags ⇒ Object (readonly)

Flag name -> bit, e.g. "tfPartialPayment" => 131072.



58
59
60
# File 'lib/xrpl/model.rb', line 58

def flags
  @flags
end

.format ⇒ Object (readonly)

Ledger field name -> optionality, including the common fields.



55
56
57
# File 'lib/xrpl/model.rb', line 55

def format
  @format
end

.type_name ⇒ Object (readonly)

The ledger's name for this type, e.g. "Payment" or "AccountRoot".



52
53
54
# File 'lib/xrpl/model.rb', line 52

def type_name
  @type_name
end

Class Method Details

.define_types!(formats, flags, flag_aliases = {}) ⇒ Object

Builds one subclass per type in formats, with an accessor for every field the type accepts and a constant for every flag it defines.

Parameters:

  • formats (Hash) —

    a *_FORMATS table, with its "common" entry

  • flags (Hash) —

    the matching *_FLAGS table

  • flag_aliases (Hash) (defaults to: {}) —

    type name -> key in flags, where they differ



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/xrpl/model.rb', line 107

def define_types!(formats, flags, flag_aliases = {})
  common = formats.fetch('common')

  formats.each do |type, own_format|
    next if type == 'common'

    format = (common + own_format)
             .to_h { |field| [field['name'], field['optionality']] }
             .freeze

    klass = Class.new(self)
    klass.instance_variable_set(:@type_name, type)
    klass.instance_variable_set(:@format, format)
    klass.instance_variable_set(:@flags, (flags[flag_aliases.fetch(type, type)] || {}).freeze)

    format.each_key do |field|
      next if field == type_field

      accessor = FIELD_TO_ACCESSOR[field] or next

      klass.define_method(accessor) { self[field] }
      klass.define_method("#{accessor}=") { |value| self[field] = value }
    end

    klass.flags.each do |flag, bit|
      klass.const_set(underscore(flag).upcase, bit)
    end

    const_set(type, klass)
  end
end

.flag_bit(flag) ⇒ Object

The bit for a flag given by ledger name, constant name or bit.



213
214
215
216
217
218
219
# File 'lib/xrpl/model.rb', line 213

def self.flag_bit(flag)
  return flag if flag.is_a?(Integer)

  key = flag.to_s
  _, bit = flags.find { |name, _| name == key || underscore(name) == key.downcase }
  bit or raise ArgumentError, "#{type_name} has no flag #{flag}"
end

.for(type) ⇒ Object

The class for a type name, or nil if the ledger has no such type.



78
79
80
# File 'lib/xrpl/model.rb', line 78

def for(type)
  const_get(type) if const_defined?(type, false)
end

.from(hash) ⇒ Object

Build the right subclass from a hash, PascalCase or snake.

Raises:



83
84
85
86
87
88
89
90
91
# File 'lib/xrpl/model.rb', line 83

def from(hash)
  type = hash[type_field] || hash[type_field.to_sym] || hash[underscore(type_field).to_sym]
  raise ValidationError, "#{label.capitalize} hash has no #{type_field}" unless type

  klass = self.for(type.to_s)
  raise ValidationError, "Unknown #{label} type #{type}" unless klass

  klass.new(hash)
end

.label ⇒ Object

How the family reads in an error message, e.g. "transaction".

Raises:

  • (NotImplementedError)


67
68
69
# File 'lib/xrpl/model.rb', line 67

def label
  raise NotImplementedError, "#{name}.label"
end

.resolve(name) ⇒ Object

Translates an accessor name to the ledger's field name.



94
95
96
97
98
99
# File 'lib/xrpl/model.rb', line 94

def resolve(name)
  key = name.to_s
  return key if FIELD_TO_ACCESSOR.key?(key)

  ACCESSOR_TO_FIELD[key] || key
end

.supplied_later ⇒ Object

Required by the format, but supplied by a later step (autofill, signing) rather than by the caller. Not demanded by #validate!.



73
74
75
# File 'lib/xrpl/model.rb', line 73

def supplied_later
  []
end

.type_field ⇒ Object

The field that names the type: "TransactionType" or "LedgerEntryType". A family defines it.

Raises:

  • (NotImplementedError)


62
63
64
# File 'lib/xrpl/model.rb', line 62

def type_field
  raise NotImplementedError, "#{name}.type_field"
end

.underscore(name) ⇒ Object

Ledger field name -> snake_case accessor, and back.

XChain, NFToken and MPToken are brand names rather than acronyms, so they are folded to a single word the way the reference SDKs write them.



32
33
34
35
36
37
38
39
40
41
# File 'lib/xrpl/model.rb', line 32

def self.underscore(name)
  name
    .sub(/\AXChain/, 'Xchain')
    .sub(/\ANFToken/, 'Nftoken')
    .sub(/\AMPToken/, 'Mptoken')
    .gsub(/([A-Z]{2,})s(?=[A-Z]|\z)/) { "#{Regexp.last_match(1).capitalize}s" }
    .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
    .gsub(/([a-z\d])([A-Z])/, '\1_\2')
    .downcase
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



226
227
228
# File 'lib/xrpl/model.rb', line 226

def ==(other)
  other.is_a?(Model) && other.to_h == to_h
end

#[](name) ⇒ Object

Reads a field by accessor name, symbol or ledger name.



146
147
148
# File 'lib/xrpl/model.rb', line 146

def [](name)
  @fields[self.class.resolve(name)]
end

#[]=(name, value) ⇒ Object

Writes a field, rejecting anything the type does not define. The type field itself is accepted, and checked, so a hash read off the ledger can be passed in whole.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/xrpl/model.rb', line 153

def []=(name, value)
  field = self.class.resolve(name)

  if field == self.class.type_field
    return if value.to_s == self.class.type_name

    raise ValidationError, "#{self.class.type_name} cannot carry #{field} #{value}"
  end

  unless self.class.format.key?(field)
    raise ValidationError, "#{self.class.type_name} has no field #{field}"
  end

  value.nil? ? @fields.delete(field) : @fields[field] = value
end

#flag?(flag) ⇒ Boolean

Whether a flag is set in Flags. Takes the ledger's name ("lsfDefaultRipple"), the constant's name (:lsf_default_ripple) or the bit itself.

Returns:

  • (Boolean)


202
203
204
# File 'lib/xrpl/model.rb', line 202

def flag?(flag)
  (self['Flags'].to_i & self.class.flag_bit(flag)) != 0
end

#flag_names ⇒ Object

The names of the flags set in Flags, in the ledger's spelling.



207
208
209
210
# File 'lib/xrpl/model.rb', line 207

def flag_names
  value = self['Flags'].to_i
  self.class.flags.select { |_, bit| (value & bit) != 0 }.keys
end

#hash ⇒ Object



231
232
233
# File 'lib/xrpl/model.rb', line 231

def hash
  to_h.hash
end

#inspect ⇒ Object



235
236
237
# File 'lib/xrpl/model.rb', line 235

def inspect
  "#<#{self.class.name} #{to_h.inspect}>"
end

#missing_fields ⇒ Object

Fields the format requires that have not been set, ignoring the ones a later step provides.



178
179
180
181
182
183
184
# File 'lib/xrpl/model.rb', line 178

def missing_fields
  self.class.format
      .select { |_, optionality| optionality == REQUIRED }
      .keys
      .reject { |field| field == self.class.type_field }
      .reject { |field| self.class.supplied_later.include?(field) || @fields.key?(field) }
end

#to_blob ⇒ Object

The serialised object, as hex.



222
223
224
# File 'lib/xrpl/model.rb', line 222

def to_blob
  BinaryCodec.json_to_binary(to_h)
end

#to_h ⇒ Object Also known as: to_hash

The object as the binary codec wants it: ledger field names, with the type field filled in.



171
172
173
# File 'lib/xrpl/model.rb', line 171

def to_h
  { self.class.type_field => self.class.type_name }.merge(@fields)
end

#valid? ⇒ Boolean

Returns:

  • (Boolean)


186
187
188
# File 'lib/xrpl/model.rb', line 186

def valid?
  missing_fields.empty?
end

#validate! ⇒ Object

Raises unless every required field is present.

Raises:



191
192
193
194
195
196
197
# File 'lib/xrpl/model.rb', line 191

def validate!
  missing = missing_fields
  return self if missing.empty?

  raise ValidationError,
        "#{self.class.type_name} is missing #{missing.join(', ')}"
end