Class: RestApi::Base

Inherits:
ActiveResource::Base show all
Includes:
ActiveModel::Dirty, ActiveModel::MassAssignmentSecurity, Cacheable
Defined in:
app/models/rest_api/base.rb,
app/models/rest_api/base.rb

Defined Under Namespace

Classes: AttributeHash

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ActiveResource::Associations

#belongs_to, #has_many, #has_one

Constructor Details

#initialize(attributes = {}, persisted = false) ⇒ Base

Returns a new instance of Base.



200
201
202
203
# File 'app/models/rest_api/base.rb', line 200

def initialize(attributes = {}, persisted=false)
  @as = attributes.delete :as
  super attributes, persisted
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_symbol, *arguments) ⇒ Object (protected)

:nodoc:



684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'app/models/rest_api/base.rb', line 684

def method_missing(method_symbol, *arguments) #:nodoc:
  #puts "in method missing of RestApi::Base #{method_symbol}"
  method_name = method_symbol.to_s

  if method_name =~ /(=|\?)$/
    case $1
    when "="
      res = :"#{method_name}_will_change!"
      send(res) if respond_to?(res) && attributes[name] != arguments.first
      attributes[$`] = arguments.first
    when "?"
      attributes[$`]
    end
  else
    return attributes[method_name] if attributes.include?(method_name)
    # not set right now but we know about it
    return nil if known_attributes.include?(method_name)
    super
  end
end

Class Attribute Details

.collection_nameObject

Returns the value of attribute collection_name.



331
332
333
# File 'app/models/rest_api/base.rb', line 331

def collection_name
  @collection_name
end

Class Method Details

.alias_attribute(from, to) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'app/models/rest_api/base.rb', line 149

def alias_attribute(from, to)
  aliased_attributes[from] = to

  define_method :"#{from}" do
    self.send :"#{to}"
  end
  define_method :"#{from}?" do
    self.send :"#{to}?"
  end
  define_method :"#{from}=" do |val|
    self.send :"#{to}=", val
  end
end

.aliased_attributesObject



162
163
164
# File 'app/models/rest_api/base.rb', line 162

def aliased_attributes
  @aliased_attributes ||= {}
end

.allow_anonymous?Boolean

Returns:

  • (Boolean)


390
391
392
# File 'app/models/rest_api/base.rb', line 390

def allow_anonymous?
  self.anonymous_api?
end

.attr_alters(from, *args) ⇒ Object



165
166
167
168
169
# File 'app/models/rest_api/base.rb', line 165

def attr_alters(from, *args)
  targets = (calculated_attributes[from] ||= [])
  targets.concat(args.flatten.uniq)
  define_attribute_method from
end

.calculated_attributesObject



170
171
172
# File 'app/models/rest_api/base.rb', line 170

def calculated_attributes
  @calculated_attributes ||= {}
end

.configuration=(config) ⇒ Object

Update the configuration of the Rest API. Use instead of setting static variables directly.



748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# File 'app/models/rest_api/base.rb', line 748

def self.configuration=(config)
  return if @last_config == config

  url = URI.parse(config[:url])
  path = url.path
  if path[-1..1] == '/'
    url.path = path[0..-2]
  else
    path = "#{path}/"
  end

  self.site = url.to_s
  self.prefix = path

  [:ssl_options, :idle_timeout, :read_timeout, :open_timeout].each do |sym|
    self.send(:"#{sym}=", config[sym]) if config[sym]
  end

  self.headers.delete 'User-Agent'
  self.headers['User-Agent'] = config[:user_agent] if config[:user_agent]

  self.proxy = if config[:proxy] == 'ENV'
      :ENV
    elsif config[:proxy]
      URI config[:proxy]
    end

  @last_config = config
  @info = false
end

.connection(options = {}, refresh = false) ⇒ Object

Make connection specific to the instance, and aware of user context



526
527
528
529
530
531
532
533
534
535
# File 'app/models/rest_api/base.rb', line 526

def connection(options = {}, refresh = false)
  c = shared_connection(options, refresh)
  if options[:as]
    UserAwareConnection.new(c, options[:as])
  elsif allow_anonymous?
    c
  else
    raise RestApi::MissingAuthorizationError
  end
end

.custom_id(name, mutable = false) ⇒ Object



312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'app/models/rest_api/base.rb', line 312

