smooth_operator 0.4.4 → 1.2.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.
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