motion_virtus 1.0.0.beta0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +445 -0
  3. data/lib/motion_virtus.rb +13 -0
  4. data/lib/project/attribute/accessor/builder.rb +69 -0
  5. data/lib/project/attribute/accessor/lazy_accessor.rb +39 -0
  6. data/lib/project/attribute/accessor.rb +100 -0
  7. data/lib/project/attribute/accessor_method.rb +73 -0
  8. data/lib/project/attribute/array.rb +24 -0
  9. data/lib/project/attribute/boolean.rb +52 -0
  10. data/lib/project/attribute/class.rb +23 -0
  11. data/lib/project/attribute/coercer.rb +43 -0
  12. data/lib/project/attribute/collection/coercible_writer.rb +83 -0
  13. data/lib/project/attribute/collection.rb +56 -0
  14. data/lib/project/attribute/date.rb +36 -0
  15. data/lib/project/attribute/date_time.rb +38 -0
  16. data/lib/project/attribute/decimal.rb +23 -0
  17. data/lib/project/attribute/default_value/from_callable.rb +37 -0
  18. data/lib/project/attribute/default_value/from_clonable.rb +37 -0
  19. data/lib/project/attribute/default_value/from_symbol.rb +37 -0
  20. data/lib/project/attribute/default_value.rb +49 -0
  21. data/lib/project/attribute/embedded_value/open_struct_coercer.rb +43 -0
  22. data/lib/project/attribute/embedded_value/struct_coercer.rb +42 -0
  23. data/lib/project/attribute/embedded_value.rb +69 -0
  24. data/lib/project/attribute/float.rb +30 -0
  25. data/lib/project/attribute/hash/coercible_writer.rb +78 -0
  26. data/lib/project/attribute/hash.rb +66 -0
  27. data/lib/project/attribute/integer.rb +27 -0
  28. data/lib/project/attribute/numeric.rb +25 -0
  29. data/lib/project/attribute/object.rb +13 -0
  30. data/lib/project/attribute/reader.rb +39 -0
  31. data/lib/project/attribute/set.rb +22 -0
  32. data/lib/project/attribute/string.rb +24 -0
  33. data/lib/project/attribute/symbol.rb +23 -0
  34. data/lib/project/attribute/time.rb +36 -0
  35. data/lib/project/attribute/writer/coercible.rb +45 -0
  36. data/lib/project/attribute/writer.rb +73 -0
  37. data/lib/project/attribute.rb +292 -0
  38. data/lib/project/attribute_set.rb +260 -0
  39. data/lib/project/class_inclusions.rb +41 -0
  40. data/lib/project/class_methods.rb +102 -0
  41. data/lib/project/configuration.rb +65 -0
  42. data/lib/project/const_missing_extensions.rb +16 -0
  43. data/lib/project/extensions.rb +101 -0
  44. data/lib/project/instance_methods.rb +165 -0
  45. data/lib/project/module_builder.rb +92 -0
  46. data/lib/project/module_extensions.rb +72 -0
  47. data/lib/project/stubs/date.rb +2 -0
  48. data/lib/project/stubs/date_time.rb +2 -0
  49. data/lib/project/stubs/decimal.rb +2 -0
  50. data/lib/project/stubs/ostruct.rb +149 -0
  51. data/lib/project/stubs/set.rb +767 -0
  52. data/lib/project/stubs.rb +5 -0
  53. data/lib/project/support/equalizer.rb +147 -0
  54. data/lib/project/support/options.rb +114 -0
  55. data/lib/project/support/type_lookup.rb +109 -0
  56. data/lib/project/value_object.rb +139 -0
  57. data/lib/project/version.rb +3 -0
  58. data/lib/project/virtus.rb +128 -0
  59. metadata +158 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YjIyMGQ5NzI0ZDRiOTlhOGQwNzNmNTdlZTdhYTAxMTA1Zjg0OWUyZQ==
