logical_model 0.4.10 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  = logical_model
2
2
 
3
- LogicalModel allows to use a restfull resources as models.
3
+ LogicalModel allows to use a RESTfull resources as models.
4
4
 
5
5
  It was written following this tutorial: http://www.slideshare.net/ihower/serviceoriented-design-and-implement-with-rails3
6
6
 
@@ -51,6 +51,21 @@ In your Gemfile:
51
51
 
52
52
  gem "logical_model"
53
53
 
54
+ In a model file:
55
+
56
+ class RemoteResource < LogicalModel
57
+
58
+ set_resource_host "remote.server"
59
+ set_resource_path "/api/remote/path"
60
+ # equivalente: set_resource_url "remote.server", "/api/path"
61
+
62
+ attribute :id
63
+ attribute :attribute_a
64
+ attribute :attribute_b
65
+
66
+ validates_presence_of :id
67
+ end
68
+
54
69
  == Testing
55
70
 
56
71
  To run spec:
@@ -70,7 +85,7 @@ To run spec:
70
85
 
71
86
  == Copyright
72
87
 
73
- Copyright (c) 2011,2012 Dwayne Macgowan. See LICENSE.txt for
88
+ Copyright (c) 2011,2012,2013 Dwayne Macgowan. See LICENSE.txt for
74
89
  further details.
75
90
 
76
91
  === TODO
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.10
1
+ 0.5.0
data/client.rb CHANGED
@@ -10,13 +10,19 @@ class User < LogicalModel
10
10
  self.hydra = Typhoeus::Hydra.new
11
11
  self.use_ssl = false #(Rails.env=="production")
12
12
 
13
- self.resource_path = "/api/v1/users"
14
- self.attribute_keys = [:id, :name, :email, :password, :bio]
13
+ set_resource_url("localhost:3000","/api/v1/users")
14
+
15
+ attribute :id
16
+ attribute :name
17
+ attribute :email
18
+ attribute :password
19
+ attribute :bio
20
+
15
21
  self.use_api_key = false
16
22
  #self.api_key_name = "token"
17
23
  #self.api_key = "8c330b5d70f86ebfa6497c901b299b79afc6d68c60df6df0bda0180d3777eb4a5528924ac96cf58a25e599b4110da3c4b690fa29263714ec6604b6cb2d943656"
18
- self.host = "localhost:3000"
19
- self.log_path = "logs/development.log"
24
+
25
+ self.log_path = "logs/development.log"
20
26
 
21
27
  TIMEOUT = 5500 # miliseconds
22
28
  PER_PAGE = 9999
@@ -2,11 +2,14 @@ require 'timeout'
2
2
  require 'active_model'
3
3
  require 'typhoeus'
4
4
  require 'active_support/all' # todo migrate to yajl
5
- require 'logger'
6
5
  require 'kaminari'
7
- require 'ssl_support'
8
- require 'safe_log'
6
+
7
+ require 'logical_model/rest_actions'
8
+ require 'logical_model/url_helper'
9
+ require 'logical_model/safe_log'
9
10
  require 'logical_model/has_many_keys'
11
+ require 'logical_model/api_key'
12
+ require 'logical_model/attributes'
10
13
 
11
14
  # Logical Model, not persistant on DB, works through API. (replaces ActiveResource)
12
15
  #
@@ -26,9 +29,11 @@ require 'logical_model/has_many_keys'
26
29
  #
27
30
  # Usage:
28
31
  # class RemoteResource < LogicalModel
29
- # self.host = "http://remote.server"
30
- # self.resource_path = "/api/remote/path"
31
- # self.attribute_keys = [:id, :attribute_a, :attribute_b]
32
+ # set_resource_url "remote.server", "/api/remote/path"
33
+ #
34
+ # attribute :id
35
+ # attribute :attribute_a
36
+ # attribute :attribute_b
32
37
  #
33
38
  # validates_presence_of :id
34
39
  # end
@@ -44,10 +49,12 @@ require 'logical_model/has_many_keys'
44
49
  # RemoteResource#destroy
45
50
  class LogicalModel
46
51
 
