smooth_operator 0.4.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +9 -9
  2. data/.gitignore +2 -1
  3. data/.rspec +4 -0
  4. data/Gemfile +13 -0
  5. data/README.md +258 -10
  6. data/console.rb +44 -0
  7. data/lib/smooth_operator/array_with_meta_data.rb +31 -0
  8. data/lib/smooth_operator/attribute_assignment.rb +102 -0
  9. data/lib/smooth_operator/attribute_methods.rb +87 -0
  10. data/lib/smooth_operator/attributes/base.rb +107 -0
  11. data/lib/smooth_operator/attributes/dirty.rb +29 -0
  12. data/lib/smooth_operator/attributes/normal.rb +15 -0
  13. data/lib/smooth_operator/delegation.rb +60 -0
  14. data/lib/smooth_operator/finder_methods.rb +43 -0
  15. data/lib/smooth_operator/helpers.rb +79 -0
  16. data/lib/smooth_operator/model_schema.rb +81 -0
  17. data/lib/smooth_operator/open_struct.rb +37 -0
  18. data/lib/smooth_operator/operator.rb +145 -0
  19. data/lib/smooth_operator/operators/faraday.rb +75 -0
  20. data/lib/smooth_operator/operators/typhoeus.rb +77 -0
  21. data/lib/smooth_operator/persistence.rb +144 -0
  22. data/lib/smooth_operator/relation/array_relation.rb +13 -0
  23. data/lib/smooth_operator/relation/association_reflection.rb +75 -0
  24. data/lib/smooth_operator/relation/associations.rb +75 -0
  25. data/lib/smooth_operator/relation/reflection.rb +41 -0
  26. data/lib/smooth_operator/relation/single_relation.rb +14 -0
  27. data/lib/smooth_operator/remote_call/base.rb +80 -0
  28. data/lib/smooth_operator/remote_call/errors/connection_failed.rb +20 -0
  29. data/lib/smooth_operator/remote_call/errors/timeout.rb +20 -0
  30. data/lib/smooth_operator/remote_call/faraday.rb +19 -0
  31. data/lib/smooth_operator/remote_call/typhoeus.rb +19 -0
  32. data/lib/smooth_operator/serialization.rb +79 -0
  33. data/lib/smooth_operator/translation.rb +27 -0
  34. data/lib/smooth_operator/validations.rb +15 -0
  35. data/lib/smooth_operator/version.rb +1 -1
  36. data/lib/smooth_operator.rb +26 -5
  37. data/smooth_operator.gemspec +12 -3
  38. data/spec/factories/user_factory.rb +34 -0
  39. data/spec/require_helper.rb +11 -0
  40. data/spec/smooth_operator/attribute_assignment_spec.rb +351 -0
  41. data/spec/smooth_operator/attributes_dirty_spec.rb +53 -0
  42. data/spec/smooth_operator/delegation_spec.rb +139 -0
  43. data/spec/smooth_operator/finder_methods_spec.rb +105 -0
  44. data/spec/smooth_operator/model_schema_spec.rb +31 -0
  45. data/spec/smooth_operator/operator_spec.rb +46 -0
  46. data/spec/smooth_operator/persistence_spec.rb +424 -0
  47. data/spec/smooth_operator/remote_call_spec.rb +320 -0
  48. data/spec/smooth_operator/serialization_spec.rb +80 -0
  49. data/spec/smooth_operator/validations_spec.rb +42 -0
  50. data/spec/spec_helper.rb +25 -0
  51. data/spec/support/helpers/persistence_helper.rb +38 -0
  52. data/spec/support/localhost_server.rb +97 -0
  53. data/spec/support/models/address.rb +14 -0
  54. data/spec/support/models/comment.rb +3 -0
  55. data/spec/support/models/post.rb +13 -0
  56. data/spec/support/models/user.rb +41 -0
  57. data/spec/support/models/user_with_address_and_posts.rb +89 -0
  58. data/spec/support/test_server.rb +165 -0
  59. metadata +108 -18
  60. data/lib/smooth_operator/base.rb +0 -30
  61. data/lib/smooth_operator/core.rb +0 -218
  62. data/lib/smooth_operator/http_handlers/typhoeus/base.rb +0 -58
  63. data/lib/smooth_operator/http_handlers/typhoeus/orm.rb +0 -34
  64. data/lib/smooth_operator/http_handlers/typhoeus/remote_call.rb +0 -28
  65. data/lib/smooth_operator/operator/base.rb +0 -43
  66. data/lib/smooth_operator/operator/exceptions.rb +0 -64
  67. data/lib/smooth_operator/operator/orm.rb +0 -118
  68. data/lib/smooth_operator/operator/remote_call.rb +0 -84
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ZDdlYTY3OGVhODk3ZWEzY2RkMTJjMGMyZmFiYmI2MTkzZTUwNDMyMg==
4
+ ZDVjYjVmMmQ5NDBjMjM3ZDFhODA0Y2FlODhmNzAxYmU2ZjJhMWU3Nw==
5
5
  data.tar.gz: !binary |-
