id 0.0.12 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -3
  3. data/Gemfile.lock +25 -10
  4. data/LICENSE.md +1 -1
  5. data/README.md +173 -35
  6. data/id.gemspec +8 -3
  7. data/lib/id.rb +21 -15
  8. data/lib/id/active_model.rb +30 -0
  9. data/lib/id/association.rb +26 -0
  10. data/lib/id/boolean.rb +8 -0
  11. data/lib/id/coercion.rb +38 -0
  12. data/lib/id/eta_expansion.rb +5 -0
  13. data/lib/id/field.rb +46 -0
  14. data/lib/id/field/definition.rb +44 -0
  15. data/lib/id/field/summary.rb +35 -0
  16. data/lib/id/form.rb +41 -13
  17. data/lib/id/form_backwards_compatibility.rb +6 -0
  18. data/lib/id/hashifier.rb +13 -24
  19. data/lib/id/model.rb +29 -25
  20. data/lib/id/timestamps.rb +15 -15
  21. data/lib/id/validations.rb +8 -0
  22. data/spec/examples/cat_spec.rb +37 -0
  23. data/spec/lib/id/active_model_spec.rb +40 -0
  24. data/spec/lib/id/association_spec.rb +73 -0
  25. data/spec/lib/id/boolean_spec.rb +38 -0
  26. data/spec/lib/id/coercion_spec.rb +53 -0
  27. data/spec/lib/id/eta_expansion_spec.rb +13 -0
  28. data/spec/lib/id/field/definition_spec.rb +37 -0
  29. data/spec/lib/id/field/summary_spec.rb +26 -0
  30. data/spec/lib/id/field_spec.rb +62 -0
  31. data/spec/lib/id/form_spec.rb +84 -0
  32. data/spec/lib/id/hashifier_spec.rb +19 -0
  33. data/spec/lib/id/model_spec.rb +30 -180
  34. data/spec/lib/id/timestamps_spec.rb +22 -20
  35. data/spec/lib/id/validations_spec.rb +18 -0
  36. data/spec/spec_helper.rb +11 -5
  37. data/spec/support/dummy_rails_form_builder.rb +9 -0
  38. metadata +84 -26
  39. data/lib/id/form/active_model_form.rb +0 -41
  40. data/lib/id/form/descriptor.rb +0 -27
  41. data/lib/id/form/field_form.rb +0 -14
  42. data/lib/id/form/field_with_form_support.rb +0 -12
  43. data/lib/id/missing_attribute_error.rb +0 -4
  44. data/lib/id/model/association.rb +0 -49
  45. data/lib/id/model/definer.rb +0 -23
  46. data/lib/id/model/descriptor.rb +0 -23
  47. data/lib/id/model/field.rb +0 -61
  48. data/lib/id/model/has_many.rb +0 -11
  49. data/lib/id/model/has_one.rb +0 -17
  50. data/lib/id/model/type_casts.rb +0 -96
  51. data/spec/lib/id/model/association_spec.rb +0 -27
  52. data/spec/lib/id/model/field_spec.rb +0 -0
  53. data/spec/lib/id/model/form_spec.rb +0 -56
  54. data/spec/lib/id/model/type_casts_spec.rb +0 -44
  55. data/spec/lib/mike_spec.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a4830e44c543e20bfbd0a69894f31e33f255d60
4
- data.tar.gz: 910da24aa9829a4dbea10c6c77e64773fdf188bd
3
+ metadata.gz: 4626765bc042e4c08215636a3c50e751729ae2c7
4
+ data.tar.gz: 6a549d6d328b1b91583f9ec7f2819b6f9ecea0a9
5
5
  SHA512:
6
- metadata.gz: 69f2da5ba66e623889de506206115604a152536f97dbfbcb75dbf00967d16fbf843d9aaae12c3d8f970d5ce4726aae2ee1691362330fc0b0d5faf6541c32915a
7
- data.tar.gz: 3ea228e5c3c75256a77029b523236ae9e6be7cc264ae9db92b3b53d86b33456c4dfe2ad188f86e7fe4fa5e2c69687c53172a2a3d796c12a4b91fdb24b25fc5c5
6
+ metadata.gz: 6698ecb274e81d09550a410d6bf547b48d4ed124f173d0f9ef0d0270d0ca76eef757887c40c73c4c16b87167c1a6e86b0a63d21975d328b9c45464d1b7f3899d
7
+ data.tar.gz: 38ac9a455e27e9751e0fc3b66ce5d730c70cfb5ce83b38cfc4f66945f3147aecd199792994a675e85b8c069753d66e954d1f1aacc26c56d101af1e16cd3a17cd
data/Gemfile CHANGED
@@ -1,5 +1,2 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
- gem 'test-unit'
4
- gem 'json', '~> 1.7.7'
5
- gem 'money', '3.7.1'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- id (0.0.11)
4
+ id (0.1)
5
5
  activemodel
6
6
  activesupport
7
7
  money
@@ -19,16 +19,27 @@ GEM
19
19
  multi_json (~> 1.3)
20
20
  thread_safe (~> 0.1)
21
21
  tzinfo (~> 0.3.37)
22
- atomic (1.1.12)
22
+ atomic (1.1.14)
23
23
  builder (3.1.4)
24
+ coveralls (0.7.0)
25
+ multi_json (~> 1.3)
26
+ rest-client
27
+ simplecov (>= 0.7)
28
+ term-ansicolor
29
+ thor
24
30
  diff-lcs (1.2.1)
25
- i18n (0.6.4)
26
- json (1.7.7)
31
+ i18n (0.6.5)
32
+ metaclass (0.0.1)
33
+ mime-types (1.25)
27
34
  minitest (4.7.5)
28
- money (3.7.1)
29
- i18n (~> 0.4)
35
+ mocha (0.14.0)
36
+ metaclass (~> 0.0.1)
37
+ money (5.1.1)
38
+ i18n (~> 0.6.0)
30
39
  multi_json (1.7.3)
31
40
  optional (0.0.7)
41
+ rest-client (1.6.7)
42
+ mime-types (>= 1.16)
32
43
  rspec (2.13.0)
33
44
  rspec-core (~> 2.13.0)
34
45
  rspec-expectations (~> 2.13.0)
@@ -41,18 +52,22 @@ GEM
41
52
  multi_json (~> 1.0)
42
53
  simplecov-html (~> 0.7.1)
43
54
  simplecov-html (0.7.1)
55
+ term-ansicolor (1.2.2)
56
+ tins (~> 0.8)
44
57
  test-unit (2.5.5)
45
- thread_safe (0.1.2)
58
+ thor (0.18.1)
59
+ thread_safe (0.1.3)
46
60
  atomic
47
- tzinfo (0.3.37)
61
+ tins (0.12.0)
62
+ tzinfo (0.3.38)
48
63
 
49
64
  PLATFORMS
50
65
  ruby
51
66
 
52
67
  DEPENDENCIES
68
+ coveralls
53
69
  id!
54
- json (~> 1.7.7)
55
- money (= 3.7.1)
70
+ mocha
56
71
  rspec
57
72
  simplecov
58
73
  test-unit
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 On The Beach Ltd
1
+ Copyright (c) 2012 Russell Dunphy
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -3,63 +3,201 @@
3
3
 
4
4
  JSON is a great way to transfer data between systems, and it's easy to parse into a Ruby hash. But sometimes it's nice to have actual methods to call when you want to get attributes from your data, rather than coupling your entire codebase to the hash representation by littering it with calls to `fetch` or `[]`. The same goes for BSON documents stored in Mongo.
5
5
 
6
- That's where `id` (as in Freud) comes in. You define your model classes using syntax that should look pretty familiar if you've used any popular Ruby ORMs - but `id` is not an ORM. Model objects defined with `id` have a constructor that accepts a hash, and you define the values of this hash that are made readable as fields - but that hash can come from any source.
6
+ That's where `id` (as in Freud) comes in. You define your model classes using syntax that should look pretty familiar if you've used any popular Ruby ORMs - but `id` is not an ORM. Model objects defined with `id` have a constructor that accepts a hash, and you define the values of this hash that are made readable as fields, but that hash can come from any source.
7
7
 
8
8
  #### Defining a model
9
9
 
10
- Defining a model looks like this:
10
+ To define a model, include the `Id::Model` module in your class.
11
11
 
12
- class MyModel
13
- include Id::Model
12
+ ```ruby
13
+ class Cat
14
+ include Id::Model
15
+ end
16
+ ```
14
17
 
15
- field :foo
16
- field :bar, default: 42
17
- field :baz, key: 'barry'
18
+ #### Defining fields
18
19
 
19
- end
20
+ At its most basic, you can define a field like this:
20
21
 
21
- my_model = MyModel.new(foo: 7, barry: 'hello')
22
- my_model.foo # => 7
23
- my_model.bar # => 42
24
- my_model.baz # => 'hello'
22
+ ```ruby
23
+ class Cat
24
+ field :name
25
+ end
26
+ ```
25
27
 
26
- As you can see, you can specify default values as well as key aliases.
28
+ This gives you the equivalent of an `attr_reader` for that field, as well as a predicate method which checks if it has been set (in the case of boolean fields, this predicate method also checks the value of that field - nil is interpreted as false).
29
+
30
+ ```ruby
31
+ cat = Cat.new(name: "Travis")
32
+ cat.name # => "Travis"
33
+ cat.name? # => true
34
+
35
+ cat = Cat.new
36
+ cat.name? # => false
37
+ ```
38
+
39
+ ###### What happens if you try to access a field that hasn't been set?
40
+
41
+ ```ruby
42
+ irb(main):007:0> Cat.new.name
43
+ Id::MissingAttributeError: Cat had a nil value for 'name'.
44
+ ```
45
+ Id is allergic to `nil`, and refuses to return it. If you want to access a field you need to be sure it's there, or you'll get an error. This means if you have a bug in your code and a field isn't set, you'll find out as soon as possible, rather that letting a `nil` leak out and cause an `undefined method 'foo' for nil:NilClass` at some unspecified future point, from where it might be hard to track down the source of the problem.
46
+
47
+ ###### What about optional fields?
48
+
49
+ Sometimes fields really are optional. In this case you can either test for their presence using the predicate methods (which is a bit ugly, but at least forces you to deal with the empty case), or you can mark the field as `optional: true`, which will make them return an `Option` type rather than a raw value.
50
+
51
+ `Option` types are a pattern found in many functional languages (the `Option` type in Scala, the `Maybe` monad in Haskell) and the Ruby implementation used by id can be found [here](http://github.com/rsslldnphy/optional).
52
+
53
+ ###### Can I set default values?
54
+
55
+ Yes. Default values are specified like this:
56
+
57
+ ```ruby
58
+ class Cat
59
+ ...
60
+ field :paws, default: 4
61
+ end
62
+
63
+ Cat.new.paws # => 4
64
+ ```
65
+
66
+ You can also specify lambda defaults. These will be run on initialization of an instance of your model.
67
+
68
+ ```ruby
69
+ class Cat
70
+ ...
71
+ field :birthday, default: -> { Date.new }
72
+ end
73
+
74
+ Cat.new.birthday # => #<Date: 2013-10-21 ((2456587j,0s,0n),+0s,2299161j)>
75
+ ```
76
+
77
+ ###### I don't like the key names in my data-source :-(
78
+
79
+ We don't always get what we want. But don't despair! If the hash you're using to create your models has badly named keys - e.g., horror of horrors, keys in camelCase - you can use key aliases to convert them to nice, succinct, Rubyish identifiers:
80
+
81
+ ```ruby
82
+ class Camel
83
+ include Id::Model
84
+
85
+ field :name, key: 'camelName'
86
+ field :humps, key: 'camelHumps'
87
+ end
88
+
89
+ Camel.new('camelName' => 'Terry').name # => "Terry"
90
+ ```
91
+
92
+ #### Type Coercion
93
+
94
+ Id can coerce your fields into a number of basic types. Just specify the type you want as part of the field definition.
95
+
96
+ ```ruby
97
+ class Cat
98
+ ...
99
+ field :paws, type: Integer
100
+ end
101
+
102
+ Cat.new(paws: "3").paws => 3
103
+ Cat.new(paws: "3").paws.class => Integer
104
+ ```
105
+
106
+ You can typecast array elements like this:
107
+
108
+ ```ruby
109
+ field :counts, type: Array[Integer]
110
+ ```
111
+
112
+ Because Ruby doesn't have a `Boolean` type (weird, right?), if you want to coerce something to either `true` or `false`, you need to use `Id::Boolean`, like this:
113
+
114
+ ```ruby
115
+ field :admin, type: Id::Boolean
116
+ ```
117
+
118
+ And if you need to coerce a type id doesn't yet support, such as a custom type of your own, you can register new coercions by passing the "from" type, "to" type, and a block to perform the conversion to `Id::Coercion.register`.
119
+
120
+ ```ruby
121
+ Id::Coercion.register String, Money do |value|
122
+ value.to_money
123
+ end
124
+ ```
125
+ or more succinctly:
126
+
127
+ ```ruby
128
+ Id::Coercion.register String, Money, &:to_money
129
+ ```
27
130
 
28
131
  #### Associations
29
132
 
30
- You can also specify has_one or has_many "associations" - what would be nested subdocuments in MongoDB for example - like this:
133
+ If you have nested arrays or hashes in your source, you can define id models for them in turn, and treat them much like you would associations in an ORM, by defining them as `has_one` or `has_many` associations on the parent model.
31
134
 
32
- class Zoo
33
- include Id::Model
135
+ ```ruby
136
+ class Lion
137
+ include Id::Model
138
+ field :name
139
+ end
34
140
 
35
- has_many :lions
36
- has_many :zebras
37
- has_one :zookeeper, type: Person
38
- end
141
+ class Person
142
+ include Id::Model
143
+ field :name
144
+ end
39
145
 
40
- zoo = Zoo.new(lions: [{name: 'Hetty'}],
41
- zebras: [{name: 'Lisa'}],
42
- zookeeper: {name: 'Russell' d})
146
+ class Zoo
147
+ include Id::Model
43
148
 
44
- zoo.lions.first.class # => Lion
45
- zoo.lions.first.name # => "Hetty"
46
- zoo.zookeeper.class # => Person
47
- zoo.zookeeper.name # => "Russell"
149
+ has_many :lions
150
+ has_one :zookeeper, type: Person
151
+ end
48
152
 
49
- Types are inferred from the association name unless one is specified.
153
+ zoo = Zoo.new(lions: [{name: 'Hetty'}],
154
+ zookeeper: {name: 'Russell'})
155
+
156
+ zoo.lions.first.class # => Lion
157
+ zoo.lions.first.name # => "Hetty"
158
+ zoo.zookeeper.class # => Person
159
+ zoo.zookeeper.name # => "Russell"
160
+ ```
161
+
162
+ Types are inferred from the association name unless specified.
50
163
 
51
164
  #### Designed for immutability
52
165
 
53
166
  `id` models provide accessor methods, but no mutator methods, because they are designed for immutability. How do immutable models work? When you need to change some field of a model object, a new copy of the object is created with the field changed as required. This is handled for you by `id`'s `set` method:
54
167
 
55
- person = Person.new(name: 'Russell', job: 'programmer')
56
- person.set(name: 'Radek') # => returns a new Person whose name is Radek and whose job is 'programmer'
168
+ ```ruby
169
+ person1 = Person.new(name: 'Russell', job: 'programmer')
170
+ person2 = person1.set(name: 'Radek')
171
+ person1.name # => 'Russell'
172
+ person2.name # => 'Radek'
173
+ person2.job # => 'programmer'
174
+ ```
175
+
176
+ Obviously, this is Ruby, and if you really want to mutate some of the internal state of an id model you will be able to find a way to do it. Don't do it!
177
+
178
+ #### Id and Rails
179
+
180
+ We might not love everything about Rails, but we probably use it at least some of the time. So does id play nicely with it?
181
+
182
+ With a few modifications, yes. Models that blow up at the sight of `nil` don't play happy with Rails' `nil`-happy forms, but you can make your id models active model compliant in forms, while otherwise retaining the `nil`-allergy in the rest of your code, by doing two things.
183
+
184
+ Include the `Id::Form` module in your model.
185
+
186
+ ```ruby
187
+ class Cat
188
+ include Id::Model
189
+ include Id::Form
190
+ end
191
+ ```
192
+
193
+ Add the following line to `config/application.rb`:
57
194
 
58
- You can even set fields on nested models in this way:
195
+ ```ruby
196
+ config.action_view.default_form_builder = Id::FormBuilder
197
+ ```
59
198
 
60
- person.hat.set(color: 'red') # => returns a new person object with a new hat object with its color set to red
199
+ With `Id::Form` included you can use Active Model validations as normal, and override methods like `to_partial_path`, `self.model_name`, and `persisted?` right there in your model.
61
200
 
62
- #### Avoiding nils
201
+ ### Timestamps
63
202
 
64
- `id` tries to avoid nils entirely, by using the Option pattern found in many functional programming languages and implemented [here](http://github.com/rsslldnphy/optional).
65
- Just mark optional fields as `optional: true` and their accessors will return either `Some[value]` or `None`.
203
+ And finally, it's reasonably common to want to know when a particular model was created and/or updated. Id provides you this functionality out of the box through the `Id::Timestamps` module. Just include it to your model to get `created_at` and `updated_at` fields that behave as you would expect.
data/id.gemspec CHANGED
@@ -1,15 +1,16 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'id'
3
- s.version = '0.0.12'
3
+ s.version = '0.1'
4
4
  s.date = '2013-03-28'
5
5
  s.summary = "Simple models based on hashes"
6
- s.description = "Developed at On The Beach Ltd. Contact russell.dunphy@onthebeach.co.uk"
6
+ s.description = "Easily convert hashes to Ruby objects. Originally developed at www.onthebeach.co.uk."
7
7
  s.authors = ["Russell Dunphy", "Radek Molenda"]
8
8
  s.email = ['rssll@rsslldnphy.com', 'radek.molenda@gmail.com']
9
9
  s.files = `git ls-files`.split($\)
10
10
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
11
11
  s.require_paths = ["lib"]
12
- s.homepage = 'http://github.com/onthebeach/id'
12
+ s.homepage = 'http://github.com/rsslldnphy/id'
13
+ s.license = 'MIT'
13
14
 
14
15
  s.add_dependency "optional"
15
16
  s.add_dependency "money"
@@ -17,6 +18,10 @@ Gem::Specification.new do |s|
17
18
  s.add_dependency "activemodel"
18
19
 
19
20
  s.add_development_dependency "rspec"
21
+ s.add_development_dependency "mocha"
20
22
  s.add_development_dependency "simplecov"
23
+ s.add_development_dependency "coveralls"
24
+ s.add_development_dependency "test-unit"
25
+
21
26
 
22
27
  end
data/lib/id.rb CHANGED
@@ -1,22 +1,28 @@
1
- require 'active_model'
1
+ module Id
2
+ end
3
+
2
4
  require 'active_support/inflector'
3
5
  require 'active_support/core_ext/string/inflections'
4
6
  require 'active_support/core_ext/hash/except'
7
+ require 'active_support/core_ext/hash/keys'
8
+
9
+ require 'active_model'
5
10
  require 'optional'
6
- require 'money'
7
- require 'id/missing_attribute_error'
11
+ require 'delegate'
12
+ require 'date'
13
+ require 'time'
14
+
15
+ require 'id/boolean'
16
+ require 'id/coercion'
8
17
  require 'id/hashifier'
9
- require 'id/model/definer'
10
- require 'id/model/descriptor'
11
- require 'id/model/type_casts'
12
- require 'id/model/field'
13
- require 'id/model/association'
14
- require 'id/model/has_one'
15
- require 'id/model/has_many'
18
+ require 'id/field/summary'
19
+ require 'id/field/definition'
20
+ require 'id/field'
21
+ require 'id/association'
22
+ require 'id/eta_expansion'
16
23
  require 'id/model'
17
- require 'id/timestamps'
24
+ require 'id/validations'
25
+ require 'id/active_model'
26
+ require 'id/form_backwards_compatibility'
18
27
  require 'id/form'
19
- require 'id/form/active_model_form'
20
- require 'id/form/descriptor'
21
- require 'id/form/field_with_form_support'
22
- require 'id/form/field_form'
28
+ require 'id/timestamps'
@@ -0,0 +1,30 @@
1
+ class Id::ActiveModel
2
+ include ActiveModel::Conversion
3
+ include ActiveModel::Validations
4
+
5
+ def self.i18n_scope
6
+ :id
7
+ end
8
+
9
+ def initialize(model, data)
10
+ @model = model
11
+ @data = data
12
+ end
13
+
14
+ def to_partial_path
15
+ model.respond_to?(:to_partial_path) ? model.to_partial_path : super
16
+ end
17
+
18
+ def respond_to?(name, include_private = false)
19
+ super || model.respond_to?(name, include_private)
20
+ end
21
+
22
+ private
23
+
24
+ def method_missing(name, *args, &block)
25
+ field = model.fields[name]
26
+ field.nil? ? model.send(name, *args, &block) : data[field.key]
27
+ end
28
+
29
+ attr_reader :model, :data
30
+ end
@@ -0,0 +1,26 @@
1
+ module Id::Association
2
+
3
+ def has_one(name, options = {})
4
+ type = options[:type] || infer_class(name)
5
+ options = options.merge(type: type)
6
+ field name, options
7
+ end
8
+
9
+ def has_many(name, options = {})
10
+ type = Array[options[:type] || infer_class(name)]
11
+ options = options.merge(type: type)
12
+ field name, options
13
+ end
14
+
15
+ private
16
+
17
+ def infer_class(name)
18
+ name = name.to_s.singularize.classify
19
+ if const_defined?(name)
20
+ const_get(name)
21
+ else
22
+ parent = self.name.sub(/::[^:]+$/, '')
23
+ parent.constantize.const_get(name)
24
+ end
25
+ end
26
+ end