def custom_id(name, mutable=false)
  raise "Name #{name.inspect} must be a symbol" unless name.is_a?(Symbol) && !name.is_a?(Class)

  define_attribute_method name

  define_method :"#{name}=" do |s|
    send(:"#{name}_will_change!") if !send(:"#{name}_changed?") && attributes[name] != s
    attributes[name] = s
  end
  define_method 'to_key' do
    persisted? ? [send(:"#{name}_was") || send(name)] : nil
  end
end

.delete(id, options = {}) ⇒ Object



519
520
521
# File 'app/models/rest_api/base.rb', line 519

def delete(id, options = {})
  connection(options).delete(element_path(id, options)) #changed
end

.element_path(id = nil, prefix_options = {}, query_options = nil) ⇒ Object

changed



342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'app/models/rest_api/base.rb', line 342

def element_path(id = nil, prefix_options = {}, query_options = nil) #changed
  check_prefix_options(prefix_options)

  prefix_options, query_options = split_options(prefix_options) if query_options.nil?

  #begin changes
  path = "#{prefix(prefix_options)}#{collection_name}"
  unless singleton?
    raise ArgumentError, "id is required for non-singleton resources #{self}" if id.nil?
    path << "/#{URI.parser.escape id.to_s}"
  end
  path << ".#{format.extension}#{query_string(query_options)}"
end

.exception_for_code(code, type = nil) ⇒ Object



490
491
492
493
494
495
496
# File 'app/models/rest_api/base.rb', line 490

def exception_for_code(code, type=nil)
  if @exit_code_conditions
    handler = @exit_code_conditions[code]
    handler = handler[type] if type && Hash === handler
    handler
  end
end

.find(*arguments) ⇒ Object



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'app/models/rest_api/base.rb', line 356

def find(*arguments)

  scope   = arguments.slice!(0)
  options = arguments.slice!(0) || {}

  scope = :one if scope.nil? && singleton? # added

  case scope
  when :all   then find_every(options)
  when :first then find_every(options).first
  when :last  then find_every(options).last
  when :one   then find_one(options)
  else             find_single(scope, options)
  end
end

.find_one(options) ⇒ Object



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'app/models/rest_api/base.rb', line 372

def find_one(options)
  as = options[:as] # for user context support

  case from = options[:from]
  when Symbol
    instantiate_record(get(from, options[:params]))
  when String
    path = "#{from}#{query_string(options[:params])}"
    instantiate_record(format.decode(connection(options).get(path, headers).body), as) #changed
  when nil #begin add
    prefix_options, query_options = split_options(options[:params])
    path = element_path(nil, prefix_options, query_options)
    instantiate_record(format.decode(connection(options).get(path, headers).body), as) #end add
  end
rescue ActiveResource::ResourceNotFound => e
  raise ResourceNotFound.new(self.model_name, nil, e.response)
end

.get(custom_method_name, options = {}, call_options = {}) ⇒ Object



516
517
518
# File 'app/models/rest_api/base.rb', line 516

def get(custom_method_name, options = {}, call_options = {})
  connection(call_options).get(custom_method_collection_url(custom_method_name, options), headers)
end

.headersObject



136
137
138
139
140
# File 'app/models/rest_api/base.rb', line 136

def headers
  @headers ||= begin
    (superclass != ActiveResource::Base) ? superclass.headers.dup : {}
  end
end

.on_exit_code(code, handles = nil, &block) ⇒ Object



469
470
471
# File 'app/models/rest_api/base.rb', line 469

def on_exit_code(code, handles=nil, &block)
  (@exit_code_conditions ||= {})[code] = handles || block
end

.remote_errors_for(response) ⇒ Object

Must provide OpenShift compatible error decoding



415
416
417
418
419
420
421
422
# File 'app/models/rest_api/base.rb', line 415

def self.remote_errors_for(response)
  format.decode(response.body)['messages'].map do |m| 
    [(m['exit_code'].to_i rescue m['exit_code']),
      m['field'],
      m['text'],
    ]
  end rescue []
end

.shared_connection(options = {}, refresh = false) ⇒ Object



537
538
539
540
541
542
543
544
# File 'app/models/rest_api/base.rb', line 537

def shared_connection(options = {}, refresh = false)
  if defined?(@connection) || _format != superclass._format || superclass == Object || superclass == ActiveResource::Base
    @connection = update_connection(ActiveResource::PersistentConnection.new(site, format)) if refresh || @connection.nil?
    @connection
  else
    superclass.shared_connection(options, refresh)
  end
end

.singleton?Boolean

Returns:

  • (Boolean)


393
394
395
# File 'app/models/rest_api/base.rb', line 393

def singleton?
  self.singleton_api?
end

