bhm 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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: deba451efeb03dbbbc84340918f311129808c08401b98cdbe9e2b309cae64b3d
4
+ data.tar.gz: 8aca882d42fedbc4fc01f0317aea620eeadbd6b63034de4e197a5855bd774c42
5
+ SHA512:
6
+ metadata.gz: 9b643a56b5e6689e4013de3a035609b222dc895cd416634be0bf889e15d0aca574cc66ccdd4f8c93fdb98a0590ae7c38142fddcf6a8a01e8dcb3ff0b7da4c71f
7
+ data.tar.gz: b9c2c24d3e2147eb0fd67534fce82c74675fc1bc639ed939666d88ca4b8faa08ee54a6642fe17c99d0bdef92cad5ccd1659276669534c75e1cb95c75dc9a817d
@@ -0,0 +1,40 @@
1
+ # Hsah
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hsah`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'hsah'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install hsah
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hsah.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,10 @@
1
+ # Example of a shared schema, `meta`, to be re-used in multiple places
2
+ require "bhm"
3
+
4
+ module Meta
5
+ include Bhm::Validation
6
+ validator :created_at, DateTime
7
+ validator :updated_at, DateTime
8
+
9
+ validator :uuid, ->(value) { value.match?(/\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z/) }
10
+ end
@@ -0,0 +1,21 @@
1
+ # Example of validating a team resource, using the validation helpers
2
+ require "bhm"
3
+ require "date"
4
+ require_relative "meta"
5
+ require_relative "user"
6
+
7
+ module Team
8
+ include Bhm::Validation
9
+ validator :name, String
10
+ validator :rank, Integer
11
+ validator :users, ->(value) { value.is_a?(Array) && value.each { |user_hash| user_hash.extend(User).validate! } }
12
+
13
+ module Budget
14
+ include Bhm::Validation
15
+
16
+ validator :amount, Integer
17
+ validator :meta, ->(value) { value.extend(Meta).validate! }
18
+ end
19
+
20
+ validator :meta, ->(value) { value.extend(Meta).validate! }
21
+ end
@@ -0,0 +1,21 @@
1
+ # Example of validating a user resource, using scaffolding
2
+ require "bhm"
3
+ require_relative "meta"
4
+
5
+ module User
6
+ @name = String
7
+ @email = ->(value) { value.is_a?(String) && value.match?(/[^\s]@[^\s]/) }
8
+ @tos_acceptance = TrueClass
9
+ module Address
10
+ @street = String
11
+ end
12
+
13
+ module Profile
14
+ @introduction = ->(value) { value.is_a?(String) && value.length < 255 }
15
+ @hobbies = Array
16
+ end
17
+
18
+ @meta = ->(value) { value.extend(Meta).validate! }
19
+ end
20
+
21
+ User.include(Bhm::Scaffold)
@@ -0,0 +1,5 @@
1
+ require_relative "bhm/errors"
2
+ require_relative "bhm/utils"
3
+ require_relative "bhm/scaffold"
4
+ require_relative "bhm/validation"
5
+ require_relative "bhm/version"
@@ -0,0 +1,90 @@
1
+ module Bhm
2
+ module Errors
3
+ module Chainable
4
+ # Helper for re-raising easily
5
+ def self.included(klass)
6
+ # Add new class method, without needing to prepend the class
7
+ klass.define_singleton_method(:raise!) do |*args, **kwargs|
8
+ fail new(args[0], **kwargs)
9
+ end
10
+ end
11
+
12
+ # Retrieve the chain of errors up to the most recent included module
13
+ # Eg.
14
+ # hash.extend(Document)
15
+ # error_chain == [Document, Components, Schemas, Primitive]
16
+ #
17
+ # hash.extend(Document::Components)
18
+ # error_chain == [Components, Schemas, Primitive]
19
+ def error_chain
20
+ @error_chain ||= -> {
21
+ out, parent = [self], cause
22
+ loop do
23
+ break out if parent.nil?
24
+ out.push(parent)
25
+ parent = parent.cause
26
+ end
27
+ }.call
28
+ end
29
+ end
30
+
31
+ # Pre-validation check indicates this should not be validated
32
+ class WontValidate < ArgumentError
33
+ include Chainable
34
+ attr_accessor :receiver
35
+ def initialize(message = nil, receiver:)
36
+ message ||= "A guard raised; Will not attempt validation for this hash"
37
+ self.receiver = receiver
38
+ super(message)
39
+ end
40
+ end
41
+
42
+ # Generic error -- the hash could not be fully validated
43
+ class InvalidHash < KeyError
44
+ include Chainable
45
+ def initialize(message = nil, receiver:, key:)
46
+ message ||= "the hash could not be fully validated"
47
+ super(message, receiver: receiver, key: key)
48
+ end
49
+
50
+ # From the invalid hash, get a period-joined string of keys leading to the invalid key or value
51
+ def ref
52
+ error_chain.map(&:key).join(".")
53
+ end
54
+
55
+ # TODO: Import absolute_ref
56
+ end
57
+
58
+ # The key does not adhere to the asserted key typing
59
+ class InvalidKeyType < InvalidHash
60
+ def initialize(message = nil, receiver:, key:, type:)
61
+ message ||= "all keys must be #{type} (got #{key.inspect})"
62
+ super(message, receiver: receiver, key: key)
63
+ end
64
+ end
65
+
66
+ # The key does not adhere to the asserted key casing
67
+ class InvalidKeyCasing < InvalidHash
68
+ def initialize(message = nil, receiver:, key:, casing:)
69
+ message ||= "the key #{key.inspect} is not in #{casing} casing"
70
+ super(message, receiver: receiver, key: key)
71
+ end
72
+ end
73
+
74
+ # A required value was not found
75
+ class MissingKey < InvalidHash
76
+ def initialize(message = nil, receiver:, key:)
77
+ message ||= "could not find key: #{key.inspect}"
78
+ super(message, receiver: receiver, key: key)
79
+ end
80
+ end
81
+
82
+ # The value was found, but calling `validate!` on it raised a validation error
83
+ class InvalidValue < InvalidHash
84
+ def initialize(message = nil, receiver:, key:)
85
+ message ||= "a value exists for key #{key.inspect} -- but it did not pass validation"
86
+ super(message, receiver: receiver, key: key)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,47 @@
1
+ require_relative "utils"
2
+ require_relative "validation"
3
+
4
+ module Bhm
5
+ # Quick way to start including hash validation. May be the only module some projects need :)
6
+ module Scaffold
7
+ # Upon being included do the following:
8
+ # - define any validators based on the declared instance_variables
9
+ # (then)
10
+ # - fetch all declared modules
11
+ ## And recursively do the following:
12
+ ## - extend it with Bhm::Validation
13
+ ## - define a new validator, using the module name as a key
14
+ ## (a nested module assumes it's a nested hash)
15
+ #### @mod_name = ->(hash) { hash.extend(ModName).validate! }
16
+ def self.included(klass)
17
+ # Include the validation suite in self
18
+ klass.include(Validation)
19
+
20
+ # Find all modules (we are assuming these are hashes)
21
+ klass.constants.each do |sym|
22
+ const = const_get([klass, sym].join("::"))
23
+ next unless const.is_a?(Module)
24
+
25
+ # Then, define the validator for the current class (calls validate! on the hash)
26
+ # default_key_case may return `nil` if no global config is set.
27
+ casing_transform = klass.default_key_case || :lower_snake
28
+ key = Utils.transform_module_casing(sym, casing_transform, klass.default_key_type)
29
+
30
+ klass.validator key, ->(hash) { hash.extend(const).validate! }
31
+
32
+ # Lastly, recursively include the scaffolding suite in these child modules
33
+ const.include(Scaffold)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Usage:
40
+ # 1. Define a group of nested modules:
41
+ # module Apex
42
+ # module Hash1
43
+ # module NestedHash
44
+ # # IF this nested hash is the bottommost "leaf", we will not traverse any further.
45
+ # # Defining instance variables is still respected
46
+ # module Hash2
47
+ # module NestedHash
@@ -0,0 +1,61 @@
1
+ module Bhm
2
+ module Utils
3
+ module_function
4
+
5
+ # Transform a stringified module to an accessible hash key
6
+ # Assume we want symbols by default
7
+ def transform_module_casing(module_name, key_casing, key_type)
8
+ transformed_module = case key_casing
9
+ when :lower_snake
10
+ module_name.to_s.gsub(/[A-Z]/) { |mtch| "_" + mtch.downcase }.sub(/^_/, "")
11
+ when :lowerCamel
12
+ module_name.to_s.sub(/^./) { |first_char| first_char.downcase }
13
+ when :UpperCamel
14
+ module_name.to_s
15
+ when :SCREAMING_SNAKE
16
+ module_name.to_s.gsub(/[A-Z]/) { |mtch| "_" + mtch }.upcase.sub(/^_/, "")
17
+ else
18
+ fail ArgumentError, "invalid casing provided: #{key_casing.inspect}"
19
+ end
20
+
21
+ # Coerce into symbols if key_type was explicitly passed as `nil`
22
+ key_type ||= :symbols
23
+ transformed_module = transformed_module.to_sym if key_type == :symbols
24
+ transformed_module
25
+ end
26
+
27
+ # Take a hash which includes Bhm::Validation & traverse upward nested module which also includes Validation
28
+ def traverse_ancestors(hash)
29
+ all = hash.singleton_class.included_modules.select { |mod| mod.include? Validation }
30
+ current = all.first
31
+ traversed_ancestors = []
32
+
33
+ while current.include?(Validation)
34
+ as_ary = current.to_s.split("::")
35
+ # Truncating the final ::(string), then rejoin
36
+
37
+ # Now that we have the module in question, transform the case using the module's method, if it has it defined
38
+ accessed_key = as_ary[-1]
39
+
40
+ # HACK: make an instance which extends the current module to use its #default_key values
41
+ transformed_key = -> do
42
+ hack = {}.extend(current)
43
+ key_casing = hack.default_key_case
44
+ key_type = hack.default_key_type
45
+
46
+ transform_module_casing(accessed_key, key_casing, key_type)
47
+ end.call
48
+
49
+ traversed_ancestors << transformed_key
50
+
51
+ last = current
52
+ current = Object.const_get(as_ary[0, (as_ary.length - 1)].join("::"))
53
+ end
54
+
55
+ # Array
56
+ # idx 0 is an array of the module names as formatted strings
57
+ # idx 1 is the apex, highest "entry" module
58
+ [traversed_ancestors, last]
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,194 @@
1
+ require_relative "errors"
2
+ require_relative "utils"
3
+
4
+ module Bhm
5
+ # The meat of the library
6
+ module Validation
7
+ def self.included(receiver)
8
+ # NOTE: Methods **MUST** be in the included block -- otherwise we pollute the module constants with
9
+ # any "initializer" module we include/extend
10
+ receiver.singleton_class.class_eval do
11
+ def config(**cfg)
12
+ pending_case, pending_keys = cfg.values_at(:default_key_case, :default_key_type)
13
+
14
+ case_options = [:lower_snake, :lowerCamel, :UpperCamel, :SCREAMING_SNAKE, nil]
15
+ fail ArgumentError, "casing must be one of #{case_options.inspect}" unless case_options.include? pending_case
16
+ @___default_key_case = pending_case
17
+
18
+ key_options = [:symbols, :strings, nil]
19
+ fail ArgumentError, "keys must be one of #{key_options.inspect}" unless key_options.include? pending_keys
20
+ @___default_key_type = pending_keys
21
+ end
22
+
23
+ # Concise API for setting both case assertion & key conformity simultaneously
24
+ def keys(arg)
25
+ key_option = arg.is_a?(String) ? :strings : :symbols
26
+ config(
27
+ default_key_type: key_option,
28
+ default_key_case: arg.to_sym
29
+ )
30
+ end
31
+
32
+ def default_key_case
33
+ # @___default_key_case ||= TODO_GLOBAL_CONFIG
34
+ @___default_key_case
35
+ end
36
+
37
+ def default_key_type
38
+ # @___default_key_type ||= TODO_GLOBAL_CONFIG
39
+ @___default_key_type
40
+ end
41
+
42
+ # TODO: `nullable` method
43
+
44
+ def guard(handler)
45
+ guards << handler
46
+ end
47
+
48
+ def guards
49
+ @___guards ||= []
50
+ end
51
+
52
+ #### 4 ways to define a validator:
53
+ ## Explicit key+proc:
54
+ # validator "KEY", ->(){}
55
+ # @KEY = "KEY", ->(){}
56
+ #
57
+ ## Explicit key+typecheck
58
+ # validator "KEY", String
59
+ # @KEY = "KEY", String
60
+ #
61
+ ## Implicit proc
62
+ # @___default_key_type = :strings
63
+ # @KEY = ->(){}
64
+ #
65
+ ## Implicit typecheck
66
+ # @___default_key_type = :strings
67
+ # @KEY = String
68
+ def validators
69
+ instance_variables.each_with_object({}) do |ivar, out|
70
+ # Skip our internals
71
+ next if ivar.to_s.start_with? "@___"
72
+
73
+ arg = instance_variable_get(ivar)
74
+ final_key, ambiguous_arg = case arg
75
+ when Array
76
+ # Assume the array idx0 is the key, idx1 is the handler or a Ruby class
77
+ arg.slice(0, 2)
78
+ else
79
+ # Assume some implicit flow. Use ivar name as key value
80
+ subbed = ivar.to_s.gsub(/^@/, "")
81
+ formatted_key = case default_key_type
82
+ when :strings then subbed
83
+ when :symbols then subbed.to_sym
84
+ else
85
+ # In the event no default settings are provided, default to sym assertion
86
+ subbed.to_sym
87
+ end
88
+
89
+ # Re-send the original arg
90
+ [formatted_key, arg]
91
+ end
92
+
93
+ # Lastly, check the ambiguous arg & set the finalized handler
94
+ out[final_key] = case ambiguous_arg
95
+ when Proc then ambiguous_arg # It is a dedicated proc handler
96
+ when Class then ->(fetched_value) { fetched_value.is_a?(ambiguous_arg) } # They want us to typecheck
97
+ else
98
+ # Assume bad user input
99
+ fail ArgumentError, "could not implement validator (supplied arg: #{ambiguous_arg.inspect}"
100
+ end
101
+ end
102
+ end
103
+
104
+ # Explicit API to set a key & handler, ignoring case/key-type config
105
+ def validator(key, handler)
106
+ # Use our array handling, to specifically bypass key checks
107
+ instance_variable_set("@#{key}", [key, handler])
108
+ end
109
+ end
110
+
111
+ # Define getters for each of these methods
112
+ %i[validators guards default_key_case default_key_type].each do |method_name|
113
+ define_method(method_name) do
114
+ singleton_class.included_modules.find { |mod|
115
+ mod.include? Validation
116
+ }.public_send(method_name)
117
+ end
118
+ end
119
+ end
120
+
121
+ # TODO: Assert the keys hash conform to a casing stantard
122
+ def validate_keys!
123
+ if default_key_type
124
+ klass = case default_key_type
125
+ when :symbols then Symbol
126
+ when :strings then String
127
+ end
128
+
129
+ keys.each do |string_or_sym|
130
+ Errors::InvalidKeyType.raise!(receiver: self, key: string_or_sym, type: default_key_type) unless string_or_sym.is_a? klass
131
+ end
132
+ end
133
+
134
+ case default_key_case
135
+ when :lower_snake
136
+ # TODO
137
+ when :lowerCamel
138
+ # TODO
139
+ when :UpperCamel
140
+ # TODO
141
+ when :SCREAMING_SNAKE
142
+ # TODO
143
+ end
144
+ end
145
+
146
+ def validate!
147
+ validate_keys!
148
+
149
+ run_guards!
150
+
151
+ # Run validation
152
+ run_validators!
153
+
154
+ # If we reach this line, the schema is valid.
155
+ self
156
+ end
157
+
158
+ def errors
159
+ @errors ||= []
160
+ end
161
+
162
+ def valid?
163
+ @errors = []
164
+ return true if validate!
165
+
166
+ # Only rescue lib-defined classes here. Let all other errors surface
167
+ rescue Errors::InvalidHash, Errors::WontValidate => e
168
+ @errors = e.error_chain
169
+ false
170
+ end
171
+
172
+ private
173
+
174
+ def run_validators!
175
+ validators.each do |key, handler|
176
+ hash = fetch(key)
177
+
178
+ result = handler.call(hash)
179
+ Errors::InvalidValue.raise!(key: key, receiver: self) unless result
180
+ rescue KeyError
181
+ mod = (singleton_class.included_modules.select { |mod| mod.include? Validation }).first
182
+ Errors::InvalidHash.raise!("could not validate: #{mod}", key: key, receiver: self)
183
+ end
184
+ end
185
+
186
+ def run_guards!
187
+ guards.each do |handler|
188
+ result = handler.call(self)
189
+
190
+ Errors::WontValidate.raise!(receiver: self) unless result
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,3 @@
1
+ module Bhm
2
+ VERSION = "0.1.0".freeze
3
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bhm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Trevor James
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-09-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: amazing_print
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry-byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: standard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email: trevor@osrs-stat.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - README.md
90
+ - examples/meta.rb
91
+ - examples/team.rb
92
+ - examples/user.rb
93
+ - lib/bhm.rb
94
+ - lib/bhm/errors.rb
95
+ - lib/bhm/scaffold.rb
96
+ - lib/bhm/utils.rb
97
+ - lib/bhm/validation.rb
98
+ - lib/bhm/version.rb
99
+ homepage: https://github.com/fire-pls/bhm
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 2.6.6
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 2.7.4
117
+ requirements: []
118
+ rubygems_version: 3.0.3
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Modular hash validation
122
+ test_files: []