Class: Forem::ForemObject

Inherits:
Object
  • Object
show all
Includes:
APIOperations::Request
Defined in:
lib/forem/forem_object.rb

Overview

Base object class for all Forem API objects. Provides dynamic attribute access from API response data.

Attributes are stored internally as a plain Hash with string keys. They can be read and written using either method-call syntax (obj.title) or hash-subscript syntax (obj). Nested hashes are recursively converted to ForemObject instances via Util.convert_to_forem_object.

Examples:

Constructing from an API response hash

obj = Forem::ForemObject.construct_from({ "id" => 1, "title" => "Hello" })
obj.id      #=> 1
obj.title   #=> "Hello"
obj["title"] #=> "Hello"

Writing attributes

obj.title = "Updated"
obj["title"] = "Updated again"

Direct Known Subclasses

APIResource

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from APIOperations::Request

included, #request

Constructor Details

#initialize(values = {}) ⇒ ForemObject

Initialise a new object, optionally pre-populating it with values.

Parameters:

  • values (Hash) (defaults to: {})

    initial attribute hash. Keys are coerced to strings.



115
116
117
118
119
# File 'lib/forem/forem_object.rb', line 115

def initialize(values = {})
  @values = {}
  @requestor = nil
  update_attributes(values)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args) ⇒ Object

Dynamic getter/setter for API attributes.

  • obj.title — returns @values
  • obj.title = x — delegates to #[]=

Raises NoMethodError for names that are neither setters nor present in the values hash.

Parameters:

  • method (Symbol)

    the missing method name.

  • args (Array)

    arguments (used only for setter calls).

Returns:

  • (Object)

    the attribute value for getters.

Raises:

  • (NoMethodError)

    if the attribute does not exist in the values hash and it is not a setter call.



230
231
232
233
234
235
236
237
238
239
240
# File 'lib/forem/forem_object.rb', line 230

def method_missing(method, *args)
  name = method.to_s
  if name.end_with?("=")
    attr = name.chomp("=")
    self[attr] = args[0]
  elsif @values.key?(name)
    @values[name]
  else
    super
  end
end

Instance Attribute Details

#requestorAPIRequestor?