47
- include SslSupport
48
- extend SafeLog
49
-
50
- extend LogicalModel::HasManyKeys
52
+ include LogicalModel::Attributes
53
+ include LogicalModel::RESTActions
54
+ include LogicalModel::UrlHelper
55
+ include LogicalModel::ApiKey
56
+ include LogicalModel::SafeLog
57
+ include LogicalModel::HasManyKeys
51
58
 
52
59
  # include ActiveModel Modules that are usefull
53
60
  extend ActiveModel::Naming
@@ -61,32 +68,21 @@ class LogicalModel
61
68
 
62
69
  self.include_root_in_json = false
63
70
 
64
- # TODO clean this file refactoring into modules
65
-
66
71
  attr_accessor :last_response_code
67
72
 
68
- def self.attribute_keys=(keys)
69
- @attribute_keys = keys
70
- attr_accessor *keys
71
- end
72
-
73
- def self.attribute_keys
74
- @attribute_keys
75
- end
76
-
77
73
  DEFAULT_TIMEOUT = 10000
78
74
  DEFAULT_RETRIES = 3
79
75
 
76
+ def initialize(attributes={})
77
+ self.attributes = attributes
78
+ end
79
+
80
80
  class << self
81
- attr_accessor :host, :resource_path, :api_key, :api_key_name,
82
- :timeout, :retries,
83
- :use_api_key, :enable_delete_multiple,
84
- :json_root, :log_path
81
+ attr_accessor :timeout, :retries,
82
+ :json_root
85
83
 
86
84
  def timeout; @timeout ||= DEFAULT_TIMEOUT; end
87
85
  def retries; @retries ||= DEFAULT_RETRIES; end
88
- def log_path; @log_path ||= "log/logical_model.log"; end
89
- def use_api_key; @use_api_key ||= false; end
90
86
  def delete_multiple_enabled?; @enable_delete_multiple ||= false; end
91
87
 
92
88
  def hydra
@@ -117,464 +113,10 @@ class LogicalModel
117
113
  @json_root ||= self.class.to_s.underscore
118
114
  end
119
115
 
