attrocity 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +30 -10
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile.lock +37 -0
- data/LICENSE +22 -0
- data/README.md +32 -2
- data/Rakefile +14 -0
- data/TODO.md +36 -0
- data/attrocity.gemspec +5 -2
- data/lib/attrocity.rb +91 -2
- data/lib/attrocity/attributes/attribute.rb +10 -0
- data/lib/attrocity/attributes/attribute_methods_builder.rb +47 -0
- data/lib/attrocity/attributes/attribute_set.rb +24 -0
- data/lib/attrocity/attributes/attribute_template.rb +28 -0
- data/lib/attrocity/attributes/attribute_template_set.rb +16 -0
- data/lib/attrocity/attributes/attributes_hash.rb +9 -0
- data/lib/attrocity/attributes/model_attribute.rb +14 -0
- data/lib/attrocity/attributes/model_attribute_set.rb +6 -0
- data/lib/attrocity/builders/model_builder.rb +60 -0
- data/lib/attrocity/builders/object_extension_builder.rb +18 -0
- data/lib/attrocity/coercer_registry.rb +38 -0
- data/lib/attrocity/coercers/boolean.rb +12 -0
- data/lib/attrocity/coercers/integer.rb +13 -0
- data/lib/attrocity/coercers/string.rb +14 -0
- data/lib/attrocity/mappers/key_mapper.rb +14 -0
- data/lib/attrocity/model.rb +10 -0
- data/lib/attrocity/value_extractor.rb +23 -0
- data/lib/attrocity/version.rb +1 -1
- data/notes.md +253 -0
- data/spec/attrocity/attributes/attribute_methods_builder_spec.rb +57 -0
- data/spec/attrocity/attributes/attribute_set_spec.rb +24 -0
- data/spec/attrocity/attributes/attribute_spec.rb +4 -0
- data/spec/attrocity/attributes/attribute_template_set_spec.rb +6 -0
- data/spec/attrocity/attributes/attribute_template_spec.rb +25 -0
- data/spec/attrocity/attributes/model_attribute_spec.rb +26 -0
- data/spec/attrocity/attrocity_spec.rb +83 -0
- data/spec/attrocity/coercer_registry_spec.rb +14 -0
- data/spec/attrocity/coercers/boolean_spec.rb +32 -0
- data/spec/attrocity/coercers/coercer_with_args_spec.rb +9 -0
- data/spec/attrocity/coercers/integer_spec.rb +25 -0
- data/spec/attrocity/coercers/string_spec.rb +13 -0
- data/spec/attrocity/default_value_spec.rb +22 -0
- data/spec/attrocity/mappers/key_mapper_spec.rb +22 -0
- data/spec/attrocity/mapping_spec.rb +47 -0
- data/spec/attrocity/model_spec.rb +34 -0
- data/spec/attrocity/object_extension_spec.rb +53 -0
- data/spec/attrocity/value_extractor_spec.rb +23 -0
- data/spec/spec_helper.rb +93 -0
- data/spec/support/examples.rb +79 -0
- metadata +99 -8
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'attribute_set'
|
2
|
+
require_relative 'attributes_hash'
|
3
|
+
|
4
|
+
module Attrocity
|
5
|
+
class AttributeTemplateSet < AttributeSet
|
6
|
+
|
7
|
+
def to_attribute_set(data)
|
8
|
+
AttributeSet.new.tap do |set|
|
9
|
+
self.attributes.each do |attr_template|
|
10
|
+
set << attr_template.to_attribute(data)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Attrocity
|
2
|
+
class ModelAttribute
|
3
|
+
attr_reader :name, :model_class
|
4
|
+
|
5
|
+
def initialize(name, model_class)
|
6
|
+
@name = name
|
7
|
+
@model_class = model_class
|
8
|
+
end
|
9
|
+
|
10
|
+
def model(data)
|
11
|
+
model_class.new(AttributesHash.new(data)).model
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Attrocity
|
2
|
+
class ModelBuilder < Module
|
3
|
+
|
4
|
+
def included(klass)
|
5
|
+
klass.extend(Attrocity::ModuleMethods)
|
6
|
+
klass.class_eval do
|
7
|
+
attr_reader :raw_data, :attribute_set
|
8
|
+
end
|
9
|
+
klass.send(:include, Initializer)
|
10
|
+
klass.send(:include, InstanceMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module Initializer
|
14
|
+
def initialize(data={})
|
15
|
+
@raw_data = AttributesHash.new(data)
|
16
|
+
@attribute_set = init_value_attributes
|
17
|
+
build_attributes_methods
|
18
|
+
setup_model_attributes
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
def model
|
24
|
+
Model.new(attribute_set.to_h.merge(model_attributes_hash))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def init_value_attributes
|
30
|
+
self.class.attribute_set.to_attribute_set(raw_data)
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_attributes_methods
|
34
|
+
AttributeMethodsBuilder.for_attribute_set(self, attribute_set).build
|
35
|
+
end
|
36
|
+
|
37
|
+
def setup_model_attributes
|
38
|
+
model_attributes.each do |model_attr|
|
39
|
+
name = model_attr.name
|
40
|
+
define_singleton_method(name) {
|
41
|
+
instance_eval("@#{name} ||= model_attr.model(raw_data)")
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def model_attributes
|
47
|
+
@model_attributes ||= self.class.model_attribute_set.attributes
|
48
|
+
end
|
49
|
+
|
50
|
+
def model_attributes_hash
|
51
|
+
Hash.new.tap do |h|
|
52
|
+
model_attributes.each do |model_attr|
|
53
|
+
name = model_attr.name
|
54
|
+
h[name] = self.send(name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Attrocity
|
2
|
+
class ObjectExtensionBuilder < Module
|
3
|
+
|
4
|
+
def included(mod)
|
5
|
+
mod.extend(Attrocity::ModuleMethods)
|
6
|
+
mod.extend(ModuleHooks)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ModuleHooks
|
10
|
+
def extend_object(obj)
|
11
|
+
value_attr_set = self.attribute_set.to_attribute_set(obj.raw_data)
|
12
|
+
obj.attribute_set << value_attr_set.attributes
|
13
|
+
AttributeMethodsBuilder.for_attribute_set(obj, value_attr_set).build
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Attrocity
|
2
|
+
|
3
|
+
UnknownCoercerError = Class.new(StandardError)
|
4
|
+
|
5
|
+
class CoercerRegistry
|
6
|
+
def self.register(&block)
|
7
|
+
class_eval(&block) if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.add(name, coercer_class)
|
11
|
+
registry[name] = coercer_class
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.to_s
|
15
|
+
registry.inspect
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.coercer_for(name)
|
19
|
+
registry.fetch(name)
|
20
|
+
rescue KeyError
|
21
|
+
raise UnknownCoercerError
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.instance_for(name, params={})
|
25
|
+
if params.empty?
|
26
|
+
coercer_for(name).new
|
27
|
+
else
|
28
|
+
coercer_for(name).new(params)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def self.registry
|
35
|
+
@registry ||= {}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Attrocity
|
2
|
+
class KeyMapper
|
3
|
+
attr_reader :key, :default_value
|
4
|
+
|
5
|
+
def initialize(key, default_value=nil)
|
6
|
+
@key = key
|
7
|
+
@default_value = default_value
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(_, attributes_data)
|
11
|
+
attributes_data.fetch(key, default_value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Attrocity
|
2
|
+
class ValueExtractor
|
3
|
+
attr_reader :data, :mapper, :coercer
|
4
|
+
|
5
|
+
def initialize(data, mapper:, coercer:)
|
6
|
+
@data, @mapper, @coercer = data, mapper, coercer
|
7
|
+
end
|
8
|
+
|
9
|
+
def value
|
10
|
+
coerce(map)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def map
|
16
|
+
mapper.call(nil, data)
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce(value)
|
20
|
+
coercer.coerce(value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/attrocity/version.rb
CHANGED
data/notes.md
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
# Notes
|
2
|
+
|
3
|
+
## Mappers/Coercers data injection
|
4
|
+
|
5
|
+
The crux of the issue is that for mapping or coercing I might want to inject some values, such as null values to compare against. These values differ on a per-attribute basis.
|
6
|
+
|
7
|
+
The library calls specific methods with specific arguments. I need to get those args into the library.
|
8
|
+
|
9
|
+
It does feel like more of a coercion problem than a mapping problem. I think I originally reached for the mapper because I thought I had more control.
|
10
|
+
|
11
|
+
- [ ] ? Via coercer registry?
|
12
|
+
|
13
|
+
- [ ] ? Change to the attribute DSL for coercer? Pass hash with args? If not a hash, but simply a symbol, assume it's the name?
|
14
|
+
|
15
|
+
- [ ] ? How might we do this with blocks?
|
16
|
+
|
17
|
+
## Current refactorings
|
18
|
+
|
19
|
+
- [ ] Process pending specs: delete unnecessary, make valid ones pass.
|
20
|
+
|
21
|
+
## Handling missing data
|
22
|
+
|
23
|
+
I think these are the rules...
|
24
|
+
|
25
|
+
It should probably be nil. Perhaps if the mapper returns nil we don't even bother coercing it. There's a concept here and it's around extracting data. Every attribute has a coercer, by rule of API, and every attribute has a mapper, either explicitly declared or the default KeyMapper. When an attribute is missing, in other words when the mapper cannot extract data (returns nil), the default value is applied. The "default" default value is nil. When the default value is applied, no coercion is performed.
|
26
|
+
|
27
|
+
ValueExtractor (or a better named class) takes a mapper, a coercer, and a default value, which defaults to nil.
|
28
|
+
|
29
|
+
- [ ] Write up some spec/examples and also for the README
|
30
|
+
|
31
|
+
## Attributes
|
32
|
+
|
33
|
+
### Collaborators
|
34
|
+
|
35
|
+
In which collaborator would it most make sense to add in default value behavior?
|
36
|
+
|
37
|
+
Mapper: Simply retrieves the data from raw attributes data. `call` it with an object and a hash of data and it returns a value. It might return nil. Potentially, it could accept a default and fallback to that value, but I'm not sure that's within the responsibility of this object.
|
38
|
+
|
39
|
+
Coercer: Simply assures the data its passed comes back as the correct type.
|
40
|
+
|
41
|
+
Who knows how to assemble/initialize a Mapper? I don't think Attribute needs to
|
42
|
+
be the keeper of that knowledge. If Mapper's public interface is to receive the
|
43
|
+
call message with obj, attributes_data, and an optional default value, then a
|
44
|
+
fully realized mapper should be passed into an Attribute, moving that work
|
45
|
+
outside of Attribute and simplifying it.
|
46
|
+
|
47
|
+
Of the 2 objects, coercer and mapper, if I had to choose between the two to shim
|
48
|
+
default value behavior, I would choose the mapper.
|
49
|
+
|
50
|
+
Do we map a coerced value OR do we coerce a mapped value? I think we coerce a
|
51
|
+
mapped value. Right. Map, then send the result to the coercer.
|
52
|
+
|
53
|
+
Do we map/coerce before handing the value to an InstanceAttribute?
|
54
|
+
|
55
|
+
I like the idea of working inside-out here, from the innermost objects outward,
|
56
|
+
because I think we have the right objects, they're just combined in funky ways.
|
57
|
+
At this point, it feels right to start with mapper -> coercer -> ?.
|
58
|
+
(ClassAttribute probably doesn't need any of those data-related collaborators.)
|
59
|
+
|
60
|
+
### Coercers
|
61
|
+
|
62
|
+
I'm concerned the return value/behavior of coercers is inconsistent. I'm not
|
63
|
+
sure what to do about it yet, though, other than note it.
|
64
|
+
|
65
|
+
Coercers::Integer - It's possible in a couple different ways to raise an error,
|
66
|
+
so we normalize the different kinds of errors into a single
|
67
|
+
Attrocity::CoercionError. We are simply using Kernel.Integer for this coercer,
|
68
|
+
which is the source for raising (at least) a couple different errors: TypeError,
|
69
|
+
ArgumentError.
|
70
|
+
|
71
|
+
Coercers::String: - It doesn't seem possible for this coercer to raise an error
|
72
|
+
in its current implementation. It uses Kernel.String, which in turn uses to_s,
|
73
|
+
which is implemented in Object.
|
74
|
+
|
75
|
+
Coercers::Boolean - At this point, this is a custom implementation, specific to
|
76
|
+
RentPath, but I'm thinking that it should default to the Ruby notion of
|
77
|
+
truthiness. Yes, the default boolean coercer will adhere to the Ruby notion of
|
78
|
+
truthiness. But we need to then inject a coercer that's not part of the built-in
|
79
|
+
coercers, which we should probably do in RentalsModels. This work is done.
|
80
|
+
|
81
|
+
### ClassAttribute
|
82
|
+
|
83
|
+
- [ ] Start with default_value as a simple Ruby attribute (data).
|
84
|
+
|
85
|
+
### InstanceAttribute
|
86
|
+
|
87
|
+
Start here and work backwards towards ClassAttribute.
|
88
|
+
|
89
|
+
---
|
90
|
+
|
91
|
+
DefaultValue
|
92
|
+
NullDefaultValue
|
93
|
+
|
94
|
+
Should a default value of nil be supported?
|
95
|
+
|
96
|
+
default_value_for
|
97
|
+
|
98
|
+
---
|
99
|
+
|
100
|
+
clone is confusing
|
101
|
+
|
102
|
+
---
|
103
|
+
|
104
|
+
Is options a class or a module?
|
105
|
+
|
106
|
+
Options attributes are first-class attributes (with coercion, mapping, etc.)
|
107
|
+
|
108
|
+
# TODO
|
109
|
+
|
110
|
+
- Convert all specs to require spec_helper
|
111
|
+
|
112
|
+
- Attribute should have a method for setting its own value that take the
|
113
|
+
arguments that attribute set uses to do the job. ???
|
114
|
+
|
115
|
+
- [ ] ? Should attrocity_spec specs be moved to something like module builder
|
116
|
+
spec?
|
117
|
+
|
118
|
+
- [ ] ? Re-evaluate attrocity_spec. There are some oddities. It seems like it's
|
119
|
+
been a default place to put spec code that we weren't sure about the eventual
|
120
|
+
home for.
|
121
|
+
|
122
|
+
Attribute set cloning
|
123
|
+
---------------------
|
124
|
+
|
125
|
+
Consider making attributes that are attached to a class, that basically act as
|
126
|
+
templates for instance attributes, to be a different kind of thing so that we're
|
127
|
+
not cloning, per se, but creating an instance variant of the class attribute
|
128
|
+
variant on the fly. So, factory approach vs. cloning approach.
|
129
|
+
|
130
|
+
Inventory
|
131
|
+
---------
|
132
|
+
|
133
|
+
### Default values
|
134
|
+
|
135
|
+
For now, let's just implement the simple version of default values. Will need to
|
136
|
+
think through best implementation. Guessing some here... the simplest
|
137
|
+
implementation is to pass an option to Attribute.new and have the attribute
|
138
|
+
handle default values. DO NOT put this responsibility into the coercer. Coercers
|
139
|
+
must be kept simple.
|
140
|
+
|
141
|
+
How to handle defaults because `Integer(nil)` raises an error
|
142
|
+
|
143
|
+
Probably handle defaults at attribute level. Based on default property of an
|
144
|
+
attribute instance, it will handle coercion errors accordingly. Does this affect
|
145
|
+
validation?
|
146
|
+
|
147
|
+
Default values should not be conflated with coercers. However, when analyzing a
|
148
|
+
default value, we might want to check that it does not get transformed when
|
149
|
+
coerced. In other words, with an integer coercer and a default value of 1,
|
150
|
+
Integer(1) does not change the value, but a default value of '1' would be
|
151
|
+
considered incorrect and we could catch that by coercing and comparing.
|
152
|
+
|
153
|
+
It should be possible to have a nil value, but I don't like the idea of nil
|
154
|
+
being the fallback value. A string coerced attribute should probably default to
|
155
|
+
''. Not sure what an integer coerced attribute should default to. Maybe nil as
|
156
|
+
fallback is the only consistent way to do this.
|
157
|
+
|
158
|
+
### Validation?
|
159
|
+
|
160
|
+
|
161
|
+
Design inspiration
|
162
|
+
------------------
|
163
|
+
|
164
|
+
- Virtus
|
165
|
+
- attrio
|
166
|
+
http://igor-alexandrov.github.io/blog/2013/05/23/attrio-typed-attributes-for-ruby-objects/
|
167
|
+
|
168
|
+
|
169
|
+
Concepts
|
170
|
+
--------
|
171
|
+
|
172
|
+
- Coercing: Converting attribute data to the desired type
|
173
|
+
|
174
|
+
- Mapping: Getting attribute data from a known hash key that differs from the
|
175
|
+
attribute name
|
176
|
+
|
177
|
+
|
178
|
+
Ideas
|
179
|
+
-----
|
180
|
+
|
181
|
+
Lazy forwarding to attribute_set (using method_missing or similar) will allow us
|
182
|
+
to modify the attribute set on an object instance without having to muck around
|
183
|
+
with undefining methods and such. For an attribute named :age, we want to allow
|
184
|
+
for methods: age, age=, [:age], age?. We want to be able to add and remove
|
185
|
+
attributes and the object should just work without a bunch of metaprogramming on
|
186
|
+
our part.
|
187
|
+
|
188
|
+
On initialize, attributes are collected into an instance-scope AttributeSet.
|
189
|
+
When an instance of this class is extended, e.g., `my_class.extend(MyModule)`,
|
190
|
+
the attribute set of the instance gets the attributes of the module. This is
|
191
|
+
very much like Virtus, only we're working with instances. We might also work
|
192
|
+
with classes, but definitely instances, which allows us to do DCI-style dynamic
|
193
|
+
role/behavior extensions.
|
194
|
+
|
195
|
+
|
196
|
+
Documentation
|
197
|
+
-------------
|
198
|
+
|
199
|
+
Initialization with a hash is required.
|
200
|
+
|
201
|
+
### AttributeSet
|
202
|
+
|
203
|
+
- AttributeSet#to_h
|
204
|
+
- AttributeSet#to_value_object. Should attribute_set be the thing that handles
|
205
|
+
handing back a value object.
|
206
|
+
|
207
|
+
Value object may be the wrong term. It may be 'immutable' not value. Investigate
|
208
|
+
ways to generate bare immutable objects in Ruby.
|
209
|
+
[immutable_struct](https://github.com/iconara/immutable_struct) looks like a
|
210
|
+
good and not bloated option. It's a bit dated, so might need to be forked.
|
211
|
+
|
212
|
+
Lightweight, but not great option:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
# from attributes hash
|
216
|
+
Struct.new(*keys).new(*values).freeze
|
217
|
+
# raises a RuntimeError on mutation
|
218
|
+
```
|
219
|
+
|
220
|
+
Attrocity objects forward #umapped_attributes and #attributes to AttributeSet
|
221
|
+
|
222
|
+
### Hooks
|
223
|
+
|
224
|
+
Implement Mod.extended(obj) hook such that when the obj.extend(Mod) triggers it,
|
225
|
+
the obj.attribute_set is merged with Mod.attributes (or Mod.attribute_set)
|
226
|
+
|
227
|
+
Coercers
|
228
|
+
--------
|
229
|
+
|
230
|
+
Comes with a built-in set of coercers.
|
231
|
+
|
232
|
+
Additional coercers are added through a registry.
|
233
|
+
|
234
|
+
The library emphasizes custom coercion over types. For example,
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
attribute :publication, :book # looks for a Book coercer
|
238
|
+
```
|
239
|
+
|
240
|
+
Should coercers be initialized with an attribute? That way they can get
|
241
|
+
defaults, etc.
|
242
|
+
|
243
|
+
I don't think the cost of inheritance is worth the enforcement of implementing a
|
244
|
+
simple API. Let's just define what coercers are and document it.
|
245
|
+
|
246
|
+
The coercion API is defined as: an instance method called coerce. Why an
|
247
|
+
instance method? Because it's more accommodating to future changes.
|
248
|
+
|
249
|
+
Documentation for README
|
250
|
+
------------------------
|
251
|
+
|
252
|
+
Attribute values are set and retrieved through their containing AttributeSet.
|
253
|
+
The Attribute set acts as an aggregate root (in DDD parlance).
|