6
- MmZiZTg4YTc3ZGI0OTI2OTA5ZTRiZjkyZDhkYzliMmMzNWE3NDUxYg==
7
- !binary "U0hBNTEy":
6
+ MjhlNzJlYTA1YTc4OTgxYzlkZTUyOTczN2M3NDY2NzdhZGQ0YzczZQ==
7
+ SHA512:
8
8
  metadata.gz: !binary |-
9
- YzdkZDE5MjY4MjZkYzA4M2ZiNGVlNzMzZTViOWM5ZWIzY2FiMGYwZGMzZjJh
10
- OTQ0NzYxMWI5ZmNhMjYzM2Y1Y2FkOGIyNmIyNmVkNzNiMmM3NGRkYTRiMjU3
11
- YTA2YzNkMjVmZDAzMTFjZThmNDZhNWFhNWRjYWZmM2Y4YTgwYTE=
9
+ YzgxZjhhZTYzNjMxZmZiYmIyOWUzODAzYjg4OGUwYzgxOTRiMmUxMjJhZTIx
10
+ ODE1NTRhYTg2NmE4OTY1ZGExNzUyYzFjZTM5NjU2ZjljOWJiMGExNmMxNmJj
11
+ YTEyYmY0NDAxMzc4ZTQxNmJlYjMyMGM1ODJmOThhOTdhNTcxYzI=
12
12
  data.tar.gz: !binary |-
13
- ZDY3ZmVjZmMyNDIxNmRhMWZmMzc2ZGUyNjNjOWQ0NzdkZjZjNTUxNTU5NmI0
14
- N2FmYjk4M2IyY2YzNDZlM2UwMzlmNDgxNDg1NDI4NTJkODcwODkzNDY3ZmZh
15
- NWUwYmJlNGYwNGQ1MTEzYzRmOGIyMDgxYTc2MDcxY2U4OGFhYWY=
13
+ YmNiZThmOGQyZTI5OWU5MTZiMTAwMjNjOTI5YTdhNmEzZWNhMTdjM2MwNDgz
14
+ Nzg1MzM2ZWRlODEwYmY1ZDY2OGRiNTBkMDhiYmJiM2UwMDAxZjVhN2JkYTQ0
15
+ NjNiNGU1N2ZkYWEwMWI3YmY4NGMyNjU5NWQ2MDkwMjJlZTdlNzk=
data/.gitignore CHANGED
@@ -16,4 +16,5 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
 
19
- .DS_Store
19
+ .DS_Store
20
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --tty
2
+ --color
3
+ --format documentation
4
+ --format html -o "tmp/rspec_result.html"
data/Gemfile CHANGED
@@ -2,3 +2,16 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in smooth_operator.gemspec
4
4
  gemspec
5
+
6
+ gem 'simplecov', :require => false, :group => :test
7
+
8
+ group :development, :test do
9
+ gem "pry"
10
+ gem "sinatra"
11
+ gem "typhoeus"
12
+ gem "activesupport"
13
+ gem "sinatra-contrib"
14
+ gem "rspec", "~> 3.0.0.beta1"
15
+ gem "factory_girl", "~> 4.0"
16
+ gem "ethon", :git => 'https://github.com/goncalvesjoao/ethon'
17
+ end
data/README.md CHANGED
@@ -1,8 +1,15 @@
1
1
  # SmoothOperator
