stannum 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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