stannum 0.1.0 → 0.3.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +85 -21
  4. data/config/locales/en.rb +17 -3
  5. data/lib/stannum/constraints/base.rb +11 -4
  6. data/lib/stannum/constraints/hashes/extra_keys.rb +10 -2
  7. data/lib/stannum/constraints/hashes/indifferent_extra_keys.rb +47 -0
  8. data/lib/stannum/constraints/hashes.rb +6 -2
  9. data/lib/stannum/constraints/parameters/extra_arguments.rb +23 -0
  10. data/lib/stannum/constraints/parameters/extra_keywords.rb +29 -0
  11. data/lib/stannum/constraints/parameters.rb +11 -0
  12. data/lib/stannum/constraints/properties/base.rb +124 -0
  13. data/lib/stannum/constraints/properties/do_not_match_property.rb +117 -0
  14. data/lib/stannum/constraints/properties/match_property.rb +117 -0
  15. data/lib/stannum/constraints/properties/matching.rb +112 -0
  16. data/lib/stannum/constraints/properties.rb +17 -0
  17. data/lib/stannum/constraints/tuples/extra_items.rb +1 -1
  18. data/lib/stannum/constraints/type.rb +1 -1
  19. data/lib/stannum/constraints/types/hash_type.rb +6 -2
  20. data/lib/stannum/constraints.rb +2 -0
  21. data/lib/stannum/contracts/builder.rb +13 -2
  22. data/lib/stannum/contracts/hash_contract.rb +14 -0
  23. data/lib/stannum/contracts/indifferent_hash_contract.rb +13 -0
  24. data/lib/stannum/contracts/parameters/arguments_contract.rb +2 -7
  25. data/lib/stannum/contracts/parameters/keywords_contract.rb +2 -7
  26. data/lib/stannum/contracts/tuple_contract.rb +1 -1
  27. data/lib/stannum/entities/attributes.rb +218 -0
  28. data/lib/stannum/entities/constraints.rb +177 -0
  29. data/lib/stannum/entities/properties.rb +186 -0
  30. data/lib/stannum/entities.rb +13 -0
  31. data/lib/stannum/entity.rb +83 -0
  32. data/lib/stannum/errors.rb +3 -3
  33. data/lib/stannum/messages/default_loader.rb +95 -0
  34. data/lib/stannum/messages/default_strategy.rb +31 -50
  35. data/lib/stannum/messages.rb +1 -0
  36. data/lib/stannum/rspec/match_errors_matcher.rb +6 -6
  37. data/lib/stannum/rspec/validate_parameter_matcher.rb +10 -9
  38. data/lib/stannum/schema.rb +78 -37
  39. data/lib/stannum/struct.rb +12 -346
  40. data/lib/stannum/support/coercion.rb +19 -0
  41. data/lib/stannum/version.rb +1 -1
  42. data/lib/stannum.rb +3 -0
  43. metadata +29 -19
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbelt'
4
+ require 'yaml'
5
+
6
+ require 'stannum/messages'
7
+
8
+ module Stannum::Messages
9
+ # Loads and parses messages configuration files and merges configuration data.
10
+ class DefaultLoader
11
+ # @param file_paths [Array<String>] The directories from which to load the
12
+ # configuration files.
13
+ # @param locale [String] The name of the locale for which to load
14
+ # configuration.
15
+ def initialize(file_paths:, locale: 'en')
16
+ @file_paths = file_paths
17
+ @locale = locale
18
+ end
19
+
20
+ # @return [Array<String>] the directories from which to load the
21
+ # configuration files.
22
+ attr_reader :file_paths
23
+
24
+ # @return [String] the name of the locale for which to load configuration.
25
+ attr_reader :locale
26
+
27
+ # Loads and parses each file, then deep merges the data from each file.
28
+ #
29
+ # The configuration file should be either a Ruby file or a YAML file, with
30
+ # the filename of the format locale.extname, e.g. en.rb or en-gb.yml, and
31
+ # located in one of the directories defined in #file_paths.
32
+ #
33
+ # The contents of each file should be either a Ruby Hash or a YAML document
34
+ # containing an associative array, with a single key equal to the locale.
35
+ # The value of the key must be a Hash or associative array, which contains
36
+ # the scoped messages to load.
37
+ #
38
+ # Each file is read in order and parsed into a Hash. Each hash is then deep
39
+ # merged in sequence, with nested hashes merged together instead of
40
+ # overwritten.
41
+ #
42
+ # @return [Hash<Symbol, Object>] the merged configuration data.
43
+ def call
44
+ file_paths.reduce({}) do |config, file_path|
45
+ loaded = load_configuration_file(file_path)
46
+
47
+ deep_update(config, loaded)
48
+ end
49
+ end
50
+ alias load call
51
+
52
+ private
53
+
54
+ def deep_update(original, target)
55
+ target.each do |key, value|
56
+ if original[key].is_a?(Hash) && value.is_a?(Hash)
57
+ deep_update(original[key], value)
58
+ else
59
+ original[key] = value
60
+ end
61
+ end
62
+
63
+ original
64
+ end
65
+
66
+ def load_configuration_file(file_path)
67
+ ruby_file = File.join(file_path, "#{locale}.rb")
68
+
69
+ return read_ruby_file(ruby_file) if File.exist?(ruby_file)
70
+
71
+ yaml_file = File.join(file_path, "#{locale}.yml")
72
+
73
+ return read_yaml_file(yaml_file) if File.exist?(yaml_file)
74
+
75
+ {}
76
+ end
77
+
78
+ def read_ruby_file(filename)
79
+ ruby = File.read(filename)
80
+
81
+ eval(ruby, binding, filename) # rubocop:disable Security/Eval
82
+ end
83
+
84
+ def read_yaml_file(filename)
85
+ raw = File.read(filename)
86
+ yaml = YAML.safe_load(raw)
87
+
88
+ tools.hsh.convert_keys_to_symbols(yaml)
89
+ end
90
+
91
+ def tools
92
+ SleepingKingStudios::Tools::Toolbelt.instance
93
+ end
94
+ end
95
+ end
@@ -9,16 +9,28 @@ require 'stannum/messages'
9
9
  module Stannum::Messages
