holotype 0.14.0 → 0.15.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cec00eba6093334124b9af2074359ab2d19ea7bf
4
- data.tar.gz: ffc97f6ca90bf16613f6567b0aa1fd911cde8683
3
+ metadata.gz: c8de676ea732eeaafa954422a48c138e2e0fb6a8
4
+ data.tar.gz: d52556331331d5b80e9d79635895f1364e4115a4
5
5
  SHA512:
6
- metadata.gz: 159ce5c4894bc7087802094a00cfdfadebf88c6c7d1a3eb939a572c37f84891b81d291567545cf5f71dfe04786a17ead18c0315d6a53a4bd3789eb9c26161174
7
- data.tar.gz: b1c8b238d0c85592928dbe487d62b141163fd8abd57ade5d11a987ff79da7ae08faeb7bcfe87814e5f9cd26de12d8d42b3c13c79acd4806104a30681e1d6b478
6
+ metadata.gz: d615c9697c7d70431b710fe7882f03867f2949503ecb85fc6465b2e0051ce85b91b886ea3de9a30cc4f3382ca5e9302e97094ecb319f768a18da01d6687364fe
7
+ data.tar.gz: 17e20c875c8fd863a3536753228a258aed88b0556120a1af750c5b8b2066507fa182f8cb6a6830fbc174dac7f52e62a81b90ee74d1a64a1d9a997f2283a40807
@@ -0,0 +1,14 @@
1
+ class Holotype
2
+ class Attribute
3
+ class Definition
4
+ class DefaultConflictError < StandardError
5
+ def message; MESSAGE; end
6
+
7
+ private
8
+
9
+ MESSAGE = 'Attribute definitions cannot have both a default value ' \
10
+ 'and a default block'.freeze
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ class Holotype
2
+ class Attribute
3
+ class Definition
4
+ class NoCollectionClassError < StandardError
5
+ attr_reader :definition
6
+
7
+ def initialize definition
8
+ @definition = definition
9
+ end
10
+
11
+ def message
12
+ "No collection class for attribute definition: #{definition.name}"
13
+ .freeze
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ class Holotype
2
+ class Attribute
3
+ class Definition
4
+ class NoValueClassError < StandardError
5
+ attr_reader :definition
6
+
7
+ def initialize definition
8
+ @definition = definition
9
+ end
10
+
11
+ def message
12
+ "No value class for attribute definition: #{definition.name}".freeze
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ class Holotype
2
+ class Attribute
3
+ class Definition
4
+ class RequiredConflictError < StandardError
5
+ def message; MESSAGE; end
6
+
7
+ private
8
+
9
+ MESSAGE = 'Attribute definitions cannot both be required and provide ' \
10
+ 'a default'.freeze
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,123 @@
1
+ %w[
2
+ default_conflict_error
3
+ no_collection_class_error
4
+ no_value_class_error
5
+ required_conflict_error
6
+ ].each { |file| require_relative "definition/#{file}" }
7
+
8
+ require_relative 'definition/default_conflict_error.rb'
9
+
10
+ class Holotype
11
+ class Attribute
12
+ class Definition
13
+ attr_reader :name
14
+
15
+ def initialize name, **options, &default_block
16
+ @collection = options.fetch :collection, false
17
+ @immutable = options.fetch :immutable, false
18
+ @name = name
19
+ @read_only = options.fetch :read_only, false
20
+ @required = options.fetch :required, false
21
+
22
+ if options.key? :collection_class
23
+ @collection = true
24
+ @has_collection_class = true
25
+ @collection_class = options[:collection_class]
26
+ else
27
+ if collection?
28
+ @has_collection_class = true
29
+ @collection_class = Array
30
+ else
31
+ @has_collection_class = false
32
+ end
33
+ end
34
+
35
+ if options.key? :value_class
36
+ @has_value_class = true
37
+ @value_class = options[:value_class]
38
+ else
39
+ @has_value_class = false
40
+ end
41
+
42
+ if default_block
43
+ raise DefaultConflictError.new if options.key? :default
44
+ raise RequiredConflictError.new if @required
45
+
46
+ @default = default_block
47
+ @default_type = :dynamic
48
+ elsif options.key? :default
49
+ raise RequiredConflictError.new if @required
50
+
51
+ @default = options[:default]
52
+ @default_type = :constant
53
+ end
54
+ end
55
+
56
+ def default receiver
57
+ case @default_type
58
+ when :constant then @default
59
+ when :dynamic then receiver.instance_exec(&@default).freeze
60
+ else nil
61
+ end
62
+ end
63
+
64
+ def normalize value
65
+ if collection?
66
+ normalize_collection value
67
+ else
68
+ normalize_single value
69
+ end
70
+ end
71
+
72
+ def required?
73
+ !!@required
74
+ end
75
+
76
+ def has_value_class?
77
+ @has_value_class
78
+ end
79
+
80
+ def value_class
81
+ raise NoValueClassError.new self unless has_value_class?
82
+ @value_class
83
+ end
84
+
85
+ def collection?
86
+ !!@collection
87
+ end
88
+
89
+ def has_collection_class?
90
+ @has_collection_class
91
+ end
92
+
93
+ def collection_class
94
+ return @collection_class if has_collection_class?
95
+ raise NoCollectionClassError.new self
96
+ end
97
+
98
+ def read_only?
99
+ !!@read_only
100
+ end
101
+
102
+ def immutable?
103
+ !!@immutable
104
+ end
105
+
106
+ private
107
+
108
+ def normalize_single value
109
+ ValueNormalizer
110
+ .new(self)
111
+ .normalize value
112
+ end
113
+
114
+ def normalize_collection values
115
+ CollectionNormalizer.new(self).normalize values
116
+ end
117
+
118
+ def symbolize_keys hash
119
+ Hash[hash.map { |key, value| [key.to_sym, value] }]
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,15 @@
1
+ class Holotype
2
+ class Attribute
3
+ class FrozenModificationError < StandardError
4
+ attr_reader :attribute_name
5
+
6
+ def initialize attribute_name
7
+ @attribute_name = attribute_name.freeze
8
+ end
9
+
10
+ def message
11
+ "Cannot modify value of `#{attribute_name}` in frozen object".freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class Holotype
2
+ class Attribute
3
+ class ImmutableValueError < StandardError
4
+ attr_reader :name
5
+
6
+ def initialize name
7
+ @name = name.freeze
8
+ end
9
+
10
+ def message
11
+ "Cannot modify value of `#{name}` in immutable class".freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class Holotype
2
+ class Attribute
3
+ class ReadOnlyError < StandardError
4
+ attr_reader :name
5
+
6
+ def initialize name
7
+ @name = name.freeze
8
+ end
9
+
10
+ def message
11
+ "Cannot modify read-only attribute `#{name}`".freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ %w[
2
+ definition
3
+ frozen_modification_error
4
+ immutable_value_error
5
+ read_only_error
6
+ ].each { |file| require_relative "attribute/#{file}" }
7
+
8
+ class Holotype
9
+ class Attribute
10
+ attr_reader :definition, :owner
11
+
12
+ def initialize owner, definition, **options
13
+ @definition = definition
14
+ @owner = owner
15
+
16
+ set_value options[:value] if options.key? :value
17
+ end
18
+
19
+ def name
20
+ definition.name
21
+ end
22
+
23
+ def value
24
+ set_value definition.default owner unless @has_value
25
+ @value
26
+ end
27
+
28
+ def value= new_value
29
+ raise ImmutableValueError.new name if definition.immutable?
30
+ raise FrozenModificationError.new name if owner.frozen?
31
+ raise ReadOnlyError.new name if definition.read_only?
32
+
33
+ set_value new_value
34
+ end
35
+
36
+ private
37
+
38
+ def set_value new_value
39
+ @has_value = true
40
+ @value = definition.normalize new_value
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ class Holotype
2
+ class AttributesAlreadyDefinedError < StandardError
3
+ def message; MESSAGE; end
4
+
5
+ private
6
+
7
+ MESSAGE = 'Cannot make class immutable afer attributes are defined'.freeze
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ class Holotype
2
+ class CollectionNormalizer
3
+ class ExpectedArrayLikeCollectionError < StandardError
4
+ attr_reader :attribute
5
+
6
+ def initialize attribute
7
+ @attribute = attribute
8
+ end
9
+
10
+ def message
11
+ "Attribute `#{attribute}` expected Array-like collection, received " \
12
+ "Hash-like collection".freeze
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ class Holotype
2
+ class CollectionNormalizer
3
+ class ExpectedHashLikeCollectionError < StandardError
4
+ attr_reader :attribute
5
+
6
+ def initialize attribute
7
+ @attribute = attribute
8
+ end
9
+
10
+ def message
11
+ "Attribute `#{attribute}` expected Hash-like collection, received " \
12
+ "Array-like collection".freeze
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,155 @@
1
+ %w[
2
+ expected_array_like_collection_error
3
+ expected_hash_like_collection_error
4
+ ].each { |file| require_relative "collection_normalizer/#{file}" }
5
+
6
+ class Holotype
7
+ class CollectionNormalizer
8
+ extend Memorandum
9
+
10
+ ARRAY_LIKE_METHODS = %i[
11
+ []
12
+ []=
13
+ ].freeze
14
+
15
+ HASH_LIKE_METHODS = %i[
16
+ []
17
+ []=
18
+ keys
19
+ values
20
+ ]
21
+
22
+ attr_reader :definition
23
+
24
+ def initialize definition
25
+ @definition = definition
26
+ end
27
+
28
+ def normalize collection
29
+ check_likeness_of collection
30
+
31
+ result = classify normalized collection
32
+
33
+ if definition.immutable?
34
+ result.freeze
35
+ else
36
+ result
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ memo def value_normalizer
43
+ ValueNormalizer.new definition
44
+ end
45
+
46
+ def check_likeness_of collection
47
+ if hash_like?
48
+ raise ExpectedHashLikeCollectionError.new definition.name \
49
+ unless collection.nil? || object_is_hash_like?(collection)
50
+ elsif array_like?
51
+ raise ExpectedArrayLikeCollectionError.new definition.name \
52
+ unless collection.nil? || object_is_array_like?(collection)
53
+ end
54
+ end
55
+
56
+ def hash_like?
57
+ return false unless definition.has_collection_class?
58
+
59
+ class_is_hash_like? definition.collection_class
60
+ end
61
+
62
+ def array_like?
63
+ return false unless definition.has_collection_class?
64
+ return false if hash_like?
65
+
66
+ class_is_array_like? definition.collection_class
67
+ end
68
+
69
+ def class_is_array_like? klass
70
+ return false if class_is_hash_like? klass
71
+
72
+ instance_methods = klass.instance_methods
73
+
74
+ ARRAY_LIKE_METHODS.all? do |method|
75
+ instance_methods.include? method
76
+ end
77
+ end
78
+
79
+ def class_is_hash_like? klass
80
+ instance_methods = klass.instance_methods
81
+
82
+ HASH_LIKE_METHODS.all? do |method|
83
+ instance_methods.include? method
84
+ end
85
+ end
86
+
87
+ def object_is_array_like? object
88
+ return false if object_is_hash_like? object
89
+
90
+ ARRAY_LIKE_METHODS.all? do |method|
91
+ object.respond_to? method
92
+ end
93
+ end
94
+
95
+ def object_is_hash_like? object
96
+ HASH_LIKE_METHODS.all? do |method|
97
+ object.respond_to? method
98
+ end
99
+ end
100
+
101
+ def hash_like collection
102
+ return collection unless definition.has_collection_class?
103
+
104
+ definition
105
+ .collection_class
106
+ .new
107
+ .tap do |result|
108
+ collection.each { |key, value| result[key] = value }
109
+ end
110
+ end
111
+
112
+ def array_like collection
113
+ return collection unless definition.has_collection_class?
114
+
115
+ definition
116
+ .collection_class
117
+ .new
118
+ .tap do |result|
119
+ collection.each { |value| result << value }
120
+ end
121
+ end
122
+
123
+ def classify collection
124
+ if object_is_hash_like? collection
125
+ hash_like collection
126
+ else
127
+ array_like collection
128
+ end
129
+ end
130
+
131
+ def normalized collection
132
+ if hash_like?
133
+ normalized_hash collection
134
+ else
135
+ normalized_array collection
136
+ end
137
+ end
138
+
139
+ def normalized_hash collection
140
+ collection ||= Hash[]
141
+
142
+ Hash[
143
+ collection.map do |key, value|
144
+ [key, value_normalizer.normalize(value)]
145
+ end
146
+ ]
147
+ end
148
+
149
+ def normalized_array collection
150
+ collection ||= []
151
+
152
+ collection.map { |value| value_normalizer.normalize value }
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,9 @@
1
+ class Holotype
2
+ class InheritanceDisallowedError < StandardError
3
+ def message; MESSAGE; end
4
+
5
+ private
6
+
7
+ MESSAGE = 'Cannot inherit from immutable class'.freeze
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ class Holotype
2
+ class MissingRequiredAttributesError < StandardError
3
+ attr_reader :attributes, :original_class
4
+
5
+ def initialize original_class, attributes
6
+ @attributes = attributes
7
+ @original_class = original_class
8
+ end
9
+
10
+ def message
11
+ "Class `#{original_class.name}` requires the following attributes:" \
12
+ "#{format_list required_attributes}" \
13
+ "\n\n" \
14
+ "Missing attributes:" \
15
+ "#{format_list attributes}".freeze
16
+ end
17
+
18
+ private
19
+
20
+ def required_attributes
21
+ original_class
22
+ .attributes
23
+ .values
24
+ .select(&:required?)
25
+ .map(&:name)
26
+ end
27
+
28
+ def format_list attributes
29
+ attributes
30
+ .map { |name| "\n * #{name}" }
31
+ .join
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ class Holotype
2
+ class ValueNormalizer
3
+ attr_reader :definition
4
+
5
+ def initialize definition
6
+ @definition = definition
7
+ end
8
+
9
+ def normalize value
10
+ result = if definition.has_value_class?
11
+ if value.nil?
12
+ nil
13
+ else
14
+ definition.value_class.new value
15
+ end
16
+ else
17
+ value
18
+ end
19
+
20
+ if definition.immutable?
21
+ result.freeze
22
+ else
23
+ result
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ class Holotype
2
+ VERSION = '0.15.1'.freeze
3
+ end
data/lib/holotype.rb ADDED
@@ -0,0 +1,182 @@
1
+ %w[
2
+ memorandum
3
+ ].each { |gem| require gem }
4
+
5
+ %i[
6
+ attribute
7
+ attributes_already_defined_error
8
+ collection_normalizer
9
+ inheritance_disallowed_error
10
+ missing_required_attributes_error
11
+ value_normalizer
12
+ version
13
+ ].each { |name| require_relative "holotype/#{name}.rb" }
14
+
15
+ class Holotype
16
+ # Singleton Definition
17
+
18
+ class << self
19
+ def attribute name, **options, &default
20
+ # symbolize name
21
+ name = name.to_sym
22
+
23
+ # prepare options
24
+ processed_options = if immutable?
25
+ [
26
+ __default_attribute_options,
27
+ options,
28
+ IMMUTABLE_OPTION,
29
+ ].reduce :merge
30
+ else
31
+ [
32
+ __default_attribute_options,
33
+ options,
34
+ ].reduce :merge
35
+ end
36
+
37
+ # create attribute definition
38
+ attribute = Attribute::Definition.new name,
39
+ **processed_options,
40
+ &default
41
+
42
+ # store the attribute definition
43
+ attributes[name] = attribute
44
+
45
+ # create an attribute reader
46
+ define_method name do
47
+ self.attributes[name].value
48
+ end
49
+
50
+ # create an attribute writer
51
+ define_method "#{name}=" do |value|
52
+ self.attributes[name].value = value
53
+ end
54
+ end
55
+
56
+ def attributes
57
+ @attributes ||= Hash[]
58
+ end
59
+
60
+ def make_immutable
61
+ raise AttributesAlreadyDefinedError.new if attributes.count != 0
62
+
63
+ define_singleton_method :inherited do |_|
64
+ raise InheritanceDisallowedError.new
65
+ end
66
+
67
+ @immutable = true
68
+ end
69
+
70
+ def immutable?
71
+ !!@immutable
72
+ end
73
+
74
+ def default_attribute_options **options
75
+ @default_attribute_options = options.freeze
76
+ end
77
+
78
+ private
79
+
80
+ IMMUTABLE_OPTION = Hash[immutable: true].freeze
81
+
82
+ def __default_attribute_options
83
+ @default_attribute_options || Hash[]
84
+ end
85
+ end
86
+
87
+ # Instance Definition
88
+
89
+ attr_reader :attributes
90
+
91
+ def initialize **attributes
92
+ __holotype_check_for_missing_required attributes
93
+ __holotype_store attributes
94
+ end
95
+
96
+ def frozen?
97
+ true
98
+ end
99
+
100
+ def to_hash
101
+ Hash[
102
+ attributes
103
+ .map do |key, attribute|
104
+ definition = attribute.definition
105
+
106
+ value = __holotype_hashify attribute.value
107
+
108
+ [key, value]
109
+ end
110
+ ]
111
+ end
112
+
113
+ def == other
114
+ return false unless self.class == other.class
115
+
116
+ attributes.all? do |name, attribute|
117
+ attribute.value == other.attributes[name].value
118
+ end
119
+ end
120
+
121
+ def with **attributes
122
+ self.class.new to_hash.merge attributes
123
+ end
124
+
125
+ def inspect
126
+ data = to_hash
127
+ .map { |attribute, value| "#{attribute}: #{value.inspect}" }
128
+ .join(', ')
129
+
130
+ "#{self.class.name}(#{data})"
131
+ end
132
+
133
+ private
134
+
135
+ def __holotype_hashify value
136
+ if value.respond_to? :to_hash
137
+ value.to_hash
138
+ elsif value.kind_of? Enumerable
139
+ value.map { |value| __holotype_hashify value }
140
+ else
141
+ value
142
+ end
143
+ end
144
+
145
+ def __holotype_check_for_missing_required attributes
146
+ self
147
+ .class
148
+ .attributes
149
+ .flat_map do |name, attribute|
150
+ # skip non-required attributes
151
+ next [] unless attribute.required?
152
+
153
+ # skip attributes with provided values
154
+ next [] if attributes.key? name
155
+
156
+ [name]
157
+ end
158
+ .tap do |missing_attributes|
159
+ next if missing_attributes.empty?
160
+ raise MissingRequiredAttributesError.new self.class, missing_attributes
161
+ end
162
+ end
163
+
164
+ def __holotype_store attributes
165
+ @attributes = Hash[
166
+ self
167
+ .class
168
+ .attributes
169
+ .map do |name, definition|
170
+ options = if attributes.key? name
171
+ Hash value: attributes[name]
172
+ else
173
+ Hash[]
174
+ end
175
+
176
+ attribute = Attribute.new self, definition, **options
177
+
178
+ [name, attribute]
179
+ end
180
+ ].freeze
181
+ end
182
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: holotype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Lude
@@ -58,20 +58,38 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 2.1.2
61
+ version: '2.1'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 2.1.2
68
+ version: '2.1'
69
69
  description:
70
70
  email: rob@ertlu.de
71
71
  executables: []
72
72
  extensions: []
73
73
  extra_rdoc_files: []
74
- files: []
74
+ files:
75
+ - lib/holotype.rb
76
+ - lib/holotype/attribute.rb
77
+ - lib/holotype/attribute/definition.rb
78
+ - lib/holotype/attribute/definition/default_conflict_error.rb
79
+ - lib/holotype/attribute/definition/no_collection_class_error.rb
80
+ - lib/holotype/attribute/definition/no_value_class_error.rb
81
+ - lib/holotype/attribute/definition/required_conflict_error.rb
82
+ - lib/holotype/attribute/frozen_modification_error.rb
83
+ - lib/holotype/attribute/immutable_value_error.rb
84
+ - lib/holotype/attribute/read_only_error.rb
85
+ - lib/holotype/attributes_already_defined_error.rb
86
+ - lib/holotype/collection_normalizer.rb
87
+ - lib/holotype/collection_normalizer/expected_array_like_collection_error.rb
88
+ - lib/holotype/collection_normalizer/expected_hash_like_collection_error.rb
89
+ - lib/holotype/inheritance_disallowed_error.rb
90
+ - lib/holotype/missing_required_attributes_error.rb
91
+ - lib/holotype/value_normalizer.rb
92
+ - lib/holotype/version.rb
75
93
  homepage: https://www.github.com/robertlude/holotype
76
94
  licenses:
77
95
  - MIT
@@ -92,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
110
  version: '0'
93
111
  requirements: []
94
112
  rubyforge_project:
95
- rubygems_version: 2.2.5
113
+ rubygems_version: 2.6.11
96
114
  signing_key:
97
115
  specification_version: 4
98
116
  summary: Simple models