logical_model 0.4.10 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: