Class: Shep::Entity

Inherits:
Object
  • Object
show all
Defined in:
lib/shep/entity_base.rb

Overview

Abstract base class for Mastodon objects.

Mastodon provides its content as JSON hashes with documented names and values. Shep takes this one step further and provides a class for each object type. These are similar to Ruby’s ‘Struct` but are also strongly typed.

Typing is primarily useful for converting things that don’t have explicit JSON types (e.g. Time, URI) into Ruby types. However, it will also catch the case where you’re trying to set a field to something with the wrong type.

Supported types are:

  • Number - (Integer but also allows Float)

  • Boolean

  • String

  • URI - (a Ruby URI object)

  • Time - parsed from and converted to ISO8601-format strings

  • Entity - an arbitrary Entity subclass

  • Array - strongly typed array of any of the above types

Fields may also be set to nil, except for ‘Array` which must instead be set to an ampty array.

Entities can be converted to and from Ruby Hashes. For this, we provide two flavours of Hash: the regular Ruby Hash where values are just the Ruby objects and the JSON hash where everything has been converted to the types expected by a Mastodon server.

For JSON hashes, ‘Time` objects become ISO8601-formatted strings, `URI` objects become strings containing the url and `Entity` subobjects become their own JSON hashes. (Note that conversion to JSON hashes isn’t really used outside of some testing and internal stuff so I don’t guarantee that a Mastodon server or client will accept them.)

Normally, we care about initializing Entity objects from the corresponding parsed JSON object and produce Ruby hashes when we need to use a feature ‘Hash` provides.

Subclasses are all defined inside the Entity namespace so that it groups nicely in YARD docs (and because it makes the intent obvious).

Defined Under Namespace

Classes: Account, Context, CustomEmoji, MediaAttachment, Notification, Status, StatusSource, Status_Application, Status_Mention, Status_Tag

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeEntity

Default constructor; creates an empty instance. You’ll probably want to use with or from instead.



62
63
64
# File 'lib/shep/entity_base.rb', line 62

def initialize
  init_fields()
end

Class Method Details

.fields(*flds) ⇒ 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.

Cool metaprogramming thing for defining Shep::Entity subclasses.

A typical Shep::Entity subclass should contain only a call to this method. For example:

class Thingy < Entity
    fields(
        :id,        %i{show},       StringBox,
        :timestamp,                 TimeBox,
        :count,     %i{show},       NumBox,
        :url,                       URIBox,
    )
end

fields takes a variable sequence of arguments that must be grouped as follows:

 1. The field name. This **must** be a symbol.

 2. An optional Array containing the symbol :show.  If given,
    this field will be included in the string returned by
    `to_s`.  (This is actually a mechanism for setting various
    properties, but all we need is `:show`, so that's it for
    now.)

 3. The type specifier.  If omitted, defaults to StringBox.

The type specifier must be either:

 1. One of the following classes: `TypeBox`, `StringBox`,
    `TimeBox`, `URIBox`, or `NumBox`, corresponding to the type
    this field will be.

 2. A subclass of Entity, indicating that this field holds
    another Mastodon object.

 3. An Array holding a single element which must be one of the
    above classes, indicating that the field holds an array of
    items of that type.


281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/shep/entity_base.rb', line 281

def self.fields(*flds)
  known_props = %i{show}.to_set

  names_and_types = []
  notables = []
  until flds.empty?
    name = flds.shift
    assert{ name.class == Symbol }

    properties = Set.new
    if flds[0].is_a?(Array) && flds[0][0].is_a?(Symbol)
      properties += flds[0]

      assert("Unknown properti(es): #{(properties - known_props).to_a}") {
        (properties - known_props).empty?
      }

      flds.shift
    end

    notables.push(name) if properties.include? :show

    if flds[0] && flds[0].class != Symbol
      typefld = flds.shift

      # Array means ArrayBox with the element as type
      if typefld.is_a? Array
        assert{typefld.size == 1 && typefld[0].is_a?(Class)}
        atype = typefld[0]

        # If this is an array of entity boxes, handle that
        atype = EntityBox.wrapping(atype) if atype < Entity

        type = ArrayBox.wrapping(atype)

      elsif typefld.is_a?(Class) && typefld < Entity
        type = EntityBox.wrapping(typefld)

      elsif typefld.is_a?(Class) && typefld < TypeBox
        type = typefld

      else
        raise Error::Caller.new("Unknown field type '#{typefld}'")
      end
    else
      type = StringBox
    end

    add_fld(name, type)
    names_and_types.push [name, type]
  end

  names_and_types.freeze
  notables.freeze

  add_init(names_and_types)
  make_has_name(names_and_types)
  make_disp_fields(notables)
  make_keys(names_and_types)

  # This gets used to generate documentation so we make it private
  # and (ab)use Object.send to call it later
  define_singleton_method(:names_and_types) { names_and_types }
  singleton_class.send(:private, :names_and_types)