120
- def self.resource_uri(id=nil)
121
- sufix = (id.nil?)? "" : "/#{id}"
122
- "#{url_protocol_prefix}#{host}#{resource_path}#{sufix}"
123
- end
124
-
125
- def initialize(attributes={})
126
- self.attributes = attributes
127
- end
128
-
129
- def attributes
130
- attrs = self.class.attribute_keys.inject(ActiveSupport::HashWithIndifferentAccess.new) do |result,key|
131
- result[key] = read_attribute_for_validation(key)
132
- result
133
- end
134
-
135
- unless self.class.has_many_keys.blank?
136
- self.class.has_many_keys.inject(attrs) do |result,key|
137
- result["#{key}_attributes"] = send(key).map {|a| a.attributes}
138
- result
139
- end
140
- end
141
- attrs.reject {|key, value| key == "_id" && value.blank?}
142
- end
143
-
144
- def attributes=(attrs)
145
- sanitize_for_mass_assignment(attrs).each{|k,v| send("#{k}=",v) if respond_to?("#{k}=")}
146
- end
147
-
148
-
149
- def self.from_json(json_string)
150
- parsed = ActiveSupport::JSON.decode(json_string)
151
- collection = parsed["collection"].map{|i|self.new(i)}
152
- return { :collection => collection, :total => parsed["total"].to_i }
153
- end
154
-
155
- def self.log_ok(response)
156
- self.logger.info("LogicalModel Log: #{response.code} #{mask_api_key(response.effective_url)} in #{response.time}s")
157
- self.logger.debug("LogicalModel Log RESPONSE: #{response.body}")
158
- end
159
-
160
- def log_ok(response)
161
- self.class.log_ok(response)
162
- end
163
-
164
- def self.log_failed(response)
165
- begin
166
- error_message = ActiveSupport::JSON.decode(response.body)["message"]
167
- rescue => e
168
- error_message = "error"
169
- end
170
- msg = "LogicalModel Log: #{response.code} #{mask_api_key(response.effective_url)} in #{response.time}s FAILED: #{error_message}"
171
- self.logger.warn(msg)
172
- self.logger.debug("LogicalModel Log RESPONSE: #{response.body}")
173
- end
174
-
175
- def log_failed(response)
176
- self.class.log_failed(response)
177
- end
178
-
179
- def self.logger
180
- Logger.new(self.log_path || "log/logical_model.log")
181
- end
182
-
183
- # if needed willmerge api_key into given hash
184
- # returns merged hash
185
- def self.merge_key(params = {})
186
- if self.use_api_key
187
- params.merge({self.api_key_name => self.api_key})
188
- else
189
- params
190
- end
191
- end
192
-
193
- def create(params={})
194
- run_callbacks :save do
195
- run_callbacks :create do
196
- _create(params)
197
- end
198
- end
199
- end
200
-
201
- # @param params [Hash] parameters to be sent to service
202
- def update(params)
203
- run_callbacks :save do
204
- run_callbacks :update do
205
- _update(params)
206
- end
207
- end
208
-
209
- end
210
-
211
- def save
212
- run_callbacks :save do
213
- run_callbacks new_record?? :create : :update do
214
- _save
215
- end
216
- end
217
- end
218
-
219
- def destroy(params={})
220
- run_callbacks :destroy do
221
- _destroy(params)
222
- end
223
- end
224
-
225
- # ============================================================================================================
226
- # Following methods are API specific.
227
- # They assume we are using a RESTfull API.
228
- # for get, put, delete :id is expected
229
- # for post, put attributes are excepted under class_name directly. eg. put( {:id => 1, :class_name => {:attr => "new value for attr"}} )
230
- #
231
- # On error (400) a "errors" is expected.
232
- # ============================================================================================================
233
-
234
- # Asynchronic Pagination
235
- # This pagination won't block excecution waiting for result, pagination will be enqueued in Objectr#hydra.
236
- #
237
- # Parameters:
238
- # @param options [Hash].
239
- # Valid options are:
240
- # * :page - indicated what page to return. Defaults to 1.
241
- # * :per_page - indicates how many records to be returned per page. Defauls to 20
242
- # * all other options will be sent in :params to WebService
243
- #
244
- # Usage:
245
- # Person.async_paginate(:page => params[:page]){|i| result = i}
246
- def self.async_paginate(options={})
247
- options[:page] ||= 1
248
- options[:per_page] ||= 20
249
-
250
- options = self.merge_key(options)
251
-
252
- request = Typhoeus::Request.new(resource_uri, :params => options)
253
- request.on_complete do |response|
254
- if response.code >= 200 && response.code < 400
255
- log_ok(response)
256
-
257
- result_set = self.from_json(response.body)
258
-
259
- # this paginate is will_paginate's Array pagination
260
- collection = Kaminari.paginate_array(
261
- result_set[:collection],
262
- {
263
- :total_count=>result_set[:total],
264
- :limit => options[:per_page],
265
- :offset => options[:per_page] * ([options[:page], 1].max - 1)
266
- }
267
- )
268
-
269
- yield collection
270
- else
271
- log_failed(response)
272
- end
273
- end
274
- self.hydra.queue(request)
275
- end
276
-
277
- #synchronic pagination
278
- def self.paginate(options={})
279
- result = nil
280
- self.retries.times do
281
- begin
282
- async_paginate(options){|i| result = i}
283
- Timeout::timeout(self.timeout/1000) do
284
- self.hydra.run
285
- end
286
- break unless result.nil?
287
- rescue Timeout::Error
288
- self.logger.warn("timeout")
289
- result = nil
290
- end
291
- end
292
- result
293
- end
294
-
295
- # Asynchronic Count
296
- # This count won't block excecution waiting for result, count will be enqueued in Objectr#hydra.
297
- #
298
- # Parameters:
299
- # @param options [Hash].
300
- # Valid options are:
301
- # @option options [Integer] :page - indicated what page to return. Defaults to 1.
302
- # @option options [Integer] :per_page - indicates how many records to be returned per page. Defauls to 20
303
- # @option options [Hash] all other options will be forwarded in :params to WebService
304
- #
305
- # @example 'Count bobs'
306
- # Person.async_count(:when => {:name => 'bob'}}){|i| result = i}
307
- def self.async_count(options={})
308
- options[:page] = 1
309
- options[:per_page] = 1
310
-
311
- options = self.merge_key(options)
312
-
313
- request = Typhoeus::Request.new(resource_uri, :params => options)
314
- request.on_complete do |response|
315
- if response.code >= 200 && response.code < 400
316
- log_ok(response)
317
-
318
- result_set = self.from_json(response.body)
319
-
320
- yield result_set[:total]
321
- else
322
- log_failed(response)
323
- end
324
- end
325
- self.hydra.queue(request)
326
- end
327
-
328
- # synchronic count
329
- def self.count(options={})
330
- result = nil
331
- async_count(options){|i| result = i}
332
- Timeout::timeout(self.timeout/1000) do
333
- self.hydra.run
334
- end
335
- result
336
- rescue Timeout::Error
337
- self.logger.warn("timeout")
338
- return nil
339
- end
340
-
341
- # Asynchronic Find
342
- # This find won't block excecution waiting for result, excecution will be enqueued in Objectr#hydra.
343
- #
344
- # Parameters:
345
- # - id, id of object to find
346
- # @param [String/Integer] id
347
- # @param [Hash] params
348
- #
349
- # Usage:
350
- # Person.async_find(params[:id])
351
- def self.async_find(id, params = {})
352
- params = self.merge_key(params)
353
- request = Typhoeus::Request.new( resource_uri(id), :params => params )
354
-
355
- request.on_complete do |response|
356
- if response.code >= 200 && response.code < 400
357
- log_ok(response)
358
- yield self.new.from_json(response.body) # this from_json is defined in ActiveModel::Serializers::JSON
359
- else
360
- log_failed(response)
361
- end
362
- end
363
-
364
- self.hydra.queue(request)
365
- end
366
-
367
- # synchronic find
368
- def self.find(id, params = {})
369
- result = nil
370
- async_find(id, params){|i| result = i}
371
- Timeout::timeout(self.timeout/1000) do
372
- self.hydra.run
373
- end
374
- result
375
- rescue Timeout::Error
376
- self.logger.warn("timeout")
377
- return nil
378
- end
379
-
380
- #
381
- # creates model.
382
- #
383
- # returns:
384
- # @return false if model invalid
385
- # @return nil if there was a connection problem
386
- # @return created model ID if successfull
387
- #
388
- # @example Usage:
389
- # @person = Person.new(params[:person])
390
- # @person.create( non_attribute_param: "value" )
391
- def _create(params = {})
392
- return false unless valid?
393
-
394
- params = { self.json_root => self.attributes }.merge(params)
395
- params = self.class.merge_key(params)
396
-
397
- response = nil
398
- Timeout::timeout(self.class.timeout/1000) do
399
- response = Typhoeus::Request.post( self.class.resource_uri, :body => params, :timeout => self.class.timeout )
400
- end
401
- self.last_response_code = response.code
402
- if response.code == 201 || response.code == 202
403
- log_ok(response)
404
- if self.respond_to?('id=')
405
- self.id = ActiveSupport::JSON.decode(response.body)["id"]
406
- else
407
- true
408
- end
409
- elsif response.code == 400
410
- log_failed(response)
411
- ws_errors = ActiveSupport::JSON.decode(response.body)["errors"]
412
- ws_errors.each_key do |k|
413
- self.errors.add k, ws_errors[k]
414
- end
415
- return false
416
- else
417
- log_failed(response)
418
- return nil
419
- end
420
- rescue Timeout::Error
421
- self.class.logger.warn "timeout"
422
- return nil
423
- end
424
-
425
- # Updates Objects attributes, this will only send attributes passed as arguments
426
- #
427
- #
428
- # Returns false if Object#valid? is false.
429
- # Returns updated object if successfull.
430
- # Returns nil if update failed
431
- #
432
- # Usage:
433
- # @person.update(params[:person])
434
- def _update(params)
435
-
436
- self.attributes = params[self.json_root]
437
-
438
- return false unless valid?
439
-
440
- params = self.class.merge_key(params)
441
-
442
- response = nil
443
- Timeout::timeout(self.class.timeout/1000) do
444
- response = Typhoeus::Request.put( self.class.resource_uri(id),
445
- :params => params,
446
- :timeout => self.class.timeout )
447
- end
448
-
449
- if response.code == 200
450
- log_ok(response)
451
- return self
452
- else
453
- log_failed(response)
454
- return nil
455
- end
456
-
457
- rescue Timeout::Error
458
- self.class.logger.warn("request timed out")
459
- return nil
460
- end
461
-
462
- # Saves Objects attributes
463
- #
464
- #
465
- # Returns false if Object#valid? is false.
466
- # Returns updated object if successfull.
467
- # Returns nil if update failed
468
- #
469
- # Usage:
470
- # @person.save
471
- def _save
472
- self.attributes = attributes
473
-
474
- return false unless valid?
475
-
476
- sending_params = self.attributes
477
- sending_params.delete(:id)
478
-
479
- params = { self.json_root => sending_params }
480
- params = self.class.merge_key(params)
481
- response = nil
482
- Timeout::timeout(self.class.timeout/1000) do
483
- response = Typhoeus::Request.put( self.class.resource_uri(id), :params => params, :timeout => self.class.timeout )
484
- end
485
- if response.code == 200
486
- log_ok(response)
487
- return self
488
- else
489
- log_failed(response)
490
- return nil
491
- end
492
- rescue Timeout::Error
493
- self.class.logger.warn "timeout"
494
- return nil
495
- end
496
-
497
- # Deletes Object#id
498
- #
499
- # Returns nil if delete failed
500
- #
501
- # @param [String] id - id of contact to be deleted
502
- # @param [Hash] params - other params to be sent to WS on request
503
- #
504
- # Usage:
505
- # Person.delete(params[:id])
506
- def self.delete(id, params={})
507
-
508
- params = self.merge_key(params)
509
-
510
- response = nil
511
- Timeout::timeout(self.timeout/1000) do
512
- response = Typhoeus::Request.delete( self.resource_uri(id),
513
- :params => params,
514
- :timeout => self.timeout
515
- )
516
- end
517
- if response.code == 200
518
- log_ok(response)
519
- return self
520
- else
521
- log_failed(response)
522
- return nil
523
- end
524
- rescue Timeout::Error
525
- self.logger.warn "timeout"
526
- return nil
527
- end
528
-
529
- # Destroy object
530
- #
531
- # Usage:
532
- # @person.destroy
533
- def _destroy(params={})
534
- self.class.delete(self.id,params)
535
- end
536
-
537
- # Deletes all Objects matching given ids
538
- #
539
- # This method will make a DELETE request to resource_uri/destroy_multiple
540
- #
541
- # Returns nil if delete failed
542
- #
543
- # @param [Array] ids - ids of contacts to be deleted
544
- # @param [Hash] params - other params to be sent to WS on request
545
- #
546
- # Usage:
547
- # Person.delete_multiple([1,2,4,5,6])
548
- def self.delete_multiple(ids, params={})
549
- raise "not-enabled" unless self.delete_multiple_enabled?
550
-
551
- params = self.merge_key(params)
552
- params = params.merge({:ids => ids})
553
-
554
- response = nil
555
- Timeout::timeout(self.timeout/1000) do
556
- response = Typhoeus::Request.delete( self.resource_uri+"/destroy_multiple",
557
- :params => params,
558
- :timeout => self.timeout
559
- )
560
- end
561
- if response.code == 200
562
- log_ok(response)
563
- return self
564
- else
565
- log_failed(response)
566
- return nil
567
- end
568
- rescue Timeout::Error
569
- self.logger.warn "timeout"
570
- return nil
571
- end
572
-
573
116
  def persisted?
574
117
  false
575
118
  end
576
119
 
577
-
578
120
  # Returns true if a record has not been persisted yet.
579
121
  #
580
122
  # Usage: