Class: Conexa::Model

Inherits:
ConexaObject show all
Extended by:
Deprecatable
Defined in:
lib/conexa/model.rb

Overview

Base class for all API resources (Customer, Charge, Contract, etc.)

Attribute Access

Attributes are automatically accessible via method_missing:

customer.name           # => "Empresa ABC"
customer.company_id     # => 3
customer.is_active      # => true

The API returns camelCase (companyId), ConexaObject converts to snake_case (company_id), and method_missing handles the lookup transparently.

Primary Key

Each resource declares primary_key_attribute, which defines #id and the operations that need the resource ID (destroy, save, fetch, etc.):

class Charge < Model
primary_key_attribute :charge_id
end

charge.charge_id  # => 123
charge.chargeId   # => 123 (camelCase alias for backwards compat)
charge.id         # => 123 — the resource's own key, falling back to a
                #    plain "id" attribute, which is what write endpoints
                #    return and what Model#create reads back

Why explicit primary_key_attribute?

The default primary_key_name generates "classname_id" (e.g., "charge_id"), but compound names like RecurringSale would generate "recurringsale_id" instead of "recurring_sale_id". Explicit declaration ensures correctness.

Constant Summary

Constants inherited from ConexaObject

ConexaObject::RESOURCES

Instance Attribute Summary

Attributes inherited from ConexaObject

#attributes

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Deprecatable

deprecate

Methods inherited from ConexaObject

#==, #[]=, convert, #empty?, #initialize, #respond_to_missing?, #to_hash, #unsaved_attributes

Constructor Details

This class inherits a constructor from Conexa::ConexaObject

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class Conexa::ConexaObject

Class Method Details

.all(*args, **params) ⇒ Object Also known as: where



159
160
161
# File 'lib/conexa/model.rb', line 159

def all(*args, **params)
  find_by(params, *args)
end

.class_nameObject



178
179
180
181
# File 'lib/conexa/model.rb', line 178

def class_name
  name = self.name.split('::').last
  name[0].downcase + name[1..]
end

.create(*args) ⇒ Object



125
126
127
# File 'lib/conexa/model.rb', line 125

def create(*args)
  self.new(*args).create
end

.destroy(id) ⇒ Object



164
165
166
167
168
# File 'lib/conexa/model.rb', line 164

def destroy id
  instance = self.new
  instance.set_primary_key(id)
  instance.destroy
end

.extract_page_size_or_params(*args, **params) ⇒ Object



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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/conexa/model.rb', line 187

def extract_page_size_or_params(*args, **params)
  if args[0].is_a?(Hash)
    params = args[0].merge(params)
    page_val = nil
  else
    page_val = args[0]
  end
  size_val = args[1]

  # Explicit new pagination (limit/offset)
  if params.key?(:limit)
    unless params[:limit].is_a?(Integer) && params[:limit].positive?
      raise RequestError, "limit must be a positive integer"
    end

    params[:offset] ||= size_val if size_val.is_a?(Integer)
    params[:offset] ||= 0

    unless params[:offset].is_a?(Integer) && params[:offset] >= 0
      raise RequestError, "offset must be a non-negative integer"
    end

    params.delete(:page)
    params.delete(:size)
    return params
  end

  # Legacy pagination (page/size) — deprecated, and broken upstream.
  #
  # The API validates `page` and then ignores it, always returning the
  # first page with offset 0 and hasNext true, so a loop over `page` never
  # terminates and silently re-yields the same batch. Converting to
  # limit/offset fixes existing callers instead of leaving them with
  # plausible wrong answers.
  if params.key?(:page) || params.key?(:size) || page_val.is_a?(Integer)
    page = params.delete(:page) || page_val || 1
    size = params.delete(:size) || size_val || 100

    unless page.is_a?(Integer) && page.positive?
      raise RequestError, "page must be a positive integer"
    end
    unless size.is_a?(Integer) && size.positive?
      raise RequestError, "size must be a positive integer"
    end

    deprecate(:page_size,
              "page/size foi substituído por limit/offset e será removido em " \
              "conexa 0.3.0. A API v2 valida `page` e depois o ignora, devolvendo " \
              "sempre a primeira página; os valores são convertidos para " \
              "limit=size, offset=(page-1)*size.")

    params[:limit]  = size
    params[:offset] = (page - 1) * size
    return params
  end

  # Default: new pagination
  params[:limit] = 100
  params[:offset] = 0
  params
end

.find_by(params = Hash.new, page = nil, size = nil) ⇒ Object Also known as: find_by_hash



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/conexa/model.rb', line 140

def find_by(params = Hash.new, page = nil, size = nil)
  # extract_page_size_or_params always returns limit/offset now, and
  # validates them, so there is no page/size left here to guard.
  params = extract_page_size_or_params(page, size, **params)

  result = Conexa::Request.get(url, params: params).call(
    underscored_class_name,
    query_context: { resource_class: self, params: params }
  )

  # A listing always answers with a Result, as the READMEs promise. Without
  # this, an empty body yielded nil and a bare-array body yielded an Array,
  # so `.data` / `.pagination` / `.next_page` blew up far from the cause.
  return result if result.is_a?(Conexa::Result)

  Conexa::Result.new("data" => Array(result), "pagination" => nil)
end

.find_by_id(id, **options) ⇒ Object Also known as: find

Raises:



129
130
131
132
133
134
135
136
137
# File 'lib/conexa/model.rb', line 129

def find_by_id(id, **options)
  # Surrounding whitespace is a copy-paste artefact, not a different id —
  # strip it rather than failing. Anything still unusable in a URL is caught
  # by Request#full_api_url and raised as a RequestError.
  id = id.to_s.strip if id.is_a?(String)
  raise RequestError.new('Invalid ID') unless id.present?

  Conexa::Request.get(show_url(id), params: options).call underscored_class_name
end

.primary_key_attribute(snake_name) ⇒ Object

DSL for the primary key attribute

Examples:

primary_key_attribute :charge_id
# Generates: charge_id + chargeId alias + #id (with an "id" fallback)


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/conexa/model.rb', line 107

def primary_key_attribute(snake_name)
  camel_name = Util.camelize_str(snake_name.to_s)

  define_method(snake_name) do
    @attributes[snake_name.to_s]
  end

  alias_method camel_name.to_sym, snake_name

  # Not an alias: #id has to keep Model#id's documented fallback to a plain
  # "id" attribute. Write endpoints answer with {"id": N} rather than the
  # resource's own key — Model#create depends on exactly that — so aliasing
  # #id straight to #charge_id silently made the fallback dead code.
  define_method(:id) do
    @attributes[snake_name.to_s] || @attributes["id"]
  end
end

.show_url(*params) ⇒ Object



174
175
176
# File 'lib/conexa/model.rb', line 174

def show_url(*params)
  ["/#{ CGI.escape class_name }", *params].join '/'
end

.underscored_class_nameObject



183
184
185
# File 'lib/conexa/model.rb', line 183

def underscored_class_name
  self.name.split('::').last.gsub(/[a-z0-9][A-Z]/){|s| "#{s[0]}_#{s[1]}"}.downcase
end

.url(*params) ⇒ Object



170
171
172
# File 'lib/conexa/model.rb', line 170

def url(*params)
  ["/#{ CGI.escape class_name }s", *params].join '/'
end

Instance Method Details

#class_nameObject



92
93
94
# File 'lib/conexa/model.rb', line 92

def class_name
  self.class.to_s.split('::').last
end

#createObject



40
41
42
43
44
45
46
47
48
49
50
# File 'lib/conexa/model.rb', line 40

def create
  created = Conexa::Request.post(self.class.show_url, params: to_hash).call(class_name)

  # A create that answers with no usable body leaves us nothing to identify
  # the new record by, so there is nothing to re-fetch. Returning the local
  # object is honest; raising NoMethodError from `nil.attributes` was not.
  return self unless created.respond_to?(:attributes)

  set_primary_key created.attributes['id']
  fetch
end

#destroyObject

Raises:



96
97
98
99
100
# File 'lib/conexa/model.rb', line 96

def destroy
  raise RequestError.new('Invalid ID') unless id.present?
  update Conexa::Request.delete(self.class.show_url(primary_key)).call(class_name)
  self
end

#fetchObject



61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/conexa/model.rb', line 61

def fetch
  fetched = self.class.find(primary_key)

  # #update ignores anything with no attributes, which is right for a write
  # that answers with no body — but a *refresh* that comes back empty must
  # not quietly leave stale values in place reporting success.
  unless fetched.respond_to?(:attributes)
    raise ResponseError.new({ url: self.class.show_url(primary_key) }, nil,
                            "a API respondeu sem corpo: nada para atualizar")
  end

  update fetched
  self
end

#idObject



80
81
82
# File 'lib/conexa/model.rb', line 80

def id
  send(primary_key_name) || attributes['id']
end

#primary_keyObject



76
77
78
# File 'lib/conexa/model.rb', line 76

def primary_key
  id
end

#primary_key_nameObject



88
89
90
# File 'lib/conexa/model.rb', line 88

def primary_key_name
  Util.to_snake_case(class_name) + "_id"
end

#saveObject

Raises:



52
53
54
55
56
57
58
59
# File 'lib/conexa/model.rb', line 52

def save
  # #destroy has always guarded this; #save did not, so an object with no id
  # silently issued `PATCH /customer/` instead of failing fast.
  raise RequestError.new('Invalid ID') unless id.present?

  update Conexa::Request.patch(self.class.show_url(primary_key), params: unsaved_attributes).call(class_name)
  self
end

#set_primary_key(id) ⇒ Object



84
85
86
# File 'lib/conexa/model.rb', line 84

def set_primary_key id
  send(primary_key_name+"=", id)
end