.translate_api_error(errors, code, field, text) ⇒ Object



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'app/models/rest_api/base.rb', line 472

def translate_api_error(errors, code, field, text)
  Rails.logger.debug "  Server error: :#{field} \##{code}: #{text}"
  if @exit_code_conditions
    handler = @exit_code_conditions[code]
    handler = handler[:raise] if Hash === handler
    case handler
    when Proc then return if handler.call errors, code, field, text
    when Class then raise handler, text
    end
  end
  message = I18n.t(code, :scope => [:rest_api, :errors], :default => text.to_s)
  field = (field || 'base').to_sym
  errors.add(field, message) unless message.blank?

  codes = errors.instance_variable_get(:@codes)
  codes = errors.instance_variable_set(:@codes, {}) unless codes
  (codes[field] ||= []).push(code)
end

.use_patch_on_update?Boolean

Returns:

  • (Boolean)


396
397
398
# File 'app/models/rest_api/base.rb', line 396

def use_patch_on_update?
  self.use_patch_api?
end

Instance Method Details

#asObject

The user under whose context we will be accessing the remote server



611
612
613
# File 'app/models/rest_api/base.rb', line 611

def as
  @as
end

#as=(as) ⇒ Object



614
615
616
617
# File 'app/models/rest_api/base.rb', line 614

def as=(as)
  @connection = nil
  @as = as
end

#assign_attributes(values, options = {}) ⇒ Object

Default mass assignment support



626
627
628
629
630
631
# File 'app/models/rest_api/base.rb', line 626

def assign_attributes(values, options = {})
  sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
    send("#{k}=", v)
  end
  self
end

#attributes=(attrs) ⇒ Object



252
253
254
255
256
257
258
259
260
# File 'app/models/rest_api/base.rb', line 252

def attributes=(attrs)
  attrs.with_indifferent_access.slice(*(
    self.class.known_attributes + 
    self.class.aliased_attributes.keys
  )).each_pair do |k,v|
    send(:"#{k}=", v)
  end
  self
end

#cloneObject

Ensure Fixnums and booleans can be cloned



187
188
189
190
191
192
193
194
195
196
197
198
# File 'app/models/rest_api/base.rb', line 187

def clone
  # Clone all attributes except the pk and any nested ARes
  cloned = Hash[attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.map { |k, v| [k, v.duplicable? ? v.clone : v] }]
  # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which
  # attempts to convert hashes into member objects and arrays into collections of objects.  We want
  # the raw objects to be cloned so we bypass load by directly setting the attributes hash.
  resource = self.class.new({})
  resource.prefix_options = self.prefix_options
  resource.send :instance_variable_set, '@attributes', cloned
  resource.as = @as # not an attribute
  resource
end

#dupObject

Ensure user authorization info is duplicated



178
179
180
181
182
# File 'app/models/rest_api/base.rb', line 178

def dup
  super.tap do |resource|
    resource.as = @as
  end
end

#duplicate_errorsObject



296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'app/models/rest_api/base.rb', line 296

def duplicate_errors
  self.class.calculated_attributes.each_pair do |from, attrs|
    attrs.each do |to|
      (errors[to] || []).each do |error|
        errors.add(from, error) unless errors.has_key?(from) && errors[:from].include?(error)
      end
    end
  end
  self.class.aliased_attributes.each_pair do |from, to|
    (errors[to] || []).each do |error|
      errors.add(from, error) unless errors.has_key?(from) && errors[:from].include?(error)
    end
  end
end

#get(custom_method_name, options = {}) ⇒ Object

Override method from CustomMethods to handle body objects



503
504
505
# File 'app/models/rest_api/base.rb', line 503

def get(custom_method_name, options = {})
  self.class.send(:instantiate_collection, self.class.format.decode(connection.get(custom_method_element_url(custom_method_name, options), self.class.headers).body), as, prefix_options ) #changed
end

#has_exit_code?(code, opts = nil) ⇒ Boolean

Returns:

  • (Boolean)


459
460
461
462
463
464
465
466
# File 'app/models/rest_api/base.rb', line 459

def has_exit_code?(code, opts=nil)
  codes = errors.instance_variable_get(:@codes) || {}
  if opts && opts[:on]
    (codes[opts[:on].to_sym] || []).include? code
  else
    codes.values.any?{ |c| c.include? code }
  end
end

#load(attributes, remove_root = false) ⇒ Object

Raises:

  • (ArgumentError)


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
248
249
250
# File 'app/models/rest_api/base.rb', line 205

