id 0.0.12 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -3
- data/Gemfile.lock +25 -10
- data/LICENSE.md +1 -1
- data/README.md +173 -35
- data/id.gemspec +8 -3
- data/lib/id.rb +21 -15
- data/lib/id/active_model.rb +30 -0
- data/lib/id/association.rb +26 -0
- data/lib/id/boolean.rb +8 -0
- data/lib/id/coercion.rb +38 -0
- data/lib/id/eta_expansion.rb +5 -0
- data/lib/id/field.rb +46 -0
- data/lib/id/field/definition.rb +44 -0
- data/lib/id/field/summary.rb +35 -0
- data/lib/id/form.rb +41 -13
- data/lib/id/form_backwards_compatibility.rb +6 -0
- data/lib/id/hashifier.rb +13 -24
- data/lib/id/model.rb +29 -25
- data/lib/id/timestamps.rb +15 -15
- data/lib/id/validations.rb +8 -0
- data/spec/examples/cat_spec.rb +37 -0
- data/spec/lib/id/active_model_spec.rb +40 -0
- data/spec/lib/id/association_spec.rb +73 -0
- data/spec/lib/id/boolean_spec.rb +38 -0
- data/spec/lib/id/coercion_spec.rb +53 -0
- data/spec/lib/id/eta_expansion_spec.rb +13 -0
- data/spec/lib/id/field/definition_spec.rb +37 -0
- data/spec/lib/id/field/summary_spec.rb +26 -0
- data/spec/lib/id/field_spec.rb +62 -0
- data/spec/lib/id/form_spec.rb +84 -0
- data/spec/lib/id/hashifier_spec.rb +19 -0
- data/spec/lib/id/model_spec.rb +30 -180
- data/spec/lib/id/timestamps_spec.rb +22 -20
- data/spec/lib/id/validations_spec.rb +18 -0
- data/spec/spec_helper.rb +11 -5
- data/spec/support/dummy_rails_form_builder.rb +9 -0
- metadata +84 -26
- data/lib/id/form/active_model_form.rb +0 -41
- data/lib/id/form/descriptor.rb +0 -27
- data/lib/id/form/field_form.rb +0 -14
- data/lib/id/form/field_with_form_support.rb +0 -12
- data/lib/id/missing_attribute_error.rb +0 -4
- data/lib/id/model/association.rb +0 -49
- data/lib/id/model/definer.rb +0 -23
- data/lib/id/model/descriptor.rb +0 -23
- data/lib/id/model/field.rb +0 -61
- data/lib/id/model/has_many.rb +0 -11
- data/lib/id/model/has_one.rb +0 -17
- data/lib/id/model/type_casts.rb +0 -96
- data/spec/lib/id/model/association_spec.rb +0 -27
- data/spec/lib/id/model/field_spec.rb +0 -0
- data/spec/lib/id/model/form_spec.rb +0 -56
- data/spec/lib/id/model/type_casts_spec.rb +0 -44
- data/spec/lib/mike_spec.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4626765bc042e4c08215636a3c50e751729ae2c7
|
4
|
+
data.tar.gz: 6a549d6d328b1b91583f9ec7f2819b6f9ecea0a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6698ecb274e81d09550a410d6bf547b48d4ed124f173d0f9ef0d0270d0ca76eef757887c40c73c4c16b87167c1a6e86b0a63d21975d328b9c45464d1b7f3899d
|
7
|
+
data.tar.gz: 38ac9a455e27e9751e0fc3b66ce5d730c70cfb5ce83b38cfc4f66945f3147aecd199792994a675e85b8c069753d66e954d1f1aacc26c56d101af1e16cd3a17cd
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
id (0.
|
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.
|
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.
|
26
|
-
|
31
|
+
i18n (0.6.5)
|
32
|
+
metaclass (0.0.1)
|
33
|
+
mime-types (1.25)
|
27
34
|
minitest (4.7.5)
|
28
|
-
|
29
|
-
|
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
|
-
|
58
|
+
thor (0.18.1)
|
59
|
+
thread_safe (0.1.3)
|
46
60
|
atomic
|
47
|
-
|
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
|
-
|
55
|
-
money (= 3.7.1)
|
70
|
+
mocha
|
56
71
|
rspec
|
57
72
|
simplecov
|
58
73
|
test-unit
|
data/LICENSE.md
CHANGED
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
|
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
|
-
|
10
|
+
To define a model, include the `Id::Model` module in your class.
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
```ruby
|
13
|
+
class Cat
|
14
|
+
include Id::Model
|
15
|
+
end
|
16
|
+
```
|
14
17
|
|
15
|
-
|
16
|
-
field :bar, default: 42
|
17
|
-
field :baz, key: 'barry'
|
18
|
+
#### Defining fields
|
18
19
|
|
19
|
-
|
20
|
+
At its most basic, you can define a field like this:
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
```ruby
|
23
|
+
class Cat
|
24
|
+
field :name
|
25
|
+
end
|
26
|
+
```
|
25
27
|
|
26
|
-
|
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
|
-
|
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
|
-
|
33
|
-
|
135
|
+
```ruby
|
136
|
+
class Lion
|
137
|
+
include Id::Model
|
138
|
+
field :name
|
139
|
+
end
|
34
140
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
141
|
+
class Person
|
142
|
+
include Id::Model
|
143
|
+
field :name
|
144
|
+
end
|
39
145
|
|
40
|
-
|
41
|
-
|
42
|
-
zookeeper: {name: 'Russell' d})
|
146
|
+
class Zoo
|
147
|
+
include Id::Model
|
43
148
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
zoo.zookeeper.name # => "Russell"
|
149
|
+
has_many :lions
|
150
|
+
has_one :zookeeper, type: Person
|
151
|
+
end
|
48
152
|
|
49
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
195
|
+
```ruby
|
196
|
+
config.action_view.default_form_builder = Id::FormBuilder
|
197
|
+
```
|
59
198
|
|
60
|
-
|
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
|
-
|
201
|
+
### Timestamps
|
63
202
|
|
64
|
-
|
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.
|
3
|
+
s.version = '0.1'
|
4
4
|
s.date = '2013-03-28'
|
5
5
|
s.summary = "Simple models based on hashes"
|
6
|
-
s.description = "
|
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/
|
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
|
-
|
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 '
|
7
|
-
require '
|
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/
|
10
|
-
require 'id/
|
11
|
-
require 'id/
|
12
|
-
require 'id/
|
13
|
-
require 'id/
|
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/
|
24
|
+
require 'id/validations'
|
25
|
+
require 'id/active_model'
|
26
|
+
require 'id/form_backwards_compatibility'
|
18
27
|
require 'id/form'
|
19
|
-
require 'id/
|
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
|