10
10
  # Strategy to generate error messages from gem configuration.
11
11
  class DefaultStrategy
12
+ # The default directories from which to load configured error messages.
13
+ DEFAULT_LOAD_PATHS = [Stannum::Messages.locales_path].freeze
14
+
15
+ # @return [Array<String>] The directories from which to load configured
16
+ # error messages.
17
+ def self.load_paths
18
+ @load_paths ||= DEFAULT_LOAD_PATHS.dup
19
+ end
20
+
12
21
  # @param configuration [Hash{Symbol, Object}] The configured messages.
13
- # @param load_path [Array<String>] The filenames for the configuration
14
- # file(s).
15
- def initialize(configuration: nil, load_path: nil)
16
- @load_path = load_path.nil? ? [default_filename] : Array(load_path)
22
+ # @param load_paths [Array<String>] The directories from which to load
23
+ # configured error messages.
24
+ # @param locale [String] The locale used to load and scope configured
25
+ # messages.
26
+ def initialize(configuration: nil, load_paths: nil, locale: 'en')
27
+ @load_paths = Array(load_paths) unless load_paths.nil?
28
+ @locale = locale
17
29
  @configuration = configuration
18
30
  end
19
31
 
20
- # @return [Array<String>] the filenames for the configuration file(s).
21
- attr_reader :load_path
32
+ # @return [String] The locale used to load and scope configured messages.
33
+ attr_reader :locale
22
34
 
23
35
  # @param error_type [String] The qualified path to the configured error
24
36
  # message.
@@ -34,6 +46,12 @@ module Stannum::Messages
34
46
  interpolate_message(message, options)
35
47
  end
36
48
 
49
+ # @return [Array<String>] The directories from which to load configured
50
+ # error messages.
51
+ def load_paths
52
+ @load_paths || self.class.load_paths
53
+ end
54
+
37
55
  # Reloads the configuration from the configured load_path.
38
56
  #
39
57
  # This can be useful when the load_path is updated after creating the
@@ -52,23 +70,9 @@ module Stannum::Messages
52
70
  @configuration ||= load_configuration
