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
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bronze/entities'
|
4
|
+
|
5
|
+
module Bronze::Entities::Attributes
|
6
|
+
# Data class that characterizes an entity attribute and allows for reflection
|
7
|
+
# on its properties and options.
|
8
|
+
class Metadata
|
9
|
+
# @param name [String, Symbol] The name of the attribute.
|
10
|
+
# @param type [Class] The type of the attribute.
|
11
|
+
# @param options [Hash] Additional options for the attribute.
|
12
|
+
def initialize(name, type, options)
|
13
|
+
@name = name.intern
|
14
|
+
@type = type
|
15
|
+
@options = options
|
16
|
+
@reader_name = name.intern
|
17
|
+
@writer_name = "#{name}=".intern
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String, Symbol] the name of the attribute.
|
21
|
+
attr_reader :name
|
22
|
+
|
23
|
+
# @return [Hash] additional options for the attribute.
|
24
|
+
attr_reader :options
|
25
|
+
|
26
|
+
# @return [String, Symbol] the name of the attribute's reader method.
|
27
|
+
attr_reader :reader_name
|
28
|
+
|
29
|
+
# @return [Class] the type of the attribute.
|
30
|
+
attr_reader :type
|
31
|
+
|
32
|
+
# @return [String, Symbol] the name of the attribute's writer method.
|
33
|
+
attr_reader :writer_name
|
34
|
+
|
35
|
+
# @return [Boolean] true if the attribute allows nil values, otherwise
|
36
|
+
# false.
|
37
|
+
def allow_nil?
|
38
|
+
!!@options[:allow_nil]
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Object] the default value for the attribute.
|
42
|
+
def default
|
43
|
+
val = @options[:default]
|
44
|
+
|
45
|
+
val.is_a?(Proc) ? val.call : val
|
46
|
+
end
|
47
|
+
alias_method :default_value, :default
|
48
|
+
|
49
|
+
# @return [Boolean] true if the default value is set, otherwise false.
|
50
|
+
def default?
|
51
|
+
!@options[:default].nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Boolean] true if the attribute does not have a custom transform,
|
55
|
+
# or if the transform is flagged as a default transform; otherwise false.
|
56
|
+
def default_transform?
|
57
|
+
!!@options[:default_transform] || !transform?
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Boolean] true if the attribute is a foreign key, otherwise false.
|
61
|
+
def foreign_key?
|
62
|
+
!!@options[:foreign_key]
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Boolean] true if the attribute is a primary key, otherwise false.
|
66
|
+
def primary_key?
|
67
|
+
!!@options[:primary_key]
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Boolean] true if the attribute is read-only, otherwise false.
|
71
|
+
def read_only?
|
72
|
+
!!@options[:read_only]
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Bronze::Transform] the transform used to normalize and
|
76
|
+
# denormalize the attribute.
|
77
|
+
def transform
|
78
|
+
@options[:transform]
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [Boolean] true if the attribute has a custom transform, otherwise
|
82
|
+
# false.
|
83
|
+
def transform?
|
84
|
+
!!@options[:transform]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sleeping_king_studios/tools/toolbox/mixin'
|
4
|
+
|
5
|
+
require 'bronze/entities'
|
6
|
+
|
7
|
+
module Bronze::Entities
|
8
|
+
# Module for transforming entities to and from a normal form.
|
9
|
+
module Normalization
|
10
|
+
extend SleepingKingStudios::Tools::Toolbox::Mixin
|
11
|
+
|
12
|
+
# Class methods to define when including Normalization in a class.
|
13
|
+
module ClassMethods
|
14
|
+
# Returns an entity instance from the given normalized representation.
|
15
|
+
#
|
16
|
+
# @param attributes [Hash] A hash with String keys and normal values.
|
17
|
+
#
|
18
|
+
# @return [Bronze::Entity] The entity.
|
19
|
+
def denormalize(attributes)
|
20
|
+
entity = new
|
21
|
+
|
22
|
+
entity.send(:validate_attributes, attributes)
|
23
|
+
|
24
|
+
each_attribute do |name, metadata|
|
25
|
+
value = attributes[name] || attributes[name.to_s]
|
26
|
+
value = metadata.transform.denormalize(value) if metadata.transform?
|
27
|
+
|
28
|
+
next if value.nil? && metadata.primary_key?
|
29
|
+
|
30
|
+
entity.set_attribute(name, value)
|
31
|
+
end
|
32
|
+
|
33
|
+
entity
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a normalized representation of the entity. The normal
|
38
|
+
# representation of an entity is a hash with String keys. Each value must be
|
39
|
+
# nil, a literal value (true, false, a String, an Integer, a Float, etc), an
|
40
|
+
# Array of normal values, or a Hash with String keys and normal values.
|
41
|
+
#
|
42
|
+
# @param [Array<Class>] permit An optional array of types to normalize
|
43
|
+
# as-is, rather than applying a transform. Only default transforms can be
|
44
|
+
# permitted, i.e. the built-in default transforms for BigDecimal, Date,
|
45
|
+
# DateTime, Symbol, and Time, or for an attribute with the
|
46
|
+
# :default_transform flag set to true.
|
47
|
+
#
|
48
|
+
# @return [Hash] The normal representation.
|
49
|
+
def normalize(permit: [])
|
50
|
+
self.class.each_attribute.with_object({}) do |(name, metadata), hsh|
|
51
|
+
value = get_attribute(name)
|
52
|
+
value = normalize_attribute(value, metadata: metadata, permit: permit)
|
53
|
+
|
54
|
+
hsh[name.to_s] = value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def normalize_attribute(value, metadata:, permit:)
|
61
|
+
return value unless metadata.transform?
|
62
|
+
|
63
|
+
return value if metadata.default_transform? &&
|
64
|
+
Array(permit).any? { |type| metadata.type <= type }
|
65
|
+
|
66
|
+
metadata.transform.normalize(value)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sleeping_king_studios/tools/toolbox/mixin'
|
4
|
+
|
5
|
+
require 'bronze/entities'
|
6
|
+
|
7
|
+
module Bronze::Entities
|
8
|
+
# Module for defining a primary key attribute on an entity class.
|
9
|
+
module PrimaryKey
|
10
|
+
extend SleepingKingStudios::Tools::Toolbox::Mixin
|
11
|
+
|
12
|
+
# Class methods to define when including PrimaryKey in a class.
|
13
|
+
module ClassMethods
|
14
|
+
# Defines the primary key with the specified name and type.
|
15
|
+
#
|
16
|
+
# @example Defining a Primary Key
|
17
|
+
# class Book
|
18
|
+
# include Bronze::Entities::Attributes
|
19
|
+
# include Bronze::Entities::PrimaryKey
|
20
|
+
#
|
21
|
+
# next_id = -1
|
22
|
+
# define_primary_key :id, Integer, default: -> { next_id += 1 }
|
23
|
+
# end # class
|
24
|
+
#
|
25
|
+
# book = Book.new
|
26
|
+
# book.id
|
27
|
+
# #=> 0
|
28
|
+
#
|
29
|
+
# Book.new.id
|
30
|
+
# #=> 1
|
31
|
+
#
|
32
|
+
# @param attribute_name [Symbol, String] The name of the primary key.
|
33
|
+
# @param attribute_type [Class] The type of the primary key.
|
34
|
+
# @param default [Proc] The proc to call when generating a new primary
|
35
|
+
# key.
|
36
|
+
#
|
37
|
+
# @return [Attributes::Metadata] the metadata for the primary key
|
38
|
+
# attribute.
|
39
|
+
def define_primary_key(attribute_name, attribute_type, default:)
|
40
|
+
@primary_key =
|
41
|
+
attribute(
|
42
|
+
attribute_name,
|
43
|
+
attribute_type,
|
44
|
+
default: default,
|
45
|
+
primary_key: true,
|
46
|
+
read_only: true
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Attributes::Metadata] the metadata for the primary key
|
51
|
+
# attribute, or nil if the primary key is not defined.
|
52
|
+
def primary_key
|
53
|
+
return @primary_key if @primary_key
|
54
|
+
|
55
|
+
return superclass.primary_key if superclass.respond_to?(:primary_key)
|
56
|
+
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Object] the primary key for the current entity.
|
62
|
+
def primary_key
|
63
|
+
attribute_name = self.class.primary_key&.name
|
64
|
+
|
65
|
+
return nil unless attribute_name
|
66
|
+
|
67
|
+
get_attribute(attribute_name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
require 'sleeping_king_studios/tools/toolbox/mixin'
|
6
|
+
|
7
|
+
require 'bronze/entities/primary_key'
|
8
|
+
require 'bronze/entities/primary_keys'
|
9
|
+
|
10
|
+
module Bronze::Entities::PrimaryKeys
|
11
|
+
# Module for defining a UUID primary key attribute on an entity class.
|
12
|
+
module Uuid
|
13
|
+
extend SleepingKingStudios::Tools::Toolbox::Mixin
|
14
|
+
include Bronze::Entities::PrimaryKey
|
15
|
+
|
16
|
+
# Class methods to define when including PrimaryKeys::Uuid in a class.
|
17
|
+
module ClassMethods
|
18
|
+
# Defines a UUID primary key with the specified name.
|
19
|
+
#
|
20
|
+
# @example Defining a Primary Key
|
21
|
+
# class Book
|
22
|
+
# include Bronze::Entities::Attributes
|
23
|
+
# include Bronze::Entities::PrimaryKey::Uuid
|
24
|
+
#
|
25
|
+
# define_primary_key :id
|
26
|
+
# end # class
|
27
|
+
#
|
28
|
+
# book = Book.new
|
29
|
+
# book.id
|
30
|
+
# #=> '19eeac71-2b8b-439a-8f5d-cb63f26e4ddf'
|
31
|
+
#
|
32
|
+
# Book.new.id
|
33
|
+
# #=> '4c08d721-8aa2-4ff9-942f-852b5c33bcc9'
|
34
|
+
#
|
35
|
+
# @param attribute_name [Symbol, String] The name of the primary key.
|
36
|
+
#
|
37
|
+
# @return [Attributes::Metadata] the metadata for the primary key
|
38
|
+
# attribute.
|
39
|
+
def define_primary_key(attribute_name)
|
40
|
+
super(attribute_name, String, default: -> { SecureRandom.uuid })
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bronze/entities/attributes'
|
4
|
+
require 'bronze/entities/normalization'
|
5
|
+
require 'bronze/entities/primary_key'
|
6
|
+
|
7
|
+
module Bronze
|
8
|
+
# Base class for implementing data entities.
|
9
|
+
class Entity
|
10
|
+
include Bronze::Entities::Attributes
|
11
|
+
include Bronze::Entities::Normalization
|
12
|
+
include Bronze::Entities::PrimaryKey
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bronze'
|
4
|
+
|
5
|
+
module Bronze
|
6
|
+
# Exception subclass to indicate an intended method has not been implemented
|
7
|
+
# on the receiver.
|
8
|
+
class NotImplementedError < StandardError
|
9
|
+
# @param receiver [Object] The object receiving the message.
|
10
|
+
# @param method_name [String] The name of the expected method.
|
11
|
+
def initialize(receiver, method_name)
|
12
|
+
receiver_message =
|
13
|
+
receiver.is_a?(Module) ? "#{receiver}." : "#{receiver.class}#"
|
14
|
+
|
15
|
+
super("#{receiver_message}#{method_name} is not implemented")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bronze/not_implemented_error'
|
4
|
+
require 'bronze/transforms'
|
5
|
+
|
6
|
+
module Bronze
|
7
|
+
# Abstract class for converting an object to and from a normalized form. This
|
8
|
+
# can be a hash for database serialization, an active model object, another
|
9
|
+
# object, or any other transformation.
|
10
|
+
class Transform
|
11
|
+
# Converts an object from its normalized form.
|
12
|
+
#
|
13
|
+
# @param _object [Object] The object to convert.
|
14
|
+
#
|
15
|
+
# @return [Object] The converted object.
|
16
|
+
def denormalize(_object)
|
17
|
+
raise Bronze::NotImplementedError.new(self, :denormalize)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Converts an object to its normalized form.
|
21
|
+
#
|
22
|
+
# @param _object [Object] The entity to convert.
|
23
|
+
#
|
24
|
+
# @return [Object] The converted object.
|
25
|
+
def normalize(_object)
|
26
|
+
raise Bronze::NotImplementedError.new(self, :normalize)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
require 'bronze/transform'
|
6
|
+
require 'bronze/transforms/attributes'
|
7
|
+
|
8
|
+
module Bronze::Transforms::Attributes
|
9
|
+
# Transform class that normalizes a BigDecimal to a string representation.
|
10
|
+
class BigDecimalTransform < Bronze::Transform
|
11
|
+
# @return [BigDecimalTransform] a memoized instance of BigDecimalTranform.
|
12
|
+
def self.instance
|
13
|
+
@instance ||= new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Converts a normalized BigDecimal (a String) to a BigDecimal instance.
|
17
|
+
#
|
18
|
+
# @param value [String] The normalized string.
|
19
|
+
#
|
20
|
+
# @return [BigDecimal] the denormalized instance.
|
21
|
+
def denormalize(value)
|
22
|
+
return nil if value.nil?
|
23
|
+
|
24
|
+
BigDecimal(value)
|
25
|
+
rescue ArgumentError
|
26
|
+
BigDecimal('0.0')
|
27
|
+
end
|
28
|
+
|
29
|
+
# Converts a BigDecimal to a string representation.
|
30
|
+
#
|
31
|
+
# @param value [BigDecimal] The BigDecimal to normalize.
|
32
|
+
#
|
33
|
+
# @return [String] the string representation.
|
34
|
+
def normalize(value)
|
35
|
+
return nil if value.nil?
|
36
|
+
|
37
|
+
value.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
require 'bronze/transform'
|
6
|
+
require 'bronze/transforms/attributes'
|
7
|
+
|
8
|
+
module Bronze::Transforms::Attributes
|
9
|
+
# Transform class that normalizes a DateTime to a formatted string
|
10
|
+
# representation.
|
11
|
+
class DateTimeTransform < Bronze::Transform
|
12
|
+
# Format string for ISO 8601 date+time format. Equivalent to
|
13
|
+
# YYYY-MM-DDTHH:MM:SS+ZZZZ.
|
14
|
+
ISO_8601 = '%FT%T%z'
|
15
|
+
|
16
|
+
# @return [DateTimeTransform] a memoized instance of DateTimeTransform.
|
17
|
+
def self.instance
|
18
|
+
@instance ||= new
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param format [String] The format string used to normalize and denormalize
|
22
|
+
# date times. The default is ISO 8601 format.
|
23
|
+
def initialize(format = ISO_8601)
|
24
|
+
@format = format
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String] the format string.
|
28
|
+
attr_reader :format
|
29
|
+
|
30
|
+
# Converts a formatted DateTime string to a Date instance.
|
31
|
+
#
|
32
|
+
# @param value [String] The normalized string.
|
33
|
+
#
|
34
|
+
# @return [DateTime] the parsed date+time.
|
35
|
+
def denormalize(value)
|
36
|
+
return value if value.is_a?(DateTime)
|
37
|
+
|
38
|
+
return nil if value.nil? || value.empty?
|
39
|
+
|
40
|
+
DateTime.strptime(value, read_format)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Converts a DateTime to a formatted string.
|
44
|
+
#
|
45
|
+
# @param value [DateTime] The DateTime to format.
|
46
|
+
#
|
47
|
+
# @return [String] the formatted string.
|
48
|
+
def normalize(value)
|
49
|
+
return nil if value.nil?
|
50
|
+
|
51
|
+
value.strftime(format)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def read_format
|
57
|
+
@read_format ||= format.gsub('%-', '%')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|