Class: MOCO::BaseEntity

Inherits:
Object
  • Object
show all
Defined in:
lib/moco/entities.rb,
lib/moco/entities/base_entity.rb

Overview

Base class for all MOCO API entities

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client, response_data) ⇒ BaseEntity

Initializes an entity instance from raw API response data (Hash). Recursively processes nested hashes and arrays, converting known entity structures into corresponding MOCO::Entity instances.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/moco/entities/base_entity.rb', line 13

def initialize(client, response_data)
  @client = client

  # Ensure response_data is a Hash before proceeding
  unless response_data.is_a?(Hash)
    raise ArgumentError, "BaseEntity must be initialized with a Hash, got: #{response_data.class}"
  end

  # Process the top-level hash: transform keys and process nested values.
  @attributes = response_data.transform_keys(&:to_sym)
                             .each_with_object({}) do |(k, v), acc|
                               # Use process_value only for the *values* within the hash
                               acc[k] = process_value(v, k)
                             end

  # Define attribute methods based on the processed attributes hash
  define_attribute_methods
end

Instance Attribute Details

#attributesObject (readonly)

Returns the value of attribute attributes.



8
9
10
# File 'lib/moco/entities/base_entity.rb', line 8

def attributes
  @attributes
end

#clientObject (readonly)

Returns the value of attribute client.



8
9
10
# File 'lib/moco/entities/base_entity.rb', line 8

def client
  @client
end

Instance Method Details

#==(other) ⇒ Object

Compares two entities based on class and ID.



43
44
45
# File 'lib/moco/entities/base_entity.rb', line 43

def ==(other)
  id == other.id
end

#association(association_name, target_class_name_override = nil) ⇒ Object

Helper method to fetch associated objects based on data in attributes. Uses memoization to avoid repeated API calls. association_name: Symbol representing the association (e.g., :project, :customer). target_class_name_override: String specifying the target class if it differs

from the classified association name (e.g., "Company" for :customer).


148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/moco/entities/base_entity.rb', line 148

def association(association_name, target_class_name_override = nil)
  # Initialize cache if it doesn't exist
  @_association_cache ||= {}
  # Return cached object if available
  return @_association_cache[association_name] if @_association_cache.key?(association_name)

  association_data = attributes[association_name]

  # If data is already a BaseEntity object (processed during initialization), use it directly.
  return @_association_cache[association_name] = association_data if association_data.is_a?(MOCO::BaseEntity)

  # If data is a hash containing an ID, fetch the object.
  if association_data.is_a?(Hash) && association_data[:id]
    assoc_id = association_data[:id]
    target_class_name = target_class_name_override || ActiveSupport::Inflector.classify(association_name.to_s)
    collection_name = ActiveSupport::Inflector.tableize(target_class_name).to_sym # e.g., "Project" -> :projects

    # Check if the client responds to the collection method (e.g., client.projects)
    if client.respond_to?(collection_name)
      # Fetch the object using the appropriate collection proxy
      fetched_object = client.send(collection_name).find(assoc_id)
      return @_association_cache[association_name] = fetched_object
    else
      warn "Warning: Client does not respond to collection '#{collection_name}' for association '#{association_name}'."
      return @_association_cache[association_name] = nil
    end
  end

  # If data is not an object or a hash with an ID, return nil.
  @_association_cache[association_name] = nil
end

#destroyObject

Deletes the entity from the API. Returns true on success, false on failure.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/moco/entities/base_entity.rb', line 104

def destroy
  return false if id.nil? # Can't destroy without an ID

  # Determine the collection name from the class name
  collection_name = ActiveSupport::Inflector.tableize(self.class.name.split("::").last).to_sym

  # Check if the client responds to the collection method
  if client.respond_to?(collection_name)
    # Use the collection proxy to delete the entity
    client.send(collection_name).delete(id)
    true
  else
    warn "Warning: Client does not respond to collection '#{collection_name}' for destroying entity."
    false
  end
end

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


9
10
11
12
13
# File 'lib/moco/entities.rb', line 9

def eql?(other)
  return false unless other.is_a? self.class

  id == other.id
end

#has_many(association_name, foreign_key = nil, target_class_name_override = nil, nested = false) ⇒ Object

Helper method to fetch associated collections based on a foreign key. Uses memoization to avoid repeated API calls. association_name: Symbol representing the association (e.g., :tasks, :activities). foreign_key: Symbol representing the foreign key to use (e.g., :project_id). target_class_name_override: String specifying the target class if it differs

from the classified association name (e.g., "Task" for :tasks).

nested: Boolean indicating if this is a nested resource (e.g., project.tasks)



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/moco/entities/base_entity.rb', line 187