53
71
  end
54
72
 
55
- def deep_merge(source, target)
56
- hsh = tools.hash_tools.deep_dup(source)
57
-
58
- target.each do |key, value|
59
- hsh[key] = value.is_a?(Hash) ? deep_merge(hsh[key] || {}, value) : value
60
- end
61
-
62
- hsh
63
- end
64
-
65
- def default_filename
66
- File.join(Stannum::Messages.locales_path, 'en.rb')
67
- end
68
-
69
73
  def generate_message(error_type, options)
70
74
  path = error_type.to_s.split('.').map(&:intern)
71
- path.unshift(:en)
75
+ path.unshift(locale.intern)
72
76
 
73
77
  message = configuration.dig(*path)
74
78
 
@@ -90,35 +94,12 @@ module Stannum::Messages
90
94
  end
91
95
 
92
96
  def load_configuration
93
- load_path.reduce({}) do |config, filename|
94
- deep_merge(config, read_configuration(filename))
95
- end
96
- end
97
-
98
- def read_configuration(filename)
99
- case File.extname(filename)
100
- when '.rb'
101
- read_ruby_file(filename)
102
- when '.yml'
103
- read_yaml_file(filename)
104
- else
105
- raise "unable to load configuration file #{filename} with extension" \
106
- " #{File.extname(filename)}"
107
- end
108
- end
109
-
110
- def read_ruby_file(filename)
111
- eval(File.read(filename), binding, filename) # rubocop:disable Security/Eval
112
- end
113
-
114
- def read_yaml_file(filename)
115
- tools.hash_tools.convert_keys_to_symbols(
116
- YAML.safe_load(File.read(filename))
117
- )
118
- end
119
-
120
- def tools
121
- SleepingKingStudios::Tools::Toolbelt.instance
97
+ Stannum::Messages::DefaultLoader
98
+ .new(
99
+ file_paths: load_paths,
100
+ locale: locale
101
+ )
102
+ .call
122
103
  end
123
104
  end
124
105
  end
@@ -5,6 +5,7 @@ require 'stannum'
5
5
  module Stannum
6
6
  # Namespace for generating messages for Stannum::Errors.
7
7
  module Messages
8
+ autoload :DefaultLoader, 'stannum/messages/default_loader'
8
9
  autoload :DefaultStrategy, 'stannum/messages/default_strategy'
9
10
 
10
11
  # @return [String] the absolute path to the configured locales.
@@ -4,8 +4,8 @@ begin
4
4
  require 'rspec/sleeping_king_studios/matchers/core/deep_matcher'
5
5
  rescue NameError
6
6
  # :nocov:
7
- Kernel.warn 'WARNING: RSpec::SleepingKingStudios is a dependency for using' \
8
- ' the MatchErrorsMatcher or the #match_errors method.'
7
+ Kernel.warn 'WARNING: RSpec::SleepingKingStudios is a dependency for using ' \
8
+ 'the MatchErrorsMatcher or the #match_errors method.'
9
9
  # :nocov:
10
10
  end
11
11
 
@@ -40,8 +40,8 @@ module Stannum::RSpec
40
40
  # @return [String] a summary message describing a failed expectation.
41
41
  def failure_message
42
42
  unless errors?
43
- return 'expected the errors to match the expected errors, but the' \
44
- ' object is not an array or Errors object'
43
+ return 'expected the errors to match the expected errors, but the ' \
44
+ 'object is not an array or Errors object'
45
45
  end
46
46
 
47
47
  equality_matcher.failure_message
@@ -51,8 +51,8 @@ module Stannum::RSpec
51
51
  # expectation.
52
52
  def failure_message_when_negated
53
53
  unless errors?
54
- return 'expected the errors not to match the expected errors, but the' \
55
- ' object is not an array or Errors object'
54
+ return 'expected the errors not to match the expected errors, but ' \
55
+ 'the object is not an array or Errors object'
56
56
  end
57
57
 
58
58
  equality_matcher.failure_message_when_negated
@@ -6,6 +6,7 @@ rescue NameError
6
6
  # Optional dependency.
7
7
  end
8
8
 
