bronze 0.0.1.alpha → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|