5
+ data.tar.gz: !binary |-
6
+ ZTVhYWFmZTNkNjAxMTA1YjBjY2M0NDYzNTA1ZmFkOTE3ODczZDRmZA==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ NDM2NzFmNjMwOTRkY2RhODhiYjQzMWNjMzU3NGI5NzJkNDQ5NTBkMmU4YmNm
10
+ MzJkZjNmMmFhZjIxODUyOGRhZDgxNDczNmNjNTZiNjI2ZjQzYTY4ZTYxZTRl
11
+ OTg0OGViMjEyMDMwY2I4MWQxYTA2NzNkNTVhZTU1YTFhNzRkYjQ=
12
+ data.tar.gz: !binary |-
13
+ MzlkYTYwMTgxMjVmM2Q1N2NhMWQxODYwODIwOTQ2MGU3ZWFmMjA4YmUwOTNi
14
+ YzQ2YTNiMDBlYThlMTczNjdlYzJjMmFjM2QyMDc3NTBhYzhhM2ZhMmI3MDVi
15
+ YTE0MGI5YmFjNTQ0OGYwODU2ZDFlOTEwMWMyYWM1NDI5YTAxYWI=
data/README.md ADDED
@@ -0,0 +1,445 @@
1
+ # motion_virtus
2
+
3
+ A RubyMotion port of [solnic's](https://github.com/solnic) [virtus](https://github.com/solnic/virtus) library.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'motion_virtus'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install motion_virtus
18
+
19
+ ## Usage
20
+
21
+ ### Using Virtus with Classes
22
+
23
+ You can create classes extended with virtus and define attributes:
24
+
25
+ ``` ruby
26
+ class User
27
+ include Virtus
28
+
29
+ attribute :name, String
30
+ attribute :age, Integer
31
+ attribute :birthday, DateTime
32
+ end
33
+
34
+ user = User.new(:name => 'Piotr', :age => 29)
35
+ user.attributes # => { :name => "Piotr", :age => 29 }
36
+
37
+ user.name # => "Piotr"
38
+
39
+ user.age = '29' # => 29
40
+ user.age.class # => Fixnum
41
+
42
+ user.birthday = 'November 18th, 1983' # => #<DateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)>
43
+
44
+ # mass-assignment
45
+ user.attributes = { :name => 'Jane', :age => 21 }
46
+ user.name # => "Jane"
47
+ user.age # => 21
48
+ ```
49
+
50
+ ### Using Virtus with Modules
51
+
52
+ You can create modules extended with virtus and define attributes for later
53
+ inclusion in your classes:
54
+
55
+ ```ruby
56
+ module Name
57
+ include Virtus
58
+
59
+ attribute :name, String
60
+ end
61
+
62
+ module Age
63
+ include Virtus
64
+
65
+ attribute :age, Integer
66
+ end
67
+
68
+ class User
69
+ include Name, Age
70
+ end
71
+
72
+ user = User.new(:name => 'John', :age => '30')
73
+ ```
74
+
75
+ ### Dynamically Extending Instances
76
+
77
+ It's also possible to dynamically extend an object with Virtus:
78
+
79
+ ```ruby
80
+ class User
81
+ # nothing here
82
+ end
83
+
84
+ user = User.new
85
+ user.extend(Virtus)
86
+ user.attribute :name, String
87
+ user.name = 'John'
88
+ user.name # => 'John'
89
+ ```
90
+
91
+ ### Default Values
92
+
93
+ ``` ruby
94
+ class Page
95
+ include Virtus
96
+
97
+ attribute :title, String
98
+
99
+ # default from a singleton value (integer in this case)
100
+ attribute :views, Integer, :default => 0
101
+
102
+ # default from a singleton value (boolean in this case)
103
+ attribute :published, Boolean, :default => false
104
+
105
+ # default from a callable object (proc in this case)
106
+ attribute :slug, String, :default => lambda { |page, attribute| page.title.downcase.gsub(' ', '-') }
107
+
108
+ # default from a method name as symbol
109
+ attribute :editor_title, String, :default => :default_editor_title
110
+
111
+ def default_editor_title
112
+ published? ? title : "UNPUBLISHED: #{title}"
113
+ end
114
+ end
115
+
116
+ page = Page.new(:title => 'Virtus README')
117
+ page.slug # => 'virtus-readme'
118
+ page.views # => 0
119
+ page.published # => false
120
+ page.editor_title # => "UNPUBLISHED: Virtus README"
121
+ ```
122
+
123
+ ### Embedded Value
124
+
125
+ ``` ruby
126
+ class City
127
+ include Virtus
128
+
129
+ attribute :name, String
130
+ end
131
+
132
+ class Address
133
+ include Virtus
134
+
135
+ attribute :street, String
136
+ attribute :zipcode, String
137
+ attribute :city, City
138
+ end
139
+
140
+ class User
141
+ include Virtus
142
+
143
+ attribute :name, String
144
+ attribute :address, Address
145
+ end
146
+
147
+ user = User.new(:address => {
148
+ :street => 'Street 1/2', :zipcode => '12345', :city => { :name => 'NYC' } })
149
+
150
+ user.address.street # => "Street 1/2"
151
+ user.address.city.name # => "NYC"
152
+ ```
153
+
154
+ ### Collection Member Coercions
155
+
156
+ ``` ruby
157
+ # Support "primitive" classes
158
+ class Book
159
+ include Virtus
160
+
161
+ attribute :page_numbers, Array[Integer]
162
+ end
163
+
164
+ book = Book.new(:page_numbers => %w[1 2 3])
165
+ book.page_numbers # => [1, 2, 3]
166
+
167
+ # Support EmbeddedValues, too!
168
+ class Address
169
+ include Virtus
170
+
171
+ attribute :address, String
172
+ attribute :locality, String
173
+ attribute :region, String
174
+ attribute :postal_code, String
175
+ end
176
+
177
+ class PhoneNumber
178
+ include Virtus
179
+
180
+ attribute :number, String
181
+ end
182
+
183
+ class User
184
+ include Virtus
185
+
186
+ attribute :phone_numbers, Array[PhoneNumber]
187
+ attribute :addresses, Set[Address]
188
+ end
189
+
190
+ user = User.new(
191
+ :phone_numbers => [
192
+ { :number => '212-555-1212' },
193
+ { :number => '919-444-3265' } ],
194
+ :addresses => [
195
+ { :address => '1234 Any St.', :locality => 'Anytown', :region => "DC", :postal_code => "21234" } ])
196
+
197
+ user.phone_numbers # => [#<PhoneNumber:0x007fdb2d3bef88 @number="212-555-1212">, #<PhoneNumber:0x007fdb2d3beb00 @number="919-444-3265">]
198
+
199
+ user.addresses # => #<Set: {#<Address:0x007fdb2d3be448 @address="1234 Any St.", @locality="Anytown", @region="DC", @postal_code="21234">}>
200
+ ```
201
+
202
+ ### Hash attributes coercion
203
+
204
+ ``` ruby
205
+ class Package
206
+ include Virtus
207
+
208
+ attribute :dimensions, Hash[Symbol => Float]
209
+ end
210
+
211
+ package = Package.new(:dimensions => { 'width' => "2.2", :height => 2, "length" => 4.5 })
212
+ package.dimensions # => { :width => 2.2, :height => 2.0, :length => 4.5 }
213
+ ```
214
+
215
+ ### IMPORTANT note about member coercions
216
+
217
+ Virtus performs coercions only when a value is being assigned. If you mutate the value later on using its own
218
+ interfaces then coercion won't be triggered.
219
+
220
+ Here's an example:
221
+
222
+ ``` ruby
223
+ class Book
224
+ include Virtus
225
+
226
+ attribute :title, String
227
+ end
228
+
229
+ class Library
230
+ include Virtus
231
+
232
+ attribute :books, Array[Book]
233
+ end
234
+
235
+ library = Library.new
236
+
237
+ # This will coerce Hash to a Book instance
238
+ library.books = [ { :title => 'Introduction to Virtus' } ]
239
+
240
+ # This WILL NOT COERCE the value because you mutate the books array with Array#<<
241
+ library.books << { :title => 'Another Introduction to Virtus' }
242
+ ```
243
+
244
+ A suggested solution to this problem would be to introduce your own class instead of using Array and implement
245
+ mutation methods that perform coercions. For example:
246
+
247
+ ``` ruby
248
+ class Book
249
+ include Virtus
250
+
251
+ attribute :title, String
252
+ end
253
+
254
+ class BookCollection < Array
255
+ def <<(book)
256
+ if book.kind_of?(Hash)
257
+ super(Book.new(book))
258
+ else
259
+ super
260
+ end
261
+ end
262
+ end
263
+
264
+ class Library
265
+ include Virtus
266
+
267
+ attribute :books, BookCollection[Book]
268
+ end
269
+
270
+ library = Library.new
271
+ library.books << { :title => 'Another Introduction to Virtus' }
272
+ ```
273
+
274
+ ### Value Objects
275
+
276
+ ``` ruby
277
+ class GeoLocation
278
+ include Virtus::ValueObject
279
+
280
+ attribute :latitude, Float
281
+ attribute :longitude, Float
282
+ end
283
+
284
+ class Venue
285
+ include Virtus
286
+
287
+ attribute :name, String
288
+ attribute :location, GeoLocation
289
+ end
290
+
291
+ venue = Venue.new(
292
+ :name => 'Pub',
293
+ :location => { :latitude => 37.160317, :longitude => -98.437500 })
294
+
295
+ venue.location.latitude # => 37.160317
296
+ venue.location.longitude # => -98.4375
297
+
298
+ # Supports object's equality
299
+
300
+ venue_other = Venue.new(
301
+ :name => 'Other Pub',
302
+ :location => { :latitude => 37.160317, :longitude => -98.437500 })
303
+
304
+ venue.location === venue_other.location # => true
305
+ ```
306
+
307
+ ### Custom Coercions
308
+
309
+ ``` ruby
310
+ require 'json'
311
+
312
+ # With a custom writer class
313
+ class JsonWriter < Virtus::Attribute::Writer::Coercible
314
+ def coerce(value)
315
+ value.is_a?(Hash) ? value : JSON.parse(value)
316
+ end
317
+ end
318
+
319
+ class User
320
+ include Virtus
321
+
322
+ attribute :info, Hash, :writer_class => JsonWriter
323
+ end
324
+
325
+ user = User.new
326
+ user.info = '{"email":"john@domain.com"}' # => {"email"=>"john@domain.com"}
327
+ user.info.class # => Hash
328
+
329
+ # With a custom attribute encapsulating coercion-specific configuration
330
+ class NoisyString < Virtus::Attribute::String
331
+ class UpperCase < Virtus::Attribute::Writer::Coercible
332
+ def coerce(value)
333
+ super.upcase
334
+ end
335
+ end
336
+
337
+ def self.writer_class(*)
338
+ UpperCase
339
+ end
340
+ end
341
+
342
+ class User
343
+ include Virtus
344
+
345
+ attribute :scream, NoisyString
346
+ end
347
+
348
+ user = User.new(:scream => 'hello world!')
349
+ user.scream # => "HELLO WORLD!"
350
+ ```
351
+
352
+ ### Private Attributes
353
+
354
+ ``` ruby
355
+ class User
356
+ include Virtus
357
+
358
+ attribute :unique_id, String, :writer => :private
359
+
360
+ def set_unique_id(id)
361
+ self.unique_id = id
362
+ end
363
+ end
364
+
365
+ user = User.new(:unique_id => '1234-1234')
366
+ user.unique_id # => nil
367
+
368
+ user.unique_id = '1234-1234' # => NoMethodError: private method `unique_id='
369
+
370
+ user.set_unique_id('1234-1234')
371
+ user.unique_id # => '1234-1234'
372
+ ```
373
+
374
+ Coercions
375
+ ---------
376
+
377
+ Virtus uses [Coercible](https://github.com/solnic/coercible) for coercions. This
378
+ feature is turned on by default. You can turn it off for all attributes like that:
379
+
380
+ ```ruby
381
+ # Turn coercions off globally
382
+ Virtus.coerce(false)
383
+
384
+ # ...or you can turn it off for a single attribute
385
+ class User
386
+ include Virtus
387
+
388
+ attribute :name, String, :coerce => false
389
+ end
390
+ ```
391
+
392
+ You can configure coercers too:
393
+
394
+ ```ruby
395
+ Virtus.coercer do |config|
396
+ config.string.boolean_map = { 'yup' => true, 'nope' => false }
397
+ end
398
+
399
+ # Virtus.coercer instance is used by default for all attributes.
400
+ # You *can* override it for a single attribute if you want:
401
+
402
+ my_cool_coercer = Coercible::Coercer.new do |config|
403
+ # some customization
404
+ end
405
+
406
+ class User
407
+ include Virtus
408
+
409
+ attribute :name, String, :coercer => my_cool_coercer
410
+ end
411
+ ```
412
+
413
+ ## Building modules with custom configuration
414
+
415
+ You can also build Virtus modules that contain their own configuration.
416
+
417
+ ```ruby
418
+ YupNopeBooleans = Virtus.module { |mod|
419
+ mod.coerce = true
420
+ mod.string.boolean_map = { 'yup' => true, 'nope' => false }
421
+ }
422
+
423
+ class User
424
+ include YupNopeBooleans
425
+
426
+ attribute :name, String
427
+ attribute :admin, Boolean
428
+ end
429
+
430
+ # Or just include the module straight away ...
431
+ class User
432
+ include Virtus.module { |m| m.coerce = false }
433
+
434
+ attribute :name, String
435
+ attribute :admin, Boolean
436
+ end
437
+ ```
438
+
439
+ ## Contributing
440
+
441
+ 1. Fork it
442
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
443
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
444
+ 4. Push to the branch (`git push origin my-new-feature`)
445
+ 5. Create new Pull Request
@@ -0,0 +1,13 @@
1
+ unless defined?(Motion::Project::Config)
2
+ raise "This file must be required within a RubyMotion project Rakefile."
3
+ end
4
+
5
+ require 'motion_descendants_tracker'
6
+ require 'motion_coercible'
7
+
8
+ require 'motion-require'
9
+ Motion::Require.all(Dir.glob(File.expand_path('../project/**/*.rb', __FILE__)))
10
+
11
+ Motion::Project::App.setup do |app|
12
+ #configuration
13
+ end
@@ -0,0 +1,69 @@
1
+ module Virtus
2
+ class Attribute
3
+ class Accessor
4
+
5
+ class Builder
6
+
7
+ def self.call(*args)
8
+ builder = new(*args)
9
+ builder.accessor
10
+ end
11
+
12
+ def initialize(name, type, options = {})
13
+ @name = name
14
+ @type = type
15
+ @options = options
16
+ @primitive = options.fetch(:primitive, ::Object)
17
+ @visibility = determine_visibility
18
+ end
19
+
20
+ def accessor
21
+ accessor_class.new(reader, writer)
22
+ end
23
+
24
+ def accessor_class
25
+ @options[:lazy] ? Accessor::LazyAccessor : Accessor
26
+ end
27
+
28
+ def reader
29
+ reader_class.new(@name, reader_options)
30
+ end
31
+
32
+ def writer
33
+ writer_class.new(@name, writer_options)
34
+ end
35
+
36
+ def reader_class
37
+ @options.fetch(:reader_class) {
38
+ @type.reader_class(@primitive, @options)
39
+ }
40
+ end
41
+
42
+ def writer_class
43
+ @options.fetch(:writer_class) {
44
+ @type.writer_class(@primitive, @options)
45
+ }
46
+ end
47
+
48
+ def reader_options
49
+ @type.reader_options(@options).update(:visibility => @visibility[:reader])
50
+ end
51
+
52
+ def writer_options
53
+ @type.writer_options(@options).update(:visibility => @visibility[:writer])
54
+ end
55
+
56
+ private
57
+
58
+ def determine_visibility
59
+ default_accessor = @options.fetch(:accessor, :public)
60
+ reader_visibility = @options.fetch(:reader, default_accessor)
61
+ writer_visibility = @options.fetch(:writer, default_accessor)
62
+ { :reader => reader_visibility, :writer => writer_visibility }
63
+ end
64
+
65
+ end # class Builder
66
+
67
+ end # class Accessor
68
+ end # class Attribute
69
+ end # module Virtus
@@ -0,0 +1,39 @@
1
+ module Virtus
2
+ class Attribute
3
+ class Accessor
4
+
5
+ # Lazy accessor provides evaluating default values on first read
6
+ #
7
+ # @api private
8
+ class LazyAccessor < self
9
+
10
+ # Read attribute value and set default value if attribute is not set yet
11
+ #
12
+ # @see Accessor#get
13
+ #
14
+ # @return [Object]
15
+ #
16
+ # @api private
17
+ def get(instance)
18
+ if instance.instance_variable_defined?(reader.instance_variable_name)
19
+ super
20
+ else
21
+ value = writer.default_value.call(instance, self)
22
+ writer.call(instance, value)
23
+ value
24
+ end
25
+ end
26
+
27
+ # Return if the accessor is lazy
28
+ #
29
+ # @return [TrueClass]
30
+ #
31
+ # @api private
32
+ def lazy?
33
+ true
34
+ end
35
+ end # LazyAccessor
36
+
37
+ end # class Accessor
38
+ end # class Attribute
39
+ end # module Virtus
@@ -0,0 +1,100 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Accessor object providing reader and writer methods
5
+ #
6
+ # @api private
7
+ class Accessor
8
+ #include Adamantium::Flat
9
+
10
+ # Return reader
11
+ #
12
+ # @return [Reader]
13
+ #
14
+ # @api private
15
+ attr_reader :reader
16
+
17
+ # Return writer
18
+ #
19
+ # @return [Writer]
20
+ #
21
+ # @api private
22
+ attr_reader :writer
23
+
24
+ # Build an accessor instance
25
+ #
26
+ # @param [Symbol] name
27
+ #
28
+ # @param [Class] type
29
+ #
30
+ # @param [Hash] options
31
+ #
32
+ # @return [Accessor]
33
+ #
34
+ # @api private
35
+ def self.build(*args)
36
+ Builder.call(*args)
37
+ end
38
+
39
+ # Initialize a new accessor instance
40
+ #
41
+ # @param [Reader]
42
+ #
43
+ # @param [Writer]
44
+ #
45
+ # @return [undefined]
46
+ #
47
+ # @api private
48
+ def initialize(reader, writer)
49
+ @reader, @writer = reader, writer
50
+ end
51
+
52
+ # Get a variable value from an object
53
+ #
54
+ # @return [Object]
55
+ #
56
+ # @api private
57
+ def get(instance)
58
+ reader.call(instance)
59
+ end
60
+
61
+ # Set a variable on an object
62
+ #
63
+ # @return [Object]
64
+ #
65
+ # @api private
66
+ def set(*args)
67
+ writer.call(*args)
68
+ end
69
+
70
+ # Return if reader method is public
71
+ #
72
+ # @return [Boolean]
73
+ #
74
+ # @api private
75
+ def public_reader?
76
+ reader.public?
77
+ end
78
+
79
+ # Return if writer method is public
80
+ #
81
+ # @return [Boolean]
82
+ #
83
+ # @api private
84
+ def public_writer?
85
+ writer.public?
86
+ end
87
+
88
+ # Return if this accessor is lazy
89
+ #
90
+ # @return [FalseClass]
91
+ #
92
+ # @api private
93
+ def lazy?
94
+ false
95
+ end
96
+
97
+ end # class Accessor
98
+
99
+ end # class Attribute
100
+ end # module Virtus