2
2
 
3
- TODO: Write a gem description
3
+ Ruby gem, that mimics the ActiveRecord behaviour but through external API's.
4
+ It's a lightweight and flexible alternative to ActiveResource, that responds to a REST API like you expect it too.
4
5
 
5
- ## Installation
6
+ Depends only on Faraday gem, no need for ActiveSupport or any other Active* gem.
7
+
8
+ Although if I18n is present it will respond to .human_attribute_name method and if ActiveModel is present it will make use of 'ActiveModel::Name' to improve .model_name method.
9
+
10
+ ---
11
+
12
+ ## 1) Installation
6
13
 
7
14
  Add this line to your application's Gemfile:
8
15
 
@@ -16,14 +23,255 @@ Or install it yourself as:
16
23
 
17
24
  $ gem install smooth_operator
18
25
 
19
- ## Usage
26
+ ---
27
+
28
+ ## 2) Usage and Examples
29
+
30
+ ```ruby
31
+ class MyBlogResource < SmoothOperator::Base
32
+ self.endpoint = 'http://myblog.com/api/v0'
33
+
34
+ # HTTP BASIC AUTH
35
+ self.endpoint_user = 'admin'
36
+ self.endpoint_pass = 'admin'
37
+ end
38
+
39
+ class Post < MyBlogResource
40
+ end
41
+ ```
42
+
43
+ ---
44
+
45
+ ### 2.1) Creating a .new 'Post' and #save it
46
+
47
+ ```ruby
48
+ post = Post.new(body: 'my first post', author: 'John Doe')
49
+
50
+ post.new_record? # true
51
+ post.persisted? # false
52
+ post.body # 'my first post'
53
+ post.author # 'John Doe'
54
+ post.something_else # will raise NoMethodError
55
+
56
+ save_result = post.save # will make a http POST call to 'http://myblog.com/api/v0/posts'
57
+ # with `{ post: { body: 'my first post', author: 'John Doe' } }`
58
+
59
+ post.last_remote_call # will contain a SmoothOperator::RemoteCall instance containing relevant information about the save remote call.
60
+
61
+ # If the server response is positive (http code between 200 and 299):
62
+ save_result # true
63
+ post.new_record? # false
64
+ post.persisted? # true
65
+ # server response contains { id: 1 } on its body
66
+ post.id # 1
67
+
68
+ # If the server response is negative (http code between 400 and 499):
69
+ save_result # false
70
+ post.new_record? # true
71
+ post.persisted? # false
72
+ # server response contains { errors: { body: ['must be less then 10 letters'] } }
73
+ post.errors.body # Array
74
+
75
+ # If the server response is an error (http code between 500 and 599), or the connection was broke:
76
+ save_result # nil
77
+ post.new_record? # true
78
+ post.persisted? # false
79
+ # server response contains { errors: { body: ['must be less then 10 letters'] } }
80
+ post.errors # will raise NoMethodError
81
+
82
+ # In the positive and negative server response comes with a json,
83
+ # e.g. { id: 1 }, post will reflect that new data
84
+ post.id # 1
85
+
86
+ # In case of error and the server response contains a json,
87
+ # e.g. { id: 1 }, post will NOT reflect that data
88
+ post.id # raise NoMethodError
89
+
90
+ ```
91
+
92
+ ---
93
+
94
+ ### 2.2) Editing an existing record
95
+ ```ruby
96
+ post = Post.find(2)
97
+
98
+ post.body = 'editing my second page'
99
+
100
+ post.save
101
+ ```
102
+
103
+ ---
104
+
105
+ ### 2.3) Customize #save 'url', 'params' and 'options'
106
+ ```ruby
107
+ post = Post.new(id: 2, body: 'editing my second page')
108
+
109
+ post.new_record? # false
110
+ post.persisted? # true
111
+
112
+ post.save("#{post.id}/save_and_add_to_list", { admin: true, post: { author: 'Agent Smith', list_id: 1 } }, { timeout: 1 })
113
+ # Will make a PUT to 'http://myblog.com/api/v0/posts/2/save_and_add_to_list'
114
+ # with { admin: true, post: { body: 'editing my second page', list_id: 1 } }
115
+ # and will only wait 1sec for the server to respond.
116
+ ```
117
+
118
+ ---
119
+
120
+ ### 2.4) Saving using HTTP Patch verb
121
+ ```ruby
122
+ class Page < MyBlogResource
123
+ self.update_http_verb = :patch
124
+ end
125
+
126
+ page = Page.find(2)
127
+
128
+ page.body = 'editing my second page'
129
+
130
+ page.save # will make a http PATCH call to 'http://myblog.com/api/v0/pages/2'
131
+ # with `{ page: { body: 'editing my second page' } }`
132
+ ```
133
+
134
+ ---
135
+
136
+ ### 2.5) Retrieving remote objects - 'index' REST action
137
+
138
+ ```ruby
139
+ remote_call = Page.find(:all) # Will make a GET call to 'http://myblog.com/api/v0/pages'
140
+ # and will return a SmoothOperator::RemoteCall instance
141
+
142
+ pages = remote_call.objects # 'pages = remote_call.data' also works
143
+
144
+ # If the server response is positive (http code between 200 and 299):
145
+ remote_call.ok? # true
146
+ remote_call.not_processed? # false
147
+ remote_call.error? # false
148
+ remote_call.status # true
149
+ pages = remote_call.data # array of Page instances
150
+ remote_call.http_status # server_response code
151
+
152
+ # If the server response is unprocessed entity (http code 422):
153
+ remote_call.ok? # false
154
+ remote_call.not_processed? # true
155
+ remote_call.error? # false
156
+ remote_call.status # false
157
+ remote_call.http_status # server_response code
158
+
159
+ # If the server response is client error (http code between 400..499, except 422):
160
+ remote_call.ok? # false
161
+ remote_call.not_processed? # false
162
+ remote_call.error? # true
163
+ remote_call.status # nil
164
+ remote_call.http_status # server_response code
165
+
166
+ # If the server response is server error (http code between 500 and 599), or the connection broke:
167
+ remote_call.ok? # false
168
+ remote_call.not_processed? # false
169
+ remote_call.error? # true
170
+ remote_call.status # nil
171
+ remote_call.http_status # server_response code or 0 if connection broke
172
+ ```
173
+
174
+ ---
175
+
176
+ ### 2.6) Retrieving remote objects - 'show' REST action
177
+
178
+ ```ruby
179
+ remote_call = Page.find(2) # Will make a GET call to 'http://myblog.com/api/v0/pages/2'
180
+ # and will return a SmoothOperator::RemoteCall instance
181
+
182
+ page = remote_call.object # 'page = remote_call.data' also works
183
+ ```
184
+
185
+ ---
186
+
187
+ ### 2.7) Retrieving remote objects - custom query
188
+ ```ruby
189
+ remote_call = Page.find('my_pages', { q: body_contains: 'link' }, { endpoint_user: 'admin', endpoint_pass: 'new_password' })
190
+ # will make a GET call to 'http://myblog.com/api/v0/pages/my_pages?q={body_contains="link"}'
191
+ # and will change the HTTP BASIC AUTH credentials to user: 'admin' and pass: 'new_password' for this connection only.
192
+
193
+ # If the server json response is an Array [{ id: 1 }, { id: 2 }]
194
+ pages = remote.data # will return an array with 2 Page's instances
195
+ pages[0].id # 1
196
+ pages[1].id # 2
197
+
198
+ # If the server json response is a Hash { id: 3 }
199
+ page = remote.data # will return a single Page instance
200
+ page.id # 3
201
+
202
+ # If the server json response is Hash with a key called 'pages' { page: 1, total: 3, pages: [{ id: 4 }, { id: 5 }] }
203
+ pages = remote.data # will return a single ArrayWithMetaData instance, that will allow you to access to both the Page's instances array and the metadata.
204
+ pages.page # 1
205
+ pages.total # 3
206
+
207
+ pages[0].id # 4
208
+ pages[1].id # 5
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 3) Methods
214
+
215
+ ---
216
+
217
+ ### 3.1) Persistence methods
218
+
219
+ Methods | Behaviour | Arguments | Return
220
+ ------- | --------- | ------ | ---------
221
+ .create | Generates a new instance of the class with *attributes and calls #save with the rest of its arguments| Hash attributes = nil, String relative_path = nil, Hash data = {}, Hash options = {} | Class instance
222
+ #new_record? | Returns @new_record if defined, else populates it with true if #id is present or false if blank. | - | Boolean
223
+ #destroyed?| Returns @destroyed if defined, else populates it with false. | - | Boolean
224
+ #persisted?| Returns true if both #new_record? and #destroyed? return false, else returns false. | - | Boolean
225
+ #save | if #new_record? makes a HTTP POST, else a PUT call. If !#new_record? and relative_path is blank, sets relative_path = id.to_s. If the server POST response is positive, sets @new_record = false. See 4.2) for more behaviour info. | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
226
+ #save! | Executes the same behaviour as #save, but will raise RecordNotSaved if the returning value is not true | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
227
+ #destroy | Does nothing if !persisted? else makes a HTTP DELETE call. If server response it positive, sets @destroyed = true. If relative_path is blank, sets relative_path = id.to_s. See 4.2) for more behaviour info. | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
228
+
229
+ ---
230
+
231
+ ### 3.2) Finder methods
232
+
233
+ Methods | Behaviour | Arguments | Return
234
+ ------- | --------- | ------ | ---------
235
+ .find | If relative_path == :all, sets relative_path = ''. Makes a Get call and initiates Class objects with the server's response data. See 4.3) and 4.4) for more behaviour info. | String relative_path, Hash data = {}, Hash options = {} | Class instance, Array of Class instances or an ArrayWithMetaData instance
236
+
237
+ ---
238
+
239
+ ### 3.3) Operator methods
240
+ ...
241
+
242
+ ---
243
+
244
+ ### 3.3) Remote call methods
245
+ ...
246
+
247
+ ---
248
+
249
+ ## 4) Behaviours
250
+
251
+ ---
252
+
253
+ ### 4.1) Delegation behaviour
254
+ ...
255
+
256
+ ---
257
+
258
+ ### 4.2) Persistent operator behaviour
259
+ ...
260
+
261
+ ---
262
+
263
+ ### 4.3) Operator behaviour
264
+ ...
265
+
266
+ ---
267
+
268
+ ### 4.4) Remote call behaviour
269
+ ...
20
270
 
21
- TODO: Write usage instructions here
271
+ ---
22
272
 
23
- ## Contributing
273
+ ## 4) TODO
24
274
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
275
+ 1. Finish "Methods" and "Behaviours" documentation;
276
+ 2. ModelSchema specs;
277
+ 3. Cache.
data/console.rb ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << './'
4
+ $LOAD_PATH << './lib'
5
+
6
+ require 'spec/require_helper'
7
+
8
+ FactoryGirl.find_definitions
9
+
10
+ LocalhostServer.new(TestServer.new, 4567)
11
+
12
+ # user = nil
13
+
14
+ # hydra = Typhoeus::Hydra::hydra
15
+
16
+ # user = User::Base.new(id: 1)
17
+ # user.reload(nil, nil, { hydra: hydra })
18
+
19
+ # User::Base.find(1, nil, { hydra: hydra }) do |remote_call|
20
+ # user = remote_call.data
21
+ # end
22
+
23
+ #User::Base.post('', { user: { age: 1, posts: [{ body: 'post1' }, 2] } })
24
+
25
+ #user = UserWithAddressAndPosts::Son.new(FactoryGirl.attributes_for(:user_with_address_and_posts))
26
+ #user.save('', { status: 200 })
27
+
28
+ # "[{\"patient_id\"=>33, \"messages\"=>[{\"id\"=>\"53722c20cb38247c36000003\", \"title\"=>\"Joao Goncalves\", \"created_at\"=>\"2014-05-13T14:28:48Z\"}, {\"id\"=>\"53722bfccb382485d5000002\", \"title\"=>\"Joao Goncalves\", \"created_at\"=>\"2014-05-13T14:28:12Z\"}, {\"id\"=>\"53722b91cb3824e913000001\", \"title\"=>\"Joao Goncalves\", \"created_at\"=>\"2014-05-13T14:26:25Z\"}]}]"
29
+
30
+ post = Post.new(comments: [{ id: 1, name: '1' }, { id: 2, name: '2' }], address: { id: 1, name: 'address' })
31
+
32
+ class Test < SimpleDelegator
33
+ def reload
34
+ "TODO"
35
+ end
36
+ end
37
+
38
+ class Test2 < Test
39
+ def reload2
40
+ "TODO2"
41
+ end
42
+ end
43
+
44
+ binding.pry
@@ -0,0 +1,31 @@
1
+ module SmoothOperator
2
+ class ArrayWithMetaData < OpenStruct::Base
3
+
4
+ extend Forwardable
5
+
6
+ include Enumerable
7
+
8
+ attr_reader :meta_data, :internal_array
9
+
10
+ def_delegators :internal_array, :length, :<<, :[]
11
+
12
+ def initialize(attributes, object_class)
13
+ _attributes, _resources_name = attributes.dup, object_class.resources_name
14
+
15
+ @internal_array = [*_attributes[_resources_name]].map { |array_entry| object_class.new(array_entry).tap { |object| object.reloaded = true } }
16
+ _attributes.delete(_resources_name)
17
+
18
+ @meta_data = _attributes
19
+ end
20
+
21
+ def each
22
+ internal_array.each { |array_entry| yield array_entry }
23
+ end
24
+
25
+ def method_missing(method, *args, &block)
26
+ _method = method.to_s
27
+ meta_data.include?(_method) ? meta_data[_method] : super
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,102 @@
1
+ require 'smooth_operator/attributes/base'
2
+ require 'smooth_operator/attributes/dirty'
3
+ require 'smooth_operator/attributes/normal'
4
+
5
+ module SmoothOperator
6
+
7
+ module AttributeAssignment
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ attr_writer :unknown_hash_class
16
+
17
+ def unknown_hash_class
18
+ Helpers.get_instance_variable(self, :unknown_hash_class, ::OpenStruct)
19
+ end
20
+
21
+ def attributes_white_list
22
+ Helpers.get_instance_variable(self, :attributes_white_list, Set.new)
23
+ end
24
+
25
+ def attributes_black_list
26
+ Helpers.get_instance_variable(self, :attributes_black_list, Set.new)
27
+ end
28
+
29
+ def attributes_white_list_add(*getters)
30
+ attributes_white_list.merge getters.map(&:to_s)
31
+ end
32
+
33
+ def attributes_black_list_add(*getters)
34
+ attributes_black_list.merge getters.map(&:to_s)
35
+ end
36
+
37
+ def dirty_attributes
38
+ @dirty_attributes = true
39
+ end
40
+
41
+ def dirty_attributes?
42
+ @dirty_attributes
43
+ end
44
+
45
+ end
46
+
47
+ def initialize(attributes = {}, options = {})
48
+ @_options = {}
49
+
50
+ before_initialize(attributes, options)
51
+
52
+ assign_attributes attributes, options
53
+
54
+ after_initialize(attributes, options)
55
+ end
56
+
57
+ attr_reader :_options, :_meta_data
58
+
59
+
60
+ def assign_attributes(_attributes = {}, options = {})
61
+ return nil unless _attributes.is_a?(Hash)
62
+
63
+ attributes = _attributes = Helpers.stringify_keys(_attributes)
64
+
65
+ if _attributes.include?(self.class.resource_name)
66
+ attributes = _attributes.delete(self.class.resource_name)
67
+ @_meta_data = _attributes
68
+ end
69
+
70
+ options.each { |key, value| @_options[key] = value } if options.is_a?(Hash)
71
+
72
+ attributes.each { |name, value| push_to_internal_data(name, value, true) }
73
+ end
74
+
75
+ def parent_object
76
+ _options[:parent_object]
77
+ end
78
+
79
+ def has_data_from_server
80
+ _options[:from_server] == true
81
+ end
82
+
83
+ alias :from_server :has_data_from_server
84
+
85
+ protected #################### PROTECTED METHODS DOWN BELOW ######################
86
+
87
+ def before_initialize(attributes, options); end
88
+
89
+ def after_initialize(attributes, options); end
90
+
91
+ def allowed_attribute(attribute)
92
+ if !self.class.attributes_white_list.empty?
93
+ self.class.attributes_white_list.include?(attribute)
94
+ elsif !self.class.attributes_black_list.empty?
95
+ !self.class.attributes_black_list.include?(attribute)
96
+ else
97
+ true
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,87 @@
1
+ module SmoothOperator
2
+ module AttributeMethods
3
+
4
+ def internal_data
5
+ @internal_data ||= {}
6
+ end
7
+
8
+ def get_internal_data(field, method = :value)
9
+ result = internal_data[field]
10
+
11
+ if result.nil?
12
+ nil
13
+ elsif method == :value
14
+ result.is_a?(Attributes::Dirty) ? internal_data[field].send(method) : internal_data[field]
15
+ else
16
+ internal_data[field].send(method)
17
+ end
18
+ end
19
+
20
+ def get_attribute_type(attribute)
21
+ self.class.internal_structure[attribute.to_s]
22
+ end
23
+
24
+ def push_to_internal_data(attribute_name, attribute_value, cast = false)
25
+ attribute_name = attribute_name.to_s
26
+
27
+ return nil unless allowed_attribute(attribute_name)
28
+
29
+ known_attributes.add attribute_name
30
+
31
+ if internal_data[attribute_name].nil?
32
+ initiate_internal_data(attribute_name, attribute_value, cast)
33
+ else
34
+ update_internal_data(attribute_name, attribute_value, cast)
35
+ end
36
+
37
+ if self.class.respond_to?(:smooth_operator?)
38
+ trigger_necessary_events(attribute_name, attribute_value)
39
+ end
40
+ end
41
+
42
+ def column_for_attribute(attribute_name)
43
+ if defined?(ActiveRecord)
44
+ type = get_attribute_type(attribute_name)
45
+ ActiveRecord::ConnectionAdapters::Column.new(attribute_name.to_sym, type, type)
46
+ else
47
+ nil
48
+ end
49
+ end
50
+
51
+ protected #################### PROTECTED METHODS DOWN BELOW ######################
52
+
53
+ def trigger_necessary_events(attribute_name, attribute_value)
54
+ mark_for_destruction?(attribute_value) if attribute_name == self.class.destroy_key
55
+
56
+ new_record?(true) if attribute_name == self.class.primary_key
57
+ end
58
+
59
+ private ######################## PRIVATE #############################
60
+
61
+ def initiate_internal_data(attribute_name, attribute_value, cast)
62
+ if cast
63
+ internal_data[attribute_name] = new_attribute_object(attribute_name, attribute_value)
64
+
65
+ internal_data[attribute_name] = internal_data[attribute_name].value unless self.class.dirty_attributes?
66
+ else
67
+ internal_data[attribute_name] = attribute_value
68
+ end
69
+ end
70
+
71
+ def update_internal_data(attribute_name, attribute_value, cast)
72
+ if self.class.dirty_attributes?
73
+ internal_data[attribute_name].set_value(attribute_value, self)
74
+ else
75
+ internal_data[attribute_name] = cast ? new_attribute_object(attribute_name, attribute_value).value : attribute_value
76
+ end
77
+ end
78
+
79
+ def new_attribute_object(attribute_name, attribute_value)
80
+ attribute_class = self.class.dirty_attributes? ? Attributes::Dirty : Attributes::Normal
81
+
82
+ attribute_class.new(attribute_name, attribute_value, self)
83
+ end
84
+
85
+ end
86
+
87
+ end