def load(attributes, remove_root=false)
  raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
  self.prefix_options, attributes = split_options(attributes)

  attributes = attributes.dup
  aliased = self.class.aliased_attributes
  calculated = self.class.calculated_attributes
  known = self.class.known_attributes

  aliased.each do |from,to|
    value = attributes.delete(from)
    send("#{to}=", value) unless value.nil?
  end

  attributes.each do |key, value|
    #Rails.logger.debug "Found nil key when deserializing #{attributes.inspect}" if key.nil?
    if !known.include? key.to_s and !calculated.include? key and respond_to?("#{key}=") 
      send("#{key}=", value)
    else
      self.attributes[key.to_s] =
        case value
          when Array
            resource = nil
            value.map do |attrs|
              if attrs.is_a?(Hash)
                resource ||= find_or_create_resource_for_collection(key)
                attrs[:as] = as if resource.method_defined? :as=
                resource.new(attrs)
              else
                attrs.duplicable? ? attrs.dup : attrs
              end
            end
          when Hash
            resource = find_or_create_resource_for(key)
            value[:as] = as if resource.method_defined? :as=
            resource.new(value)
          else
            value.duplicable? ? value.dup : value
        end
    end
  end

  calculated.each_key { |key| send("#{key}=", attributes[key]) if attributes.include?(key) }
  @changed_attributes.clear if @changed_attributes
  self
end

#load_remote_errors(remote_errors, save_cache = false, optional = false) ⇒ Object



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'app/models/rest_api/base.rb', line 424

def load_remote_errors(remote_errors, save_cache=false, optional=false)
  begin
    self.class.remote_errors_for(remote_errors.response).each do |m|
      self.class.translate_api_error(errors, *m)
    end
    Rails.logger.debug "  Found errors on the response object: #{errors.to_hash.inspect}"
    duplicate_errors
  rescue ActiveResource::ConnectionError
    raise
  rescue Exception => e
    Rails.logger.warn e
    Rails.logger.warn e.backtrace
    msg = if defined? response
      Rails.logger.warn "Unable to read server response, #{response.inspect}"
      Rails.logger.warn "  Body: #{response.body.inspect}" if defined? response.body
      defined?(response.body) ? response.body.to_s : 'No response body from server'
    else
      'No response object'
    end
    raise RestApi::BadServerResponseError, msg, $@ unless optional
  end
  optional ? !errors.empty? : errors
end

#raise_on_invalidObject

Raises:

  • (ActiveResource::ResourceInvalid)


262
263
264
265
266
267
268
# File 'app/models/rest_api/base.rb', line 262

def raise_on_invalid
  (errors.instance_variable_get(:@codes) || {}).values.flatten(1).compact.uniq.each do |code|
    exc = self.class.exception_for_code(code, :on_invalid)
    raise exc.new(self) if exc
  end
  raise(ActiveResource::ResourceInvalid.new(self))
end

#reloadObject

Override methods from ActiveResource to make them contextual connection aware



511
512
513
# File 'app/models/rest_api/base.rb', line 511

def reload
  self.load(prefix_options.merge(self.class.find(to_param, :params => prefix_options, :as => as).attributes))
end

#remote_resultsObject

FIXME may be refactored



456
457
458
# File 'app/models/rest_api/base.rb', line 456

def remote_results
  (attributes[:messages] || []).select{ |m| m['field'] == 'result' }.map{ |m| m['text'].presence }.compact
end

#save!Object



270
271
272
# File 'app/models/rest_api/base.rb', line 270

def save!
  save || raise_on_invalid
end

#save_with_change_tracking(*args, &block) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'app/models/rest_api/base.rb', line 274

def save_with_change_tracking(*args, &block)
  save_without_change_tracking(*args, &block).tap do |valid|
    if valid
      @previously_changed = changes
      @changed_attributes.clear
    end
  end

rescue ActiveResource::ConnectionError => error
  # if the server returns a body that has messages, filter them through
  # the error handler.  If one or more errors were set, assume that the message
  # is more useful than the exception and return false. Otherwise throw as ActiveResource
  # would
  raise unless set_remote_errors(error, true)
end

#to_json(options = {}) ⇒ Object



619
620
621
# File 'app/models/rest_api/base.rb', line 619

def to_json(options={})
  super({:root => nil}.merge(options))
end

#valid?Boolean

Copy calculated attribute errors

Returns:

  • (Boolean)


292
293
294
# File 'app/models/rest_api/base.rb', line 292

def valid?
  super.tap { |valid| duplicate_errors unless valid }
end