def has_many(association_name, foreign_key = nil, target_class_name_override = nil, nested = false)
  # Initialize cache if it doesn't exist
  @_association_cache ||= {}
  # Return cached collection if available
  return @_association_cache[association_name] if @_association_cache.key?(association_name)

  # If the association data is already in attributes and is an array of entities
  # AND this is NOT a nested resource, use it directly
  # For nested resources, we always create a proxy to ensure CRUD operations work
  association_data = attributes[association_name]
  if !nested && association_data.is_a?(Array) && association_data.all? { |item| item.is_a?(MOCO::BaseEntity) }
    return @_association_cache[association_name] = association_data
  end

  # Otherwise, fetch the collection from the API
  target_class_name = target_class_name_override || ActiveSupport::Inflector.classify(association_name.to_s)
  collection_name = ActiveSupport::Inflector.tableize(target_class_name).to_sym # e.g., "Task" -> :tasks

  # Determine the foreign key to use
  fk = foreign_key || :"#{ActiveSupport::Inflector.underscore(self.class.name.split("::").last)}_id"

  # Check if this is a nested resource
  if nested
    # For nested resources, create a NestedCollectionProxy
    @_association_cache[association_name] = MOCO::NestedCollectionProxy.new(
      client,
      self,
      collection_name,
      target_class_name
    )
  elsif client.respond_to?(collection_name)
    # For regular resources, use the standard collection proxy with a filter
    @_association_cache[association_name] = client.send(collection_name).where(fk => id)
  else
    warn "Warning: Client does not respond to collection '#{collection_name}' for association '#{association_name}'."
    @_association_cache[association_name] = []
  end
end

#hashObject



15
16
17
# File 'lib/moco/entities.rb', line 15

def hash
  id.hash
end

#idObject

Returns the entity’s ID.



38
39
40
# File 'lib/moco/entities/base_entity.rb', line 38

def id
  attributes[:id] || attributes["id"]
end

#inspectObject

Provides a string representation of the entity.



64
65
66
# File 'lib/moco/entities/base_entity.rb', line 64

def inspect
  "#<#{self.class.name}:#{object_id} @attributes=#{@attributes.inspect}>"
end

#reloadObject

Reloads the entity from the API. Returns self on success for method chaining.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/moco/entities/base_entity.rb', line 123

def reload
  return self if id.nil? # Can't reload without an ID

  # Determine the collection name from the class name
  collection_name = ActiveSupport::Inflector.tableize(self.class.name.split("::").last).to_sym

  # Check if the client responds to the collection method
  if client.respond_to?(collection_name)
    # Use the collection proxy to find the entity
    reloaded = client.send(collection_name).find(id)

    # Update attributes with the reloaded data
    @attributes = reloaded.attributes if reloaded
  else
    warn "Warning: Client does not respond to collection '#{collection_name}' for reloading entity."
  end

  self # Return self for method chaining
end

#saveObject

Saves changes to the entity back to the API. Returns self on success for method chaining.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/moco/entities/base_entity.rb', line 70

def save
  return self if id.nil? # Can't save without an ID

  # Determine the collection name from the class name
  collection_name = ActiveSupport::Inflector.tableize(self.class.name.split("::").last).to_sym

  # Check if the client responds to the collection method
  if client.respond_to?(collection_name)
    # Use the collection proxy to update the entity
    updated_data = client.send(collection_name).update(id, attributes)

    # Update local attributes with the response data
    @attributes = updated_data.attributes if updated_data
  else
    warn "Warning: Client does not respond to collection '#{collection_name}' for saving entity."
  end

  self # Return self for method chaining
end

#to_hObject

Converts the entity to a Hash, recursively converting nested entities.



48
49
50
51
52
53
54
55
# File 'lib/moco/entities/base_entity.rb', line 48

def to_h
  hash = {}
  instance_variables.each do |var|
    key = var.to_s.delete_prefix("@")
    hash[key.to_sym] = instance_variable_get(var)
  end
  hash
end

#to_json(*options) ⇒ Object

Converts the entity to a JSON string.



59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/moco/entities/base_entity.rb', line 59

def to_json(*arg)
  to_h do |k, v|
    if v.is_a? Hash
      if v.key?(:id) && !v[:id].nil?
        ["#{k}_id", v[:id]]
      else
        [k, v.except(:id)]
      end
    else
      [k, v]
    end
  end.to_h.to_json(arg)
end

#to_sObject

Provides a basic string representation (can be overridden by subclasses).



33
34
35
# File 'lib/moco/entities/base_entity.rb', line 33

def to_s
  "#{self.class.name.split("::").last} ##{id}"
end

#update(new_attributes) ⇒ Object

Updates attributes and saves the entity in one step. Returns self on success for method chaining.



92
93
94
95
96
97
98
99
100
# File 'lib/moco/entities/base_entity.rb', line 92

def update(new_attributes)
  # Update attributes
  new_attributes.each do |key, value|
    send("#{key}=", value) if respond_to?("#{key}=")
  end

  # Save changes
  save
end