bronze 0.0.1.alpha → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +31 -0
- data/DEVELOPMENT.md +78 -0
- data/README.md +801 -14
- data/lib/bronze.rb +2 -11
- data/lib/bronze/entities.rb +10 -0
- data/lib/bronze/entities/attributes.rb +241 -0
- data/lib/bronze/entities/attributes/builder.rb +249 -0
- data/lib/bronze/entities/attributes/metadata.rb +87 -0
- data/lib/bronze/entities/normalization.rb +69 -0
- data/lib/bronze/entities/primary_key.rb +70 -0
- data/lib/bronze/entities/primary_keys.rb +8 -0
- data/lib/bronze/entities/primary_keys/uuid.rb +44 -0
- data/lib/bronze/entity.rb +14 -0
- data/lib/bronze/not_implemented_error.rb +18 -0
- data/lib/bronze/transform.rb +29 -0
- data/lib/bronze/transforms.rb +9 -0
- data/lib/bronze/transforms/attributes.rb +9 -0
- data/lib/bronze/transforms/attributes/big_decimal_transform.rb +40 -0
- data/lib/bronze/transforms/attributes/date_time_transform.rb +60 -0
- data/lib/bronze/transforms/attributes/date_transform.rb +58 -0
- data/lib/bronze/transforms/attributes/symbol_transform.rb +36 -0
- data/lib/bronze/transforms/attributes/time_transform.rb +38 -0
- data/lib/bronze/transforms/entities.rb +9 -0
- data/lib/bronze/transforms/entities/normalize_transform.rb +52 -0
- data/lib/bronze/transforms/identity_transform.rb +31 -0
- data/lib/bronze/version.rb +12 -11
- metadata +70 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 50254e8fbcd0578e935770b70b65de1d39f80c3fdbd071203054d3acf802bf7d
|
4
|
+
data.tar.gz: 468d03f80901e6b245dd27b35ad7af5dcbb9ca006c165244a067ddbdec5a5df2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd6faa02f627b9d585e3229f3bcff896a7c7262cff2d0ae3503b3a04561e5532103dbec681ee5ce4f52b9ea989f8e8a63dac736de2b4d44ad29722d44f74907d
|
7
|
+
data.tar.gz: c332d23085bd7b381431c2ea887a78f4292c9e801939459a59b6a2113aeba197c51fdb540ee7d2b2cfeb1c4b8c765de81eee142e9614e4b97a9fb3b516efd919
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.1.0
|
4
|
+
|
5
|
+
Initial release.
|
6
|
+
|
7
|
+
### Attributes
|
8
|
+
|
9
|
+
Adds the Bronze::Entities::Attributes module, which can be included in any class to define and use attribute properties. Attributes are defined by the ::attribute class method and accessed by getter and setter methods and/or the #attributes, #get_attribute and #set_attribute methods.
|
10
|
+
|
11
|
+
### Entities
|
12
|
+
|
13
|
+
Adds the Bronze::Entity class, which serves as an abstract base class for defining application entities. Includes the Attributes and PrimaryKey modules.
|
14
|
+
|
15
|
+
#### Normalization
|
16
|
+
|
17
|
+
Adds the Bronze::Entities::Normalization module, which can be included in any entity class to define normalization methods, which transform an entity class to a hash of data values and vice versa.
|
18
|
+
|
19
|
+
#### Primary Keys
|
20
|
+
|
21
|
+
Adds the Bronze::Entities::PrimaryKey module, which can be included in any entity class to define a primary key for the entity class.
|
22
|
+
|
23
|
+
Adds the Bronze::Entities::PrimaryKeys::Uuid module, which defines a UUID primary key for the entity class.
|
24
|
+
|
25
|
+
### Transforms
|
26
|
+
|
27
|
+
Adds the Bronze::Transform class, which provides an abstract base class for mono- or bi-directional data transformations.
|
28
|
+
|
29
|
+
Adds normalization transforms for the BigDecimal, DateTime, Date, Symbol and Time classes.
|
30
|
+
|
31
|
+
Adds normalization transform for entities.
|
data/DEVELOPMENT.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Development
|
2
|
+
|
3
|
+
## Entities
|
4
|
+
|
5
|
+
### Attributes
|
6
|
+
|
7
|
+
#### Boolean attributes
|
8
|
+
|
9
|
+
- attribute :flag, Boolean, default: false
|
10
|
+
|
11
|
+
- also generates #flag? predicate
|
12
|
+
|
13
|
+
#### :default option
|
14
|
+
|
15
|
+
- default value method: |
|
16
|
+
#default_introduction => 'It was a dark and stormy night...'
|
17
|
+
|
18
|
+
- update #set_attribute to use default value unless allow_nil is true ?
|
19
|
+
|
20
|
+
- default proc can call instance methods: |
|
21
|
+
attribute :serial_id, String, default: -> { generate_serial_id }
|
22
|
+
|
23
|
+
also applies to Primary Key generation
|
24
|
+
- default proc that uses existing attributes: |
|
25
|
+
attribute :full_name, String, default:
|
26
|
+
->(user) { [user.first_name, user.last_name].compact.join(' ') }
|
27
|
+
|
28
|
+
#### :enum option
|
29
|
+
|
30
|
+
- Unmapped: |
|
31
|
+
attribute :rarity, String, enum: %w(rare medium well)
|
32
|
+
|
33
|
+
Entity::RARITY => %w(rare medium well)
|
34
|
+
Entity::RARITY::WELL => 'well'
|
35
|
+
entity.attributes[:rarity] => 'well'
|
36
|
+
entity.normalize => { rarity: 'well' }
|
37
|
+
|
38
|
+
- Mapped: |
|
39
|
+
attribute :power_level, Integer,
|
40
|
+
enum: { basic: 1, spinal_tap: 11, memetic: 9001 }
|
41
|
+
|
42
|
+
Entity::POWER_LEVEL => { basic: 1, spinal_tap: 11, memetic: 9001 }
|
43
|
+
Entity::POWER_LEVEL::MEMETIC => 9001
|
44
|
+
entity.attributes[:power_level] => 9001
|
45
|
+
entity.power_level => 'memetic'
|
46
|
+
entity.normalize => { power_level: 9001 }
|
47
|
+
|
48
|
+
Integration spec:
|
49
|
+
class PlayingCard
|
50
|
+
attribute :suit,
|
51
|
+
String,
|
52
|
+
enum: %w[clubs diamonds hearts spades]
|
53
|
+
attribute :value,
|
54
|
+
Integer,
|
55
|
+
enum: {
|
56
|
+
ace: 1,
|
57
|
+
two: 2,
|
58
|
+
...
|
59
|
+
ten: 10,
|
60
|
+
jack: 11,
|
61
|
+
king: 12,
|
62
|
+
queen: 13
|
63
|
+
}
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
#### :visible option
|
68
|
+
|
69
|
+
- attribute :hidden, String, visible: false
|
70
|
+
- defaults to true
|
71
|
+
- if visible: false:
|
72
|
+
- do not include in #attributes
|
73
|
+
- make getter, setter private
|
74
|
+
- do include in normalize-denormalize
|
75
|
+
|
76
|
+
## Transforms
|
77
|
+
|
78
|
+
- JSON transform - to/from JSON string
|
data/README.md
CHANGED
@@ -1,31 +1,818 @@
|
|
1
1
|
# Bronze
|
2
2
|
|
3
|
-
A
|
3
|
+
A composable application toolkit, providing data entities and collections, transforms, contract-based validations and pre-built operations. Architecture agnostic for easy integration with other toolkits or frameworks.
|
4
4
|
|
5
|
-
|
5
|
+
Bronze defines the following components:
|
6
6
|
|
7
|
-
|
7
|
+
- [Entities](#label-Entities) - Data structures with defined attributes.
|
8
|
+
- [Transforms](#label-Transforms) - Map values or objects to and from a normal form.
|
8
9
|
|
9
|
-
##
|
10
|
+
## About
|
10
11
|
|
11
|
-
|
12
|
+
[comment]: # "Status Badges will go here."
|
13
|
+
|
14
|
+
### Compatibility
|
15
|
+
|
16
|
+
Bronze is tested against Ruby (MRI) 2.4 through 2.6.
|
17
|
+
|
18
|
+
### Documentation
|
19
|
+
|
20
|
+
Method and class documentation is available courtesy of [RubyDoc](http://www.rubydoc.info/github/sleepingkingstudios/bronze/master).
|
21
|
+
|
22
|
+
Documentation is generated using [YARD](https://yardoc.org/), and can be generated locally using the `yard` gem.
|
23
|
+
|
24
|
+
### License
|
25
|
+
|
26
|
+
Copyright (c) 2018 Rob Smith
|
27
|
+
|
28
|
+
Bronze is released under the [MIT License](https://opensource.org/licenses/MIT).
|
29
|
+
|
30
|
+
### Contribute
|
12
31
|
|
13
32
|
The canonical repository for this gem is located at https://github.com/sleepingkingstudios/bronze.
|
14
33
|
|
15
|
-
|
34
|
+
To report a bug or submit a feature request, please use the [Issue Tracker](https://github.com/sleepingkingstudios/bronze/issues).
|
35
|
+
|
36
|
+
To contribute code, please fork the repository, make the desired updates, and then provide a [Pull Request](https://github.com/sleepingkingstudios/bronze/pulls). Pull requests must include appropriate tests for consideration, and all code must be properly formatted.
|
37
|
+
|
38
|
+
### Dedication
|
39
|
+
|
40
|
+
This project is dedicated to the memory of my grandfather, who taught me the joy of flight.
|
41
|
+
|
42
|
+
## Entities
|
43
|
+
|
44
|
+
require 'bronze/entity'
|
45
|
+
|
46
|
+
An entity is a data object. Each entity class can define [attributes](#label-Entities-3A+Attributes), including a [primary key](#label-Primary+Keys). Entities can be [normalized](#label-Normalization), which transforms the entity to a hash of data values or vice versa.
|
47
|
+
|
48
|
+
class Book < Bronze::Entity
|
49
|
+
attribute :title, String
|
50
|
+
end
|
51
|
+
|
52
|
+
# Creating an entity.
|
53
|
+
book = Book.new
|
54
|
+
book.title => nil
|
55
|
+
|
56
|
+
# Creating an entity with attributes.
|
57
|
+
book = Book.new(title: 'The Hobbit')
|
58
|
+
book.title => 'The Hobbit'
|
59
|
+
|
60
|
+
# Updating an entity's attributes.
|
61
|
+
book.title = 'The Silmarillion'
|
62
|
+
book.title => 'The Silmarillion'
|
63
|
+
|
64
|
+
### Attributes
|
65
|
+
|
66
|
+
require 'bronze/entities/attributes'
|
67
|
+
|
68
|
+
An entity class defines zero or more attributes, which represent the data stored in the entity. Each attribute has a name, a type, and properties, which are stored as metadata and determine how the attribute is read and updated.
|
69
|
+
|
70
|
+
You can also define a thin entity class by including the Attributes module:
|
71
|
+
|
72
|
+
class ThinEntity
|
73
|
+
include Bronze::Entities::Attributes
|
74
|
+
end
|
75
|
+
|
76
|
+
#### ::attribute Class Method
|
77
|
+
|
78
|
+
The `::attribute` class method is used to define attributes for an entity class. For example, the following code defines the `:title` entity for our `Book` entity class.
|
79
|
+
|
80
|
+
class Book < Bronze::Entity
|
81
|
+
attribute :title, String
|
82
|
+
end
|
83
|
+
|
84
|
+
The `::attribute` method requires, at minimum, the name of the attribute (can be either a String or Symbol) and the type of the attribute value. The name determines how the attribute can be read and written. For example, since we have defined a `:title` attribute on `Book`, then each instance of `Book` will have a `#title` reader and a `#title=` writer method.
|
85
|
+
|
86
|
+
The attribute type is used for validations, and when normalizing or denormalizing the entity data.
|
87
|
+
|
88
|
+
You can pass additional options to `::attribute`; see below, starting at [:allow_nil Option](#label-3Aallow_nil+Option).
|
89
|
+
|
90
|
+
#### ::attributes Class Method
|
91
|
+
|
92
|
+
The attributes defined for an entity class are stored as metadata, and is accessible via the `::attributes` class method. This method returns a hash, with the attribute name (as a Symbol) as the hash key and a value of the corresponding metadata. For our Book class, this will look like the following.
|
93
|
+
|
94
|
+
# Listing all defined attributes.
|
95
|
+
Book.attributes => { title: #<Bronze::Entities::Attributes::Metadata> }
|
96
|
+
|
97
|
+
# Metadata for a specific attribute.
|
98
|
+
metadata = Book.attributes[:title]
|
99
|
+
metadata.name #=> :title
|
100
|
+
metadata.type #=> String
|
101
|
+
metadata.options #=> {}
|
102
|
+
|
103
|
+
The metadata also provides helper methods for the attribute options:
|
104
|
+
|
105
|
+
metadata = Book.attributes[:title]
|
106
|
+
metadata.allow_nil? #=> false
|
107
|
+
metadata.default? #=> false
|
108
|
+
metadata.read_only? #=> false
|
109
|
+
|
110
|
+
#### ::each_attribute Class Method
|
111
|
+
|
112
|
+
As an alternative, the `::each_attribute` method allows you to iterate through the attributes defined for an entity class. If no block is given, it returns an Enumerator, otherwise, it yields the name and metadata of each defined attribute to the block.
|
113
|
+
|
114
|
+
Book.each_attribute { |name, metadata| puts name, metadata.options }
|
115
|
+
|
116
|
+
Using `::each_attribute` is recommended over `::attributes` where possible.
|
117
|
+
|
118
|
+
#### #== Operator
|
119
|
+
|
120
|
+
An entity can be compared with other entities or with a hash of attributes.
|
121
|
+
|
122
|
+
If the entity is compared to a hash, then the `#==` operator will return true if the hash is equal to the entity's attributes.
|
123
|
+
|
124
|
+
book = Book.new(title: 'The Hobbit')
|
125
|
+
book == {} #=> false
|
126
|
+
book == { title: 'The Silmarillion' } #=> false
|
127
|
+
book == { title: 'The Hobbit' } #=> true
|
128
|
+
|
129
|
+
If the entity is compared to another object, then the `#==` operator will return true if and only if the other object has the same class (not a subclass) and the same attributes.
|
130
|
+
|
131
|
+
# Comparing with the same class but different attributes.
|
132
|
+
book == Book.new #=> false
|
133
|
+
|
134
|
+
# Comparing with a different class but the same attributes.
|
135
|
+
book == Periodical.new(title: 'The Hobbit') #=> false
|
136
|
+
|
137
|
+
# Comparing with the same class and attributes.
|
138
|
+
book == Book.new(title: 'The Hobbit') #=> true
|
139
|
+
|
140
|
+
#### #assign_attributes Method
|
141
|
+
|
142
|
+
The `#assign_attributes` method updates the entity with the given attributes. Any attributes that are not in the given hash are unchanged, as are any attributes that are flagged as [read-only](#label-3Aread_only+Option).
|
143
|
+
|
144
|
+
class Book < Bronze::Entity
|
145
|
+
attribute :title, String
|
146
|
+
attribute :subtitle, String
|
147
|
+
attribute :isbn, String, read_only: true
|
148
|
+
end
|
149
|
+
|
150
|
+
book = Book.new(
|
151
|
+
title: 'The Hobbit',
|
152
|
+
subtitle: 'There And Back Again',
|
153
|
+
isbn: '123-4-56-789012-3'
|
154
|
+
)
|
155
|
+
book.assign_attributes(
|
156
|
+
subtitle: 'The Desolation of Smaug',
|
157
|
+
isbn: '098-7-65-432109-8'
|
158
|
+
)
|
159
|
+
|
160
|
+
# The title is unchanged because it was not in the attributes hash.
|
161
|
+
book.title #=> 'The Hobbit'
|
162
|
+
|
163
|
+
# The subtitle is updated.
|
164
|
+
book.subtitle #=> 'The Desolation of Smaug'
|
165
|
+
|
166
|
+
# The ISBN is unchanged because it is read-only.
|
167
|
+
book.isbn #=> '123-4-56-789012-3'
|
168
|
+
|
169
|
+
If the hash includes keys that do not correspond to attributes, it will raise an ArgumentError.
|
170
|
+
|
171
|
+
book.assign_attributes(banned_date: Date.today) #=> raises ArgumentError
|
172
|
+
|
173
|
+
#### #attribute? Method
|
174
|
+
|
175
|
+
The `#attribute?` method tests whether the entity defines the given attribute, which can be either a String or Symbol.
|
176
|
+
|
177
|
+
class Book < Bronze::Entity
|
178
|
+
attribute :title, String
|
179
|
+
end
|
180
|
+
|
181
|
+
book = Book.new
|
182
|
+
|
183
|
+
# With a valid attribute name.
|
184
|
+
book.attribute?('title') #=> true
|
185
|
+
book.attribute?(:title) #=> true
|
186
|
+
|
187
|
+
# With an invalid attribute name.
|
188
|
+
book.attribute?(:banned_date) #=> false
|
189
|
+
|
190
|
+
#### #attributes Method
|
191
|
+
|
192
|
+
The `#attributes` method returns the current values of each defined attribute.
|
193
|
+
|
194
|
+
class Book < Bronze::Entity
|
195
|
+
attribute :title, String
|
196
|
+
end
|
197
|
+
|
198
|
+
book = Book.new
|
199
|
+
book.attributes #=> { title: nil }
|
200
|
+
|
201
|
+
book = Book.new(title: 'The Hobbit')
|
202
|
+
book.attributes #=> { title: 'The Hobbit' }
|
203
|
+
|
204
|
+
#### #attributes= Method
|
205
|
+
|
206
|
+
The `#attributes=` method sets the attributes to the given hash, even if the attribute is flagged as read-only. Any attributes that are not in the hash are set to nil.
|
207
|
+
|
208
|
+
Generally, the `#assign_attributes` method is preferred for updating attributes.
|
209
|
+
|
210
|
+
class Book < Bronze::Entity
|
211
|
+
attribute :title, String
|
212
|
+
attribute :subtitle, String
|
213
|
+
attribute :isbn, String, read_only: true
|
214
|
+
end
|
215
|
+
|
216
|
+
book = Book.new(
|
217
|
+
title: 'The Hobbit',
|
218
|
+
subtitle: 'There And Back Again',
|
219
|
+
isbn: '123-4-56-789012-3'
|
220
|
+
)
|
221
|
+
book.attributes = {
|
222
|
+
subtitle: 'The Desolation of Smaug',
|
223
|
+
isbn: '098-7-65-432109-8'
|
224
|
+
}
|
225
|
+
|
226
|
+
# The title is set to nil because it was not in the attributes hash.
|
227
|
+
book.title #=> nil
|
228
|
+
|
229
|
+
# The subtitle is updated.
|
230
|
+
book.subtitle #=> 'The Desolation of Smaug'
|
231
|
+
|
232
|
+
# The ISBN is updated, even though it is read-only.
|
233
|
+
book.isbn #=> '098-7-65-432109-8'
|
234
|
+
|
235
|
+
If the hash includes keys that do not correspond to attributes, it will raise an ArgumentError.
|
236
|
+
|
237
|
+
book.attributes = { banned_date: Date.today } #=> raises ArgumentError
|
238
|
+
|
239
|
+
#### #get_attribute Method
|
240
|
+
|
241
|
+
The `#get_attribute` method returns the current value of the given attribute, which can be either a String or a Symbol.
|
242
|
+
|
243
|
+
class Book < Bronze::Entity
|
244
|
+
attribute :title, String
|
245
|
+
end
|
246
|
+
|
247
|
+
book = Book.new(title: 'The Hobbit')
|
248
|
+
book.get_attribute('title') => 'The Hobbit'
|
249
|
+
book.get_attribute(:title) => 'The Hobbit'
|
250
|
+
|
251
|
+
If the named attribute is not a valid attribute for the entity, it will raise an ArgumentError.
|
252
|
+
|
253
|
+
book.get_attribute(:banned_date) #=> raises ArgumentError
|
254
|
+
|
255
|
+
#### #set_attribute Method
|
256
|
+
|
257
|
+
The `#set_attribute` method updates the attribute to the given value. The attribute name must be either a String or a Symbol.
|
258
|
+
|
259
|
+
class Book < Bronze::Entity
|
260
|
+
attribute :title, String
|
261
|
+
end
|
262
|
+
|
263
|
+
book = Book.new(title: 'The Hobbit')
|
264
|
+
book.set_attribute(:title, 'The Silmarillion')
|
265
|
+
book.title => 'The Silmarillion'
|
266
|
+
|
267
|
+
If the named attribute is not a valid attribute for the entity, it will raise an ArgumentError.
|
268
|
+
|
269
|
+
book.get_attribute(:banned_date, Date.today) #=> raises ArgumentError
|
270
|
+
|
271
|
+
#### :allow_nil Option
|
272
|
+
|
273
|
+
The `:allow_nil` option marks the attribute as permitting `nil` values. This flag is used in validations.
|
274
|
+
|
275
|
+
class Book
|
276
|
+
attribute :subtitle, String, allow_nil: true
|
277
|
+
end
|
278
|
+
|
279
|
+
metadata = Book.attributes[:subtitle]
|
280
|
+
metadata.allow_nil? #=> true
|
281
|
+
|
282
|
+
#### :default Option
|
283
|
+
|
284
|
+
The `:default` option provides a default value or proc to pre-populate the attribute when creating an entity. Unless this option is used, the initial value of the entity will be `nil`.
|
285
|
+
|
286
|
+
When the default is a value, then new instances of the entity class will pre-populate the attribute with that value.
|
287
|
+
|
288
|
+
class Book
|
289
|
+
attribute :introduction,
|
290
|
+
String,
|
291
|
+
default: 'It was a dark and stormy night.'
|
292
|
+
end
|
293
|
+
|
294
|
+
book = Book.new
|
295
|
+
book.introduction #=> 'It was a dark and stormy night.'
|
296
|
+
|
297
|
+
When the default is a block, then the block will be called each time the entity class is instantiated, setting the attribute to the value returned from the block.
|
298
|
+
|
299
|
+
class Book
|
300
|
+
next_index = 0
|
301
|
+
|
302
|
+
attribute :index, Integer, default: -> { next_index += 1 }
|
303
|
+
end
|
304
|
+
|
305
|
+
book = Book.new
|
306
|
+
book.index #=> 1
|
307
|
+
|
308
|
+
book = Book.new
|
309
|
+
book.index #=> 2
|
310
|
+
|
311
|
+
#### :read_only Option
|
312
|
+
|
313
|
+
The `:read_only` option marks the attribute as being read-only, i.e. written to only once (typically when the entity is initialized). An entity with this flag set will mark the writer method as private, and will not be updated by the `#assign_attributes` or `#set_attribute` methods.
|
314
|
+
|
315
|
+
class Book
|
316
|
+
attribute :isbn, String, read_only: true
|
317
|
+
end
|
318
|
+
|
319
|
+
metadata = Book.attributes[:isbn]
|
320
|
+
metadata.read_only? #=> true
|
321
|
+
|
322
|
+
book = Book.new(isbn: '123-4-56-789012-3')
|
323
|
+
book.isbn #=> '123-4-56-789012-3'
|
324
|
+
|
325
|
+
# Setting the value with a writer method.
|
326
|
+
book.isbn = '098-7-65-432109-8' #=> raises NoMethodError
|
327
|
+
|
328
|
+
# Setting the value with #assign_attributes.
|
329
|
+
book.assign_attributes(isbn: '098-7-65-432109-8')
|
330
|
+
book.isbn #=> '123-4-56-789012-3'
|
331
|
+
|
332
|
+
# Setting the value with #set_attribute.
|
333
|
+
book.set_attribute(:isbn, '098-7-65-432109-8')
|
334
|
+
book.isbn #=> '123-4-56-789012-3'
|
335
|
+
|
336
|
+
#### :transform Option
|
337
|
+
|
338
|
+
The `:transform` option sets the transform used to convert the attribute to and
|
339
|
+
from a normal form. This is used for normalization, e.g. converting the entity to a portable form, and for serializing the entity to and from a data store.
|
340
|
+
|
341
|
+
Most attributes do not require a transform, and are unchanged during normalization/serialization since most data stores will natively support that data type. For select builtin types, there are default transforms defined (see [Attribute Transforms](#label-Attribute+Transforms), below). Finally, the `:transform` option lets you set the transform for the current attribute, whether that is to override an existing default or to support a custom data type.
|
342
|
+
|
343
|
+
class Point < Struct.new(:x, :y)
|
344
|
+
|
345
|
+
class PointTransform < Bronze::Transform
|
346
|
+
def denormalize(coords)
|
347
|
+
Point.new(*Array(coords))
|
348
|
+
end
|
349
|
+
|
350
|
+
def normalize(point)
|
351
|
+
[point.x, point.y]
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
class Map
|
356
|
+
attribute :treasure, Point, transform: PointTransform
|
357
|
+
end
|
358
|
+
|
359
|
+
point = Point.new(3, 4)
|
360
|
+
map = Map.new(point: point)
|
361
|
+
|
362
|
+
##### :default_transform Option
|
363
|
+
|
364
|
+
The `:default_transform` option flags the transform as a default. Default transforms can be skipped when normalizing the entity (see [Normalization](#label-Normalization), below).
|
365
|
+
|
366
|
+
### Normalization
|
367
|
+
|
368
|
+
require 'bronze/entities/normalization'
|
369
|
+
|
370
|
+
An entity class can define normalization helper methods, which convert an entity to a hash of data values and vice versa.
|
371
|
+
|
372
|
+
A normalized value is one of the following:
|
373
|
+
|
374
|
+
- A literal value:
|
375
|
+
- `nil`
|
376
|
+
- `true` or `false`
|
377
|
+
- A `String`
|
378
|
+
- An `Integer`
|
379
|
+
- A `Float`
|
380
|
+
- An `Array` of normalized items
|
381
|
+
- A `Hash` with `String` keys and with normalized values
|
382
|
+
|
383
|
+
Any other values should be converted to a normalized value, e.g. by setting a [:transform option](#label-3Atransform+Option) when defining the attribute.
|
384
|
+
|
385
|
+
#### ::denormalize Class Method
|
386
|
+
|
387
|
+
The `::denormalize` class method converts a normalized hash to an entity instance.
|
388
|
+
|
389
|
+
class Book < Bronze::Entity
|
390
|
+
attribute :title, String
|
391
|
+
attribute :subtitle, String, allow_nil: true
|
392
|
+
attribute :isbn, String, read_only: true
|
393
|
+
end
|
394
|
+
|
395
|
+
attributes = {
|
396
|
+
title: 'Journey To The West',
|
397
|
+
isbn: '123-4-56-789012-3'
|
398
|
+
}
|
399
|
+
book = Book.denormalize(attributes)
|
400
|
+
book.class #=> Book
|
401
|
+
book.title #=> 'Journey To The West'
|
402
|
+
book.subtitle #=> nil
|
403
|
+
book.isbn #=> '123-4-56-789012-3'
|
404
|
+
|
405
|
+
When an attribute has a defined transform (either from an [attribute transform](#label-Attribute+Transforms) or by defining the [:transform option](#label-3Atransform+Option)), then that transform is used when creating the entity from the data hash.
|
406
|
+
|
407
|
+
class Periodical
|
408
|
+
attribute :title, String
|
409
|
+
attribute :issue, Integer
|
410
|
+
attribute :date, DateTime
|
411
|
+
end
|
412
|
+
|
413
|
+
attributes = {
|
414
|
+
title: 'Triskadecaphobia Today',
|
415
|
+
issue: 13,
|
416
|
+
date: '2013-10-03T13:13:13+1300'
|
417
|
+
}
|
418
|
+
periodical = Periodical.denormalize(attributes)
|
419
|
+
periodical.class #=> Periodical
|
420
|
+
periodical.title #=> 'Triskadecaphobia Today'
|
421
|
+
periodical.issue #=> 13
|
422
|
+
periodical.date #=> #<DateTime: 2013-10-03T13:13:13+13:00>
|
423
|
+
|
424
|
+
#### #normalize Method
|
425
|
+
|
426
|
+
The `#normalize` method converts an entity instance to a normalized hash.
|
427
|
+
|
428
|
+
class Book < Bronze::Entity
|
429
|
+
attribute :title, String
|
430
|
+
attribute :subtitle, String, allow_nil: true
|
431
|
+
attribute :isbn, String, read_only: true
|
432
|
+
end
|
433
|
+
|
434
|
+
attributes = {
|
435
|
+
title: 'Journey To The West',
|
436
|
+
isbn: '123-4-56-789012-3'
|
437
|
+
}
|
438
|
+
book = Book.new(attributes)
|
439
|
+
hash = book.normalize
|
440
|
+
hash.class #=> Hash
|
441
|
+
hash['title'] #=> 'Journey To The West'
|
442
|
+
hash['subtitle'] #=> nil
|
443
|
+
hash['isbn'] #=> '123-4-56-789012-3'
|
444
|
+
|
445
|
+
When an attribute has a defined transform (either from an [attribute transform](#label-Attribute+Transforms) or by defining the [:transform option](#label-3Atransform+Option)), then that transform is used when generating the entity from the data hash.
|
446
|
+
|
447
|
+
class Periodical
|
448
|
+
attribute :title, String
|
449
|
+
attribute :issue, Integer
|
450
|
+
attribute :date, DateTime
|
451
|
+
end
|
452
|
+
|
453
|
+
attributes = {
|
454
|
+
title: 'Triskadecaphobia Today',
|
455
|
+
issue: 13,
|
456
|
+
date: DateTime.new(2013, 10, 3, 13, 13, 13, '+13:00')
|
457
|
+
}
|
458
|
+
periodical = Periodical.new(attributes)
|
459
|
+
hash = periodical.normalize
|
460
|
+
hash.class #=> Hash
|
461
|
+
hash['title'] #=> 'Triskadecaphobia Today'
|
462
|
+
hash['issue'] #=> 13
|
463
|
+
hash['date'] #=> '2013-10-03T13:13:13+1300'
|
464
|
+
|
465
|
+
##### :permit Option
|
466
|
+
|
467
|
+
If the `:permit` option is set, then the given class or classes are normalized as-is, rather than by applying a transform. This can be useful when the destination has native support for certain data types, such as an ORM for a SQL database natively converting date and time objects.
|
468
|
+
|
469
|
+
attributes = {
|
470
|
+
title: 'Triskadecaphobia Today',
|
471
|
+
issue: 13,
|
472
|
+
date: DateTime.new(2013, 10, 3, 13, 13, 13, '+13:00')
|
473
|
+
}
|
474
|
+
periodical = Periodical.new(attributes)
|
475
|
+
hash = periodical.normalize(permit: DateTime)
|
476
|
+
hash.class #=> Hash
|
477
|
+
hash['title'] #=> 'Triskadecaphobia Today'
|
478
|
+
hash['issue'] #=> 13
|
479
|
+
hash['date'] #=> #<DateTime: 2013-10-03T13:13:13+13:00>
|
480
|
+
|
481
|
+
Only default transforms can be skipped, i.e. the built-in default transforms for `BigDecimal`, `Date`, `DateTime`, `Symbol`, and `Time`, or any attributes with the `:default_transform` option set.
|
482
|
+
|
483
|
+
### Primary Keys
|
484
|
+
|
485
|
+
require 'bronze/entities/primary_key'
|
486
|
+
|
487
|
+
An entity class can define a primary key attribute, which serves as a unique identifier for each entity. A primary key never allows for `nil` values, is `read-only`, and has additional protections against being overwritten (for example, by the `#attributes=` method).
|
488
|
+
|
489
|
+
Since the primary key is an attribute, defining a primary key requires both the Attributes module and the PrimaryKey module.
|
490
|
+
|
491
|
+
class ThinEntity
|
492
|
+
include Bronze::Entities::Attributes
|
493
|
+
include Bronze::Entities::PrimaryKey
|
494
|
+
end
|
495
|
+
|
496
|
+
Some predefined primary key solutions are available; see below, starting at [PrimaryKeys: UUID](#label-Primary+Keys-3A+UUID).
|
497
|
+
|
498
|
+
#### ::define_primary_key Class Method
|
499
|
+
|
500
|
+
The `::define_primary_key` class method is used to define a primary key for an entity class and its descendants.
|
501
|
+
|
502
|
+
class Book
|
503
|
+
define_primary_key :id, String, default: -> { SecureRandom.uuid }
|
504
|
+
end
|
505
|
+
|
506
|
+
As with defining an attribute, defining a primary key requires the name and object type of the key. In addition, a default block must be provided for generating the primary key. In this case, we are setting the primary key to `:id`, which is a `String` generated by calling `SecureRandom.uuid`. This value is automatically generated when the entity is instantiated, unless an `id` value is explicitly passed into `::new`.
|
507
|
+
|
508
|
+
Internally, this delegates to calling `::attribute`. This means our `#id` accessor is defined for us (but not `#id=`, since the primary key is read-only). Like any other attribute, the primary key will appear in `#attributes`, can be accessed via `#get_attribute`, and we can access the metadata via the `::attributes` class method.
|
509
|
+
|
510
|
+
#### ::primary_key Class Method
|
511
|
+
|
512
|
+
The `::primary_key` class method returns the metadata for our primary key attribute directly, without having to go through `::attributes`. This will return an instance of `Bronze::Entities::Attributes::Metadata` If a primary key is not defined for the entity class, it will return `nil`.
|
513
|
+
|
514
|
+
For example, the following code will return the name of the primary key for our Book class:
|
515
|
+
|
516
|
+
Book.primary_key.name
|
517
|
+
|
518
|
+
#### #primary_key Method
|
519
|
+
|
520
|
+
The `#primary_key` method returns the value of the primary key for the entity. This can be useful when different entities may use different attributes as their primary keys, such as applications using multiple datastores or with legacy data.
|
521
|
+
|
522
|
+
book = Book.new(id: '7c582500-2b33-4b41-bffc-68231c23949a')
|
523
|
+
book.id #=> '7c582500-2b33-4b41-bffc-68231c23949a'
|
524
|
+
book.primary_key #=> '7c582500-2b33-4b41-bffc-68231c23949a'
|
525
|
+
|
526
|
+
### Primary Keys: UUID
|
527
|
+
|
528
|
+
require 'bronze/entities/primary_keys/uuid'
|
529
|
+
|
530
|
+
A common format for primary keys is the UUID, or Universally unique identifier (also known as the GUID). Each UUID is unique for all practical purposes, even distributed across different servers or processes.
|
531
|
+
|
532
|
+
The `PrimaryKeys::Uuid` module simplifies defining a UUID-based primary key by overriding the `::define_primary_key` class method (see below). It can be included in a subclass of `Bronze::Entity`, or directly in any class that includes `Bronze::Entities::Attributes`.
|
533
|
+
|
534
|
+
# Including in a subclass of Bronze::Entity.
|
535
|
+
class Book < Bronze::Entity
|
536
|
+
include Bronze::Entities::PrimaryKeys::Uuid
|
537
|
+
end
|
538
|
+
|
539
|
+
# Including directly in a custom entity class.
|
540
|
+
class Periodical
|
541
|
+
include Bronze::Entities::Attributes
|
542
|
+
include Bronze::Entities::PrimaryKeys::Uuid
|
543
|
+
end
|
544
|
+
|
545
|
+
A UUID is represented in Bronze by its string representation, which looks something like this: `"6891120c-c018-4060-a8b1-22d0278003f8"`. Generation is delegated to the `SecureRandom.uuid` method.
|
546
|
+
|
547
|
+
#### ::define_primary_key Class Method
|
548
|
+
|
549
|
+
The `::define_primary_key` class method is used to define a UUID primary key. Both the object type and the default generation are handled, so all that is required is the name of the primary key.
|
550
|
+
|
551
|
+
class Book < Bronze::Entity
|
552
|
+
include Bronze::Entities::PrimaryKeys::Uuid
|
553
|
+
|
554
|
+
define_primary_key :id
|
555
|
+
end
|
556
|
+
|
557
|
+
## Transforms
|
558
|
+
|
559
|
+
require 'bronze/transform'
|
560
|
+
|
561
|
+
A transform represents a mapping between one type of object to another.
|
562
|
+
|
563
|
+
class Point < Struct.new(:x, :y)
|
564
|
+
|
565
|
+
class PointTransform < Bronze::Transform
|
566
|
+
def denormalize(coords)
|
567
|
+
Point.new(*Array(coords))
|
568
|
+
end
|
569
|
+
|
570
|
+
def normalize(point)
|
571
|
+
[point.x, point.y]
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
point = Point.new(3, 4)
|
576
|
+
transform = PointTransform.new
|
577
|
+
transform.normalize(point)
|
578
|
+
#=> [3, 4]
|
579
|
+
|
580
|
+
point = transform.denormalize([5, 12])
|
581
|
+
point.class
|
582
|
+
#=> Point
|
583
|
+
point.x
|
584
|
+
#=> 5
|
585
|
+
point.y
|
586
|
+
#=> 12
|
587
|
+
|
588
|
+
Transforms can be either mono- or bi-directional.
|
589
|
+
|
590
|
+
class UpcaseTransform < Bronze::Transform
|
591
|
+
# No denormalize method is defined, since this not reversible.
|
592
|
+
|
593
|
+
def normalize(string)
|
594
|
+
string.upcase
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
transform = UpcaseTransform
|
599
|
+
transform.normalize('lower case string')
|
600
|
+
#=> 'LOWER CASE STRING'
|
601
|
+
|
602
|
+
transform.denormalize('UPPER CASE STRING')
|
603
|
+
#=> raises NotImplementedError
|
604
|
+
|
605
|
+
### Methods
|
606
|
+
|
607
|
+
Each transform must define the `#normalize` and `#denormalize` methods. Transforms that inherit from Bronze::Transform will raise a `NotImplementedError` unless those methods are redefined.
|
608
|
+
|
609
|
+
#### ::instance Class Method
|
610
|
+
|
611
|
+
Optional. An `::instance` class method is recommended For transforms that take no arguments and have no internal state. `::instance` should memoize a call to `::new` and return the same transform instance each time it is called to minimize object allocation and memory usage.
|
612
|
+
|
613
|
+
class CaseTransform
|
614
|
+
def self.instance
|
615
|
+
@instance ||= new
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
transform = CaseTransform.instance
|
620
|
+
|
621
|
+
If the transform does have internal state, e.g. stores values with instance variables, an alternative might be to use a thread-local instance.
|
622
|
+
|
623
|
+
#### #denormalize Method
|
624
|
+
|
625
|
+
The `#denormalize` method converts the object or data back from an alternate form. This should be the inverse of the `#normalize` method (see below).
|
626
|
+
|
627
|
+
transform = CaseTransform.new
|
628
|
+
transform.denormalize('lower case string')
|
629
|
+
#=> 'LOWER CASE STRING'
|
630
|
+
|
631
|
+
For one-way transforms, the `#denormalize` method should raise a `NotImplementedError`.
|
632
|
+
|
633
|
+
#### #normalize Method
|
634
|
+
|
635
|
+
The `#normalize` method converts the object or data to an alternate form. By convention, the normalized result is a simpler or standardized form, such as converting an entity to a hash of attributes.
|
636
|
+
|
637
|
+
transform = CaseTransform.new
|
638
|
+
transform.normalize('UPPER CASE STRING')
|
639
|
+
#=> 'upper case string'
|
640
|
+
|
641
|
+
For one-way transforms, use the `#normalize` method to transform the data.
|
642
|
+
|
643
|
+
### Attribute Transforms
|
644
|
+
|
645
|
+
Transforms can be used to serialize and store custom attributes (see [:transform Option](#label-3Atransform+Option), above). Each attribute transform converts the value to an easily-stored form with `#normalize` and restores the original form with `#denormalize`.
|
646
|
+
|
647
|
+
#### BigDecimalTransform
|
648
|
+
|
649
|
+
require 'bronze/transforms/attributes/big_decimal_transform'
|
650
|
+
|
651
|
+
Converts a BigDecimal to a string representation.
|
652
|
+
|
653
|
+
transform = Bronze::Transforms::Attributes::BigDecimalTransform.instance
|
654
|
+
transform.normalize(BigDecimal('3.14'))
|
655
|
+
#=> '3.14'
|
656
|
+
|
657
|
+
decimal = transform.denormalize('3.14')
|
658
|
+
decimal.class
|
659
|
+
#=> BigDecimal
|
660
|
+
decimal.to_f
|
661
|
+
#=> 3.14
|
662
|
+
|
663
|
+
#### DateTimeTransform
|
664
|
+
|
665
|
+
require 'bronze/transforms/attributes/date_time_transform'
|
666
|
+
|
667
|
+
Converts a DateTime to a string representation. By default, uses an ISO 8601 string format.
|
668
|
+
|
669
|
+
transform = Bronze::Transforms::Attributes::DateTimeTransform.instance
|
670
|
+
date_time = DateTime.new(1982, 7, 9, 12, 30, 0)
|
671
|
+
transform.normalize(date)
|
672
|
+
#=> '1982-07-09T12:30:00+0000'
|
673
|
+
|
674
|
+
date_time = transform.denormalize('1982-07-09T12:30:00+0000')
|
675
|
+
date_time.class
|
676
|
+
#=> DateTime
|
677
|
+
date_time.year
|
678
|
+
#=> 1982
|
679
|
+
date_time.hour
|
680
|
+
#=> 12
|
681
|
+
date_time.zone
|
682
|
+
#=> '+00:00'
|
683
|
+
|
684
|
+
By passing an optional format parameter, the transform can serialize to and from an alternate string format. Not all possible formats are guaranteed to work with both `#normalize` and `#denormalize`, however, and custom formats may not ensure that all data from the date is stored in the serialized string.
|
685
|
+
|
686
|
+
format = '%B %-d, %Y at %T'
|
687
|
+
transform = Bronze::Transforms::Attributes::DateTimeTransform.new(format)
|
688
|
+
date_time = DateTime.new(1982, 7, 9, 12, 30, 0)
|
689
|
+
transform.normalize(date_time)
|
690
|
+
#=> 'July 9, 1982 at 12:30:00'
|
691
|
+
|
692
|
+
date_time = transform.denormalize('July 9, 1982 at 12:30:00')
|
693
|
+
date_time.class
|
694
|
+
#=> DateTime
|
695
|
+
date_time.year
|
696
|
+
#=> 1982
|
697
|
+
date_time.hour
|
698
|
+
#=> 12
|
699
|
+
date_time.zone
|
700
|
+
#=> '+00:00'
|
701
|
+
|
702
|
+
#### DateTransform
|
703
|
+
|
704
|
+
require 'bronze/transforms/attributes/date_transform'
|
705
|
+
|
706
|
+
Converts a Date to a string representation. By default, uses an ISO 8601 string format.
|
707
|
+
|
708
|
+
transform = Bronze::Transforms::Attributes::DateTransform.instance
|
709
|
+
date = Date.new(1982, 7, 9)
|
710
|
+
transform.normalize(date)
|
711
|
+
#=> '1982-07-09'
|
712
|
+
|
713
|
+
date = transform.denormalize('1982-07-09')
|
714
|
+
date.class
|
715
|
+
#=> DateTime
|
716
|
+
date.year
|
717
|
+
#=> 1982
|
718
|
+
date.month
|
719
|
+
#=> 7
|
720
|
+
date.day
|
721
|
+
#=> 9
|
722
|
+
|
723
|
+
By passing an optional format parameter, the transform can serialize to and from an alternate string format. Not all possible formats are guaranteed to work with both `#normalize` and `#denormalize`, however, and custom formats may not ensure that all data from the date is stored in the serialized string.
|
724
|
+
|
725
|
+
format = '%B %-d, %Y'
|
726
|
+
transform = Bronze::Transforms::Attributes::DateTransform.new(format)
|
727
|
+
date = Date.new(1982, 7, 9)
|
728
|
+
transform.normalize(date)
|
729
|
+
#=> 'July 9, 1982'
|
730
|
+
|
731
|
+
date = transform.denormalize('July 9, 1982')
|
732
|
+
date.class
|
733
|
+
#=> DateTime
|
734
|
+
date.year
|
735
|
+
#=> 1982
|
736
|
+
date.month
|
737
|
+
#=> 7
|
738
|
+
date.day
|
739
|
+
#=> 9
|
740
|
+
|
741
|
+
#### SymbolTransform
|
742
|
+
|
743
|
+
require 'bronze/transforms/attributes/symbol_transform'
|
744
|
+
|
745
|
+
Converts a Symbol to a string representation.
|
746
|
+
|
747
|
+
transform = Bronze::Transforms::Attributes::SymbolTransform.instance
|
748
|
+
transform.normalize(:symbol_value)
|
749
|
+
#=> 'symbol_value'
|
750
|
+
transform.denormalize('string_value')
|
751
|
+
#=> :string_value
|
752
|
+
|
753
|
+
#### TimeTransform
|
754
|
+
|
755
|
+
require 'bronze/transforms/attributes/time_transform'
|
756
|
+
|
757
|
+
Converts a Time to an integer representation.
|
758
|
+
|
759
|
+
transform = Bronze::Transforms::Attributes::SymbolTransform.instance
|
760
|
+
time = Time.new(1982, 7, 9)
|
761
|
+
transform.normalize(time)
|
762
|
+
#=> 395035200
|
763
|
+
|
764
|
+
time = transform.denormalize(395035200)
|
765
|
+
time.class
|
766
|
+
#=> Time
|
767
|
+
time.year
|
768
|
+
#=> 1982
|
769
|
+
time.hour
|
770
|
+
#=> 0
|
771
|
+
|
772
|
+
### Entity Transforms
|
16
773
|
|
17
|
-
|
774
|
+
The following transforms can be used to serialize or map entity objects.
|
18
775
|
|
19
|
-
|
776
|
+
#### Normalize Transform
|
20
777
|
|
21
|
-
|
778
|
+
require 'bronze/transforms/entities/normalize_transform'
|
22
779
|
|
23
|
-
|
780
|
+
Converts an entity to a normal representation and vice versa. See [Normalization](#label-Normalization), above.
|
24
781
|
|
25
|
-
|
782
|
+
class Periodical
|
783
|
+
attribute :title, String
|
784
|
+
attribute :issue, Integer
|
785
|
+
attribute :date, DateTime
|
786
|
+
end
|
787
|
+
attributes = {
|
788
|
+
title: 'Triskadecaphobia Today',
|
789
|
+
issue: 13,
|
790
|
+
date: DateTime.new(2013, 10, 3, 13, 13, 13, '+13:00')
|
791
|
+
}
|
792
|
+
transform =
|
793
|
+
Bronze::Transforms::Entities::NormalizeTransform.new(Periodical)
|
794
|
+
periodical = Periodical.new(attributes)
|
795
|
+
hash = transform.normalize(periodical)
|
796
|
+
hash.class #=> Hash
|
797
|
+
hash['title'] #=> 'Triskadecaphobia Today'
|
798
|
+
hash['issue'] #=> 13
|
799
|
+
hash['date'] #=> '2013-10-03T13:13:13+1300'
|
26
800
|
|
27
|
-
|
801
|
+
periodical = transform.denormalize(attributes)
|
802
|
+
#=> an instance of Periodical
|
803
|
+
periodical.title #=> 'Triskadecaphobia Today'
|
804
|
+
periodical.issue #=> 13
|
805
|
+
periodical.date #=> #<DateTime: 2013-10-03T13:13:13+13:00>
|
28
806
|
|
29
|
-
|
807
|
+
The constructor also accepts an array of permitted types.
|
30
808
|
|
31
|
-
|
809
|
+
transform =
|
810
|
+
Bronze::Transforms::Entities::NormalizeTransform.new(
|
811
|
+
Periodical,
|
812
|
+
permit: [DateTime]
|
813
|
+
)
|
814
|
+
hash = transform.normalize(periodical)
|
815
|
+
hash.class #=> Hash
|
816
|
+
hash['title'] #=> 'Triskadecaphobia Today'
|
817
|
+
hash['issue'] #=> 13
|
818
|
+
hash['date'] #=> #<DateTime: 2013-10-03T13:13:13+13:00>
|