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.
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bronze/entities'
4
+
5
+ module Bronze::Entities
6
+ # Namespace for primary key implementations.
7
+ module PrimaryKeys; end
8
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bronze'
4
+
5
+ module Bronze
6
+ # Namespace for defining transform objects, which that map a data object into
7
+ # another representation of that data.
8
+ module Transforms; end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bronze/transforms'
4
+
5
+ module Bronze::Transforms
6
+ # Namespace for defining attribute transforms, which convert data objects to
7
+ # normalized forms.
8
+ module Attributes; end
9
+ 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