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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1fb4521e173664e74c80c904cccdd3abdb8f570b
4
- data.tar.gz: 57f90ee2911cbb36e8be5e608a66c0f31f4061d5
2
+ SHA256:
3
+ metadata.gz: 50254e8fbcd0578e935770b70b65de1d39f80c3fdbd071203054d3acf802bf7d
4
+ data.tar.gz: 468d03f80901e6b245dd27b35ad7af5dcbb9ca006c165244a067ddbdec5a5df2
5
5
  SHA512:
6
- metadata.gz: d4599f4a39dfe803be03ba974e61c176b92120f0563451d01998411731615ae5aea4abe01f2cae50d4781b75792059758504041c7b862f0dc12bcf5e7b9cfe04
7
- data.tar.gz: 08aa472e88d060ba1837a57baefbe4f9df5852f551b2d1f54198637ff8049d0d72d5b4a7a56fe29c4fece0f0774ac29cf8b7c88e910e5d72cbb5bb1d8e06db3b
6
+ metadata.gz: dd6faa02f627b9d585e3229f3bcff896a7c7262cff2d0ae3503b3a04561e5532103dbec681ee5ce4f52b9ea989f8e8a63dac736de2b4d44ad29722d44f74907d
7
+ data.tar.gz: c332d23085bd7b381431c2ea887a78f4292c9e801939459a59b6a2113aeba197c51fdb540ee7d2b2cfeb1c4b8c765de81eee142e9614e4b97a9fb3b516efd919
@@ -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.
@@ -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 set of objects and tools for building composable applications without the conceptual overhead of common frameworks. Allows the developer to incorporate other architectures by composition without forcing foreign constraints on your business logic.
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
- ## Support
5
+ Bronze defines the following components:
6
6
 
7
- Bronze and Patina are tested against Ruby 2.3.
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
- ## Contribute
10
+ ## About
10
11
 
11
- ### GitHub
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
- ### A Note From The Developer
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
- Hi, I'm Rob Smith, a Ruby Engineer and the developer of this library. I use these tools every day, but they're not just written for me. If you find this project helpful in your own work, or if you have any questions, suggestions or critiques, please feel free to get in touch! I can be reached on GitHub (see above, and feel encouraged to submit bug reports or merge requests there) or via email at merlin@sleepingkingstudios.com. I look forward to hearing from you!
774
+ The following transforms can be used to serialize or map entity objects.
18
775
 
19
- ### Why Bronze and Patina?
776
+ #### Normalize Transform
20
777
 
21
- Mainly symbolism. In metallurgy, bronze is an alloy composed chiefly of copper and tin and known since antiquity. The Bronze gem incorporates influences from different sources, and it is combination of ingredients that (hopefully) form a harmonious whole. As such, an alloy (a mixture of different metals and elements) is metaphorically apt. Copper alloys including bronze and brass have been used since antiquity in projects large and small, from arrowheads and chariots to great wonders such as the Colossus of Rhodes and the Statue of Liberty.
778
+ require 'bronze/transforms/entities/normalize_transform'
22
779
 
23
- A patina is the surface layer of aged copper that gives the metal its distinctive green coloration, as well protection from corrosion and weathering. The Patina gem provides a finishing layer of additional functionality and features to developers using the Bronze framework.
780
+ Converts an entity to a normal representation and vice versa. See [Normalization](#label-Normalization), above.
24
781
 
25
- ## Bronze Features
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
- The Bronze gem provides the core tools and objects for building an application. Wherever possible, these tools can be used on their own as part of another framework or project - use as much or as little as you want.
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
- ## Patina Features
807
+ The constructor also accepts an array of permitted types.
30
808
 
31
- The optional Patina gem provides extensions and concrete implementations of the Bronze application tools and patterns.
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>