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.
@@ -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