end

.from(json_hash) ⇒ Object

Construct an instance from the (parsed) JSON object returned by Mastodon.

Values must be of the expected types as they appear in the Mastodon object (i.e. the JSON Hash described above). Missing key/value pairs are allowed and treated as nil; unknown keys are ignored.



87
88
# File 'lib/shep/entity_base.rb', line 87

def self.from(json_hash) =
new.set_from_hash!(json_hash, ignore_unknown: true, from_json: true)

.with(**fields) ⇒ Object

Construct an instance initialized with Ruby objects.

This intended for creating Shep::Entity subobjects in Ruby code. Keys of fields must correspond to the class’s supported fields and be of the correct type. No fields may be omitted.



75
76
77
78
# File 'lib/shep/entity_base.rb', line 75

def self.with(**fields) =
      new.set_from_hash!(fields,
ignore_unknown: false,
from_json: false)

Instance Method Details

#==(other) ⇒ Boolean

Compare for equality.

Two Entity subinstances are identical if they are of the same class and all of their field values are also equal according to ‘:==`

Returns:

  • (Boolean)


181
182
183
184
185
# File 'lib/shep/entity_base.rb', line 181

def ==(other)
  return false unless self.class == other.class
  keys.each{|k| return false unless self[k] == other[k] }
  return true
end

#[](key) ⇒ Object

Retrieve a field value by name

Parameters:

  • key (String, Symbol)

Returns:

  • (Object)


161
# File 'lib/shep/entity_base.rb', line 161

def [](key) = getbox(key).get

#[]=(key, value) ⇒ Object

Set a field value by name

Parameters:

  • key (String, Symbol)
  • value (Object)

Returns:

  • (Object)

    the ‘value` parameter



169
170
171
172
# File 'lib/shep/entity_base.rb', line 169

def []=(key, value)
  getbox(key).set(value)
  return value
end

Wrapper around ‘puts to_long_s()`



232
# File 'lib/shep/entity_base.rb', line 232

def print = puts(to_long_s)

#set_from_hash!(some_hash, ignore_unknown: false, from_json: false) ⇒ Object

Set all fields from a hash.

This is the back-end for from and with.

Parameters:

  • some_hash (Hash)

    the Hash containing the contents

  • ignore_unknown (Bool) (defaults to: false)

    if false, unknown keys cause an error

  • from_json (Bool) (defaults to: false)

    if true, expect values in the format provided by the Mastodon API and convert accordingly. Otherwise, expect Ruby types.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/shep/entity_base.rb', line 103

def set_from_hash!(some_hash, ignore_unknown: false, from_json: false)
  some_hash.each do |key, value|
    key = key.intern
    unless has_fld?(key)
      raise Error::Caller.new("Unknown field: '#{key}'!") unless
        ignore_unknown
      next
    end

    if from_json
      getbox(key).set_from_json(value)
    else
      self.send("#{key}=".intern, value)
    end
  end

  return self
end

#to_h(json_compatible = false) ⇒ Hash

Return a hash of the contents mapping field name to value.

If ‘json_compatible` is true, the resulting hash will be easily convertable to Mastodon-format JSON. See above.

Unset (i.e. nil) values appear as entries with nil values.

Parameters:

  • json_compatible (Boolean) (defaults to: false)

    if true, convert to JSON-friendly form

Returns:

  • (Hash)


197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/shep/entity_base.rb', line 197

def to_h(json_compatible = false)
  result = {}

  keys.each{|name|
    hkey = json_compatible ? name.to_s : name

    box = getbox(name)
    val = json_compatible ? box.get_for_json : box.get

    result[hkey] = val
  }

  result
end

#to_long_s(indent_level = 0) ⇒ String

Produce a long-form human-friendly description of this Entity.

This is mostly here for debugging.

Returns:

  • (String)


217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/shep/entity_base.rb', line 217

def to_long_s(indent_level = 0)
  name_pad = keys.map(&:size).max + 2

  result = keys.map do |key|
    line = " " * (indent_level * 2)
    line += sprintf("%-*s", name_pad, "#{key}:")

    val = self[key]
    line += val.is_a?(Entity) ? val.to_long_s(indent_level + 1) : val.to_s
  end

  return result.join("\n")
end

#to_sString Also known as: inspect

Produce a short human-friendly description.

Returns:

  • (String)


130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/shep/entity_base.rb', line 130

def to_s
  maxlen = 20
  notable = self.disp_fields()
    .reject{|fld| getbox(fld).get_for_json.to_s.empty? }
    .map{|fld|
      valtxt = getbox(fld).get
      valtxt = valtxt[0..maxlen] + '...' unless valtxt.size < maxlen+3
      "#{fld}=#{valtxt}"
    }
    .join(",")
  notable = "0x#{self.object_id.to_s(16)}" if notable.empty?

  "#{self.class}<#{notable}>"
end