Returns the requestor that produced this object, used by instance methods (e.g. APIOperations::Save#save) when no explicit :requestor option is supplied.

Returns:



26
27
28
# File 'lib/forem/forem_object.rb', line 26

def requestor
  @requestor
end

Class Method Details

.construct_from(values, requestor: nil) ⇒ ForemObject

Construct a new Forem::ForemObject from an API response hash.

This is the preferred factory method — it is called by Util and resource class methods rather than #initialize directly.

Examples:

obj = Forem::ForemObject.construct_from({ "id" => 42, "user" => { "name" => "Alice" } })
obj.user.name  #=> "Alice"

Parameters:

  • values (Hash)

    the API response data. Nested Hashes and Arrays are recursively converted via Util.convert_to_forem_object.

  • requestor (APIRequestor, nil) (defaults to: nil)

    the requestor to attach so that subsequent instance methods (e.g. article.save) can issue follow-up calls without having to be passed a :requestor opt explicitly.

Returns:

  • (ForemObject)

    a new object populated with the given attributes.



137
138
139
140
141
142
# File 'lib/forem/forem_object.rb', line 137

def self.construct_from(values, requestor: nil)
  obj = new
  obj.send(:update_attributes, values)
  obj.requestor = requestor
  obj
end

.cursor_list(path, params = {}, opts = {}, cursor_param: :after, cursor_from: ->(item) { item["id"] }) ⇒ ListObject

Wrap a cursor-based ("after") GET-array endpoint as a ListObject.

Used by endpoints (e.g. survey poll votes) that use an +after+-style cursor — successive pages are fetched by passing the last seen ID as the cursor. Cursor pagination is forward-only; ListObject#previous_page returns nil.

Parameters:

  • path (String)

    the API path to GET.

  • params (Hash) (defaults to: {})

    query parameters (filters and per_page).

  • opts (Hash) (defaults to: {})

    per-request options including :requestor.

  • cursor_param (Symbol) (defaults to: :after)

    the query-string key for the cursor (default :after).

  • cursor_from (Proc) (defaults to: ->(item) { item["id"] })

    a callable returning the cursor value for an item (default extracts "id").

Returns:



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/forem/forem_object.rb', line 84

def self.cursor_list(path, params = {}, opts = {}, cursor_param: :after, cursor_from: ->(item) { item["id"] })
  requestor = opts[:requestor]
  resp = request(:get, path, params, opts)
  per_page = (params[:per_page] || params["per_page"] || 30).to_i
  klass = self
  data = (resp.parsed_body || []).map { |item| klass.construct_from(item, requestor: requestor) }

  fetcher = lambda do |direction, state, extra|
    return nil unless direction == :next
    return nil if state.data.empty?
    next_cursor = cursor_from.call(state.data.last)
    new_params = state.filters.merge(cursor_param => next_cursor, per_page: state.per_page).merge(extra)
    klass.cursor_list(path, new_params, { requestor: state.requestor },
                      cursor_param: cursor_param, cursor_from: cursor_from)
  end

  ListObject.new(
    data: data,
    current_page: nil,
    per_page: per_page,
    resource_class: klass,
    filters: params.reject { |k, _| [cursor_param, cursor_param.to_s, :per_page, "per_page"].include?(k) },
    requestor: requestor,
    fetcher: fetcher
  )
end

.paginated_list(path, params = {}, opts = {}) ⇒ ListObject

Wrap a custom-path GET-array endpoint as a page-based ListObject.

Used by resource methods that hit a non-standard path (e.g. /api/articles/me/published) but otherwise behave like APIOperations::List#list: page-based pagination with +page+/+per_page+ query params, returning a JSON array of resource objects.

The returned ListObject re-uses this same helper for #next_page and #previous_page, so pagination works without any further wiring.

Parameters:

  • path (String)

    the API path to GET.

  • params (Hash) (defaults to: {})

    query parameters (filters and pagination).

  • opts (Hash) (defaults to: {})

    per-request options including :requestor.

Returns:



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/forem/forem_object.rb', line 43

def self.paginated_list(path, params = {}, opts = {})
  requestor = opts[:requestor]
  resp = request(:get, path, params, opts)
  per_page = (params[:per_page] || params["per_page"] || 30).to_i
  page = (params[:page] || params["page"] || 1).to_i
  klass = self
  data = (resp.parsed_body || []).map { |item| klass.construct_from(item, requestor: requestor) }

  fetcher = lambda do |direction, state, extra|
    target = direction == :next ? state.current_page + 1 : state.current_page - 1
    return nil if target < 1
    new_params = state.filters.merge(page: target, per_page: state.per_page).merge(extra)
    klass.paginated_list(path, new_params, { requestor: state.requestor })
  end

  ListObject.new(
    data: data,
    current_page: page,
    per_page: per_page,
    resource_class: klass,
    filters: params.reject { |k, _| [:page, :per_page, "page", "per_page"].include?(k) },
    requestor: requestor,
    fetcher: fetcher
  )
end

Instance Method Details

#==(other) ⇒ Boolean

Compare two Forem::ForemObject instances by their internal value hash.

Parameters:

  • other (Object)

    the object to compare against.

Returns:



199
200
201
# File 'lib/forem/forem_object.rb', line 199

def ==(other)
  other.is_a?(ForemObject) && @values == other.instance_variable_get(:@values)
end

#[](key) ⇒ Object?

Read an attribute by key.

Examples:

obj["id"]    #=> 42
obj[:title]  #=> "Hello"

Parameters:

  • key (String, Symbol)

    the attribute name. Symbols are coerced to strings before lookup.

Returns:

  • (Object, nil)

    the stored value, or nil if the key is absent.



153
154
155
# File 'lib/forem/forem_object.rb', line 153

def [](key)
  @values[key.to_s]
end

#[]=(key, value) ⇒ Object

Write an attribute by key.

The value is passed through Util.convert_to_forem_object so nested Hashes become Forem::ForemObject instances automatically.

Examples:

obj["title"] = "New title"
obj[:count]  = 5

Parameters:

  • key (String, Symbol)

    the attribute name. Symbols are coerced to strings.

  • value (Object)

    the value to store.

Returns:

  • (Object)

    the stored (possibly converted) value.



170
171
172
# File 'lib/forem/forem_object.rb', line 170

def []=(key, value)
  @values[key.to_s] = Util.convert_to_forem_object(value)
end

#inspectString

Return a human-readable string representation of the object.

Returns:

  • (String)

    the class name, object ID, and internal values hash.



245
246
247
# File 'lib/forem/forem_object.rb', line 245

def inspect
  "#<#{self.class}:0x#{object_id.to_s(16)} #{@values.inspect}>"
end

#respond_to_missing?(method, include_private = false) ⇒ Boolean

Allow respond_to? checks for dynamic attribute accessors.

Returns true for any key currently stored in the internal values hash, as well as the corresponding setter (e.g. title=).

Parameters:

  • method (Symbol)

    the method name being queried.

  • include_private (Boolean) (defaults to: false)

    whether to include private methods.

Returns:

  • (Boolean)


211
212
213
214
215
# File 'lib/forem/forem_object.rb', line 211

def respond_to_missing?(method, include_private = false)
  name = method.to_s
  name = name.chomp("=")
  @values.key?(name) || super
end

#to_hashHash{String => Object}

Recursively convert this object to a plain Ruby Hash.

Nested Forem::ForemObject instances are converted via their own #to_hash; Arrays whose elements are Forem::ForemObject instances are mapped similarly.

Examples:

obj.to_hash  #=> { "id" => 1, "title" => "Hello" }

Returns:

  • (Hash{String => Object})

    a plain Hash representation of all attributes.



184
185
186
187
188
189
190
191
192
# File 'lib/forem/forem_object.rb', line 184

def to_hash
  @values.transform_values do |v|
    case v
    when ForemObject then v.to_hash
    when Array then v.map { |e| e.is_a?(ForemObject) ? e.to_hash : e }
    else v
    end
  end
end