9
+ require 'stannum/constraints/parameters/extra_keywords'
9
10
  require 'stannum/rspec'
10
11
  require 'stannum/support/coercion'
11
12
 
@@ -138,11 +139,11 @@ module Stannum::RSpec
138
139
  when :method_does_not_have_parameter
139
140
  "##{method_name} does not have a #{parameter_name.inspect} parameter"
140
141
  when :parameter_not_validated
141
- "##{method_name} does not expect a #{parameter_name.inspect}" \
142
- " #{parameter_type}"
142
+ "##{method_name} does not expect a #{parameter_name.inspect} " \
143
+ "#{parameter_type}"
143
144
  when :valid_parameter_value
144
- "#{valid_value.inspect} is a valid value for the" \
145
- " #{parameter_name.inspect} #{parameter_type}"
145
+ "#{valid_value.inspect} is a valid value for the " \
146
+ "#{parameter_name.inspect} #{parameter_type}"
146
147
  end
147
148
 
148
149
  [message, reason].compact.join(', but ')
@@ -280,20 +281,20 @@ module Stannum::RSpec
280
281
  unless @expected_constraint.nil?
281
282
  raise RuntimeError,
282
283
  '#does_not_match? with #using_constraint is not supported',
283
- caller[1..-1]
284
+ caller[1..]
284
285
  end
285
286
 
286
287
  unless @parameters.nil?
287
288
  raise RuntimeError,
288
289
  '#does_not_match? with #with_parameters is not supported',
289
- caller[1..-1]
290
+ caller[1..]
290
291
  end
291
292
 
292
293
  return if @parameter_value.nil?
293
294
 
294
295
  raise RuntimeError,
295
296
  '#does_not_match? with #with_value is not supported',
296
- caller[1..-1]
297
+ caller[1..]
297
298
  end
298
299
 
299
300
  def equality_matcher
@@ -313,9 +314,9 @@ module Stannum::RSpec
313
314
 
314
315
  def extra_parameter?
315
316
  extra_arguments_type =
316
- Stannum::Contracts::Parameters::ArgumentsContract::EXTRA_ARGUMENTS_TYPE
317
+ Stannum::Constraints::Parameters::ExtraArguments::TYPE
317
318
  extra_keywords_type =
318
- Stannum::Contracts::Parameters::KeywordsContract::EXTRA_KEYWORDS_TYPE
319
+ Stannum::Constraints::Parameters::ExtraKeywords::TYPE
319
320
 
320
321
  return false unless scoped_errors(indexed: true).any? do |error|
321
322
  error[:type] == extra_arguments_type ||
@@ -18,27 +18,6 @@ module Stannum
18
18
  @attributes = {}
19
19
  end
20
20
 
21
- # @!method each
22
- # Iterates through the the attributes by name and attribute object.
23
- #
24
- # @yieldparam name [String] The name of the attribute.
25
- # @yieldparam attribute [Stannum::Attribute] The attribute object.
26
-
27
- # @!method each_key
28
- # Iterates through the the attributes by name.
29
- #
30
- # @yieldparam name [String] The name of the attribute.
31
-
32
- # @!method each_value
33
- # Iterates through the the attributes by attribute object.
34
- #
35
- # @yieldparam attribute [Stannum::Attribute] The attribute object.
36
-
37
- def_delegators :attributes,
38
- :each,
39
- :each_key,
40
- :each_value
41
-
42
21
  # Retrieves the named attribute object.
43
22
  #
44
23
  # @param key [String, Symbol] The name of the requested attribute.
@@ -48,9 +27,17 @@ module Stannum
48
27
  # @raise ArgumentError if the key is invalid.
49
28
  # @raise KeyError if the attribute is not defined.
50
29
  def [](key)
51
- validate_key(key)
30
+ tools.assertions.assert_name(key, as: 'key', error_class: ArgumentError)
31
+
32
+ str = -key.to_s
52
33
 
53
- attributes.fetch(key.to_s)
34
+ each_ancestor do |ancestor|
35
+ next unless ancestor.own_attributes.key?(str)
36
+
37
+ return ancestor.own_attributes[str]
38
+ end
39
+
40
+ {}.fetch(str)
54
41
  end
55
42
 
56
43
  # rubocop:disable Metrics/MethodLength
@@ -81,33 +68,83 @@ module Stannum
81
68
  end
82
69
  # rubocop:enable Metrics/MethodLength
83
70
 
71
+ # Iterates through the the attributes by name and attribute object.
72
+ #
73
+ # @yieldparam name [String] The name of the attribute.
74
+ # @yieldparam attribute [Stannum::Attribute] The attribute object.
75
+ def each(&block)
76
+ return enum_for(:each) { size } unless block_given?
77
+
78
+ each_ancestor do |ancestor|
79
+ ancestor.own_attributes.each(&block)
80
+ end
81
+ end
82
+
83
+ # Iterates through the the attributes by name.
84
+ #
85
+ # @yieldparam name [String] The name of the attribute.
86
+ def each_key(&block)
87
+ return enum_for(:each_key) { size } unless block_given?
88
+
89
+ each_ancestor do |ancestor|
90
+ ancestor.own_attributes.each_key(&block)
91
+ end
92
+ end
93
+
94
+ # Iterates through the the attributes by attribute object.
95
+ #
96
+ # @yieldparam attribute [Stannum::Attribute] The attribute object.
97
+ def each_value(&block)
98
+ return enum_for(:each_value) { size } unless block_given?
99
+
100
+ each_ancestor do |ancestor|
101
+ ancestor.own_attributes.each_value(&block)
102
+ end
103
+ end
104
+
84
105
  # Checks if the given attribute is defined.
85
106
  #
86
107
  # @param key [String, Symbol] the name of the attribute to check.
87
108
  #
88
109
  # @return [Boolean] true if the attribute is defined; otherwise false.
89
110
  def key?(key)
90
- validate_key(key)
111
+ tools.assertions.assert_name(key, as: 'key', error_class: ArgumentError)
91
112
 
92
- attributes.key?(key.to_s)
113
+ each_ancestor.any? do |ancestor|
114
+ ancestor.own_attributes.key?(key.to_s)
115
+ end
93
116
  end
94
117
  alias has_key? key?
95
118
 
119
+ # Returns the defined attribute keys.
120
+ #
121
+ # @return [Array<String>] the attribute keys.
122
+ def keys
123
+ each_key.to_a
124
+ end
125
+
96
126
  # @private
97
127
  def own_attributes
98
128
  @attributes
99
129
  end
100
130
 
101
- private
131
+ # @return [Integer] the number of defined attributes.
132
+ def size
133
+ each_ancestor.reduce(0) do |memo, ancestor|
134
+ memo + ancestor.own_attributes.size
135
+ end
136
+ end
137
+ alias count size
102
138
 
103
- def attributes
104
- ancestors
105
- .reverse_each
106
- .select { |mod| mod.is_a?(Stannum::Schema) }
107
- .map(&:own_attributes)
108
- .reduce(&:merge)
139
+ # Returns the defined attribute value.
140
+ #
141
+ # @return [Array<Stannum::Attribute>] the attribute values.
142
+ def values
143
+ each_value.to_a
109
144
  end
110
145
 
146
+ private
147
+
111
148
  def define_reader(attr_name, reader_name)
112
149
  define_method(reader_name) { @attributes[attr_name] }
113
150
  end
@@ -118,14 +155,18 @@ module Stannum
118
155
  end
119
156
  end
120
157
 
121
- def validate_key(key)
122
- raise ArgumentError, "key can't be blank" if key.nil?
158
+ def each_ancestor
159
+ return enum_for(:each_ancestor) unless block_given?
123
160
 
124
- unless key.is_a?(String) || key.is_a?(Symbol)
125
- raise ArgumentError, 'key must be a String or Symbol'
161
+ ancestors.reverse_each do |ancestor|
162
+ break unless ancestor.is_a?(Stannum::Schema)
163
+
164
+ yield ancestor
126
165
  end
166
+ end
127
167
 
128
- raise ArgumentError, "key can't be blank" if key.to_s.empty?
168
+ def tools
169
+ SleepingKingStudios::Tools::Toolbelt.instance
129
170
  end
130
171
  end
131
172
  end