bhm 0.1.0

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