structish 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: b8edee95c617e99bb439af1073b4b79697d301c487106320c2b075b2246a5bab
4
+ data.tar.gz: eb452debefdf9b4d6348bb11e6b5c812bdffbe97272d33651be399a4b7d12e4b
5
+ SHA512:
6
+ metadata.gz: f3ebf97ad136e4f175a5a4fbfb0bb06a8a31d3b63e01df5db44edbee439f8f43eacf4805fd262cb05cc20b51d884d5b6864b4e8b794f3d9c9a3c017b2c21c21d
7
+ data.tar.gz: 633f3b2c776edc67e49bc8084683e4eaf5e628c1e434631f1e2950636d941b1849c3987dd3e2a5854ff492a98b970c5e932a2b9e3e53447748515e1931a06221
@@ -0,0 +1,26 @@
1
+ env:
2
+ RUBY_VERSION: '2.6.6'
3
+
4
+ name: Tests
5
+ on: [push, pull_request]
6
+ jobs:
7
+ rspec-test:
8
+ name: Rspec
9
+ runs-on: ubuntu-18.04
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - uses: actions/setup-ruby@v1
13
+ with:
14
+ ruby-version: ${{ env.RUBY_VERSION }}
15
+ - name: Install dependencies
16
+ run: |
17
+ gem install bundler
18
+ bundler install
19
+ - name: Run tests
20
+ run: bundler exec rspec
21
+ - name: Upload coverage results
22
+ uses: actions/upload-artifact@master
23
+ if: always()
24
+ with:
25
+ name: coverage-report
26
+ path: coverage
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /out/
10
+ /Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ gemspec
6
+
7
+ group :test do
8
+ gem "rspec"
9
+ gem "simplecov", :require => false
10
+ end
11
+
12
+ group :test, :development do
13
+ gem "pry"
14
+ gem "pry-byebug"
15
+ end
@@ -0,0 +1,257 @@
1
+ # Structish
2
+
3
+ Structish objects are objects which maintain all the properties and methods of their parent classes, but add customisable validations, defaults, and mutations.
4
+
5
+ The concept came about when trying to create a data structure which could provide flexible validation for entries in a `Hash`; serve as a base class for inheritance so that further functions could be defined; and keep all the functionality of standard `Hash` objects.
6
+
7
+ Existing gems did not quite satisfy the requirements. [Dry::Struct](https://github.com/dry-rb/dry-struct) objects were found to be too rigid for the use cases, in that they do not allow additional keys to be defined other than the explicitly defined attributes. Furthermore, the biggest drawback is that `Dry::Struct` objects *are not hashes*. We wanted to keep all the functionality of hashes so that they would be both intuitive to use and highly flexible.
8
+
9
+ Other existing solutions generally fall into the `schema` pattern. Some type of schema is defined, and a method is added to the `Hash` object which allows one to validate against the schema. We still wanted a class-based inheritance system similar to `Dry::Struct`, in order to give more meaning to the objects we were creating. This in turn meant that continuous hash creation and validation would become tiresome and create less readable code.
10
+
11
+ So to summarise, the problem we were trying to solve needed a data structure that:
12
+ - Should be *rigid* enough to allow us to validate the data stored within the structure
13
+ - Should be *flexible* enough to allow us to define our own validations and methods, as well as allow for more than just the validated data to exist within the object
14
+ - Should be *functional* enough to still be used as a Hash (or other underlying base object)
15
+
16
+ The solution is a data structure which includes the functionality of `Hashes` (or `Arrays`), flexibility of `Schema` validations, and rigidity of `Structs` - the `Structish` object. When a class inherits from `Structish::Hash` or `Structish::Array`, it gains the ability to `validate` entries in the object. Properties which can be validated include the class type, value, and presence. Custom validations can also be added by creating a class which inherits from the `Structish::Validation` class and overriding the `validate` method.
17
+
18
+ Besides validating the constructor object, `Structish` objects also dynamically create accessor methods for the validated keys. So if we have `validate :foo` on the class, and an instance `obj` of that class, then we will have `obj.foo == constructor_hash[:foo]`
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'structish'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install structish
35
+
36
+ ## Usage
37
+
38
+ ### Creating Structish hashes and arrays
39
+
40
+ #### Class validation
41
+
42
+ There are two existing `Structish` classes: `Structish::Hash` and `Structish::Array`. Both follow similar usage. The simplest example is a hash which validates the classes of the keys:
43
+ ```ruby
44
+ class MyStructishHash < Structish::Hash
45
+ validate :foo, Float
46
+ end
47
+
48
+ # Validations
49
+ MyStructishHash.new({}) -> "Structish::ValidationError: Required value foo not present"
50
+ MyStructishHash.new({foo: "bar"}) -> "Structish::ValidationError: Class mismatch for foo -> String. Should be a Float"
51
+ MyStructishHash.new({foo: 1.0}) -> {:foo=>1.0}
52
+ MyStructishHash.new({foo: 1.0, bar: 2.0}) -> {:foo=>1.0, :bar=>2.0}
53
+
54
+ # Dynamic method creation
55
+ MyStructishHash.new({foo: 1.0, bar: 2.0}).foo -> 1.0
56
+ MyStructishHash.new({foo: 1.0, bar: 2.0})[:foo] -> 1.0
57
+ MyStructishHash.new({foo: 1.0, bar: 2.0}).bar -> "NoMethodError: undefined method `bar' for {:foo=>1.0, :bar=>2.0}:MyStructishHash"
58
+ MyStructishHash.new({foo: 1.0, bar: 2.0})[:bar] -> 1.0
59
+
60
+ # Inherited functionality
61
+ MyStructishHash.new({foo: 1.0}).merge(bar: 2.0) -> {:foo=>1.0, :bar=>2.0}
62
+ MyStructishHash.new({foo: 1.0}).merge(bar: 2.0).class -> MyStructishHash
63
+ ```
64
+
65
+ From the above example we can see that the validation is checking two properties - presence and class. Since the `:foo` key is validated, it is by default required to create the object. If the validation conditions are not met, we get clear errors telling us what is failing the validation. This is how we introduce rigidity and struct-like behaviour into the object.
66
+
67
+ We can also see that `Structish` obects are not restrictive, since any keys can be added to the constructor hash, but only the specified keys are validated. This is where the flexibility comes in.
68
+
69
+ The example also shows how the dynamic accessor methods are assigned - although in the last two lines we are defining `{foo: 1.0, bar: 2.0}` as the constructor, we only get `.foo` as an accessor method. This is by design - the idea is that the validated entries will in general be expected - any other entries are extra details, and not necessary to fully describe the object.
70
+
71
+ Finally we can see how `Structish` objects inherit the functionality from the parent class. Standard Ruby hash functions can be used freely with the new Structish objects, and (where applicable) we will get a new object of the same type as the object on which we are performing the operation. This is where the deep functionality comes in.
72
+
73
+ Note that the required class can be an array, and will pass if the value is an instance of one of the classes in the array.
74
+
75
+ When the required class is a `Array`, we can define a further requirement using the `of:` keyword to validate each element of the value array.
76
+
77
+ ```ruby
78
+ class MyStructishHash < Structish::Hash
79
+ validate :foo, ::Array, of: Float
80
+ end
81
+
82
+ MyStructishHash.new({foo: 1.0}) -> "Structish::ValidationError: Class mismatch for foo. All values should be of type Float"
83
+ MyStructishHash.new({foo: [1.0, "bar"]}) -> "Structish::ValidationError: Class mismatch for foo. All values should be of type Float"
84
+ MyStructishHash.new({foo: [1.0, 2.0]}) -> {:foo=>[1.0, 2.0]}
85
+ ```
86
+
87
+ #### Accessor aliasing
88
+
89
+ Instead of having the accessor methods be automatically named, we can define an alias for the method.
90
+ ```ruby
91
+ class MyStructishHash < Structish::Hash
92
+ validate :foo, Float, alias_to: :aliased_method
93
+ end
94
+
95
+ MyStructishHash.new({foo: 1.0}).aliased_method -> 1.0
96
+ ```
97
+
98
+ #### Optional attributes
99
+
100
+ `Structish` object attributes can be flagged as optional. The usage should be fairly intuitive:
101
+ ```ruby
102
+ class MyStructishHash < Structish::Hash
103
+ validate :foo, Float, optional: true
104
+ end
105
+
106
+ MyStructishHash.new({}) -> {:foo=>nil}
107
+ MyStructishHash.new({}).foo -> nil
108
+ ```
109
+
110
+ As shown above, when an attribute is missing from the constructor hash, the key gets added to the `Structish` object, and the accessor method is defined as usual.
111
+
112
+ #### Default values
113
+
114
+ When an attribute is flagged as optional, a default value can be assigned to the key. Assigning a default to a non-optional (required) key does nothing - instantiating the object without a required key will still raise an error, regardless of whether a default is defined.
115
+
116
+ ```ruby
117
+ class MyStructishHash < Structish::Hash
118
+ validate :foo, Float, optional: true, default: 1.0
119
+ end
120
+
121
+ MyStructishHash.new({}) -> {:foo=>1.0}
122
+ MyStructishHash.new({}).foo -> 1.0
123
+ ```
124
+
125
+ ```ruby
126
+ class MyStructishHash < Structish::Hash
127
+ validate :foo, Float, default: 1.0
128
+ end
129
+
130
+ MyStructishHash.new({}) -> "Structish::ValidationError: Required value foo not present"
131
+ ```
132
+
133
+ A useful feature of the default option is that you can map the value from one key to the default value:
134
+
135
+ ```ruby
136
+ class MyStructishHash < Structish::Hash
137
+ validate :foo, Float
138
+ validate :bar, Float, optional: true, default: assign(:foo)
139
+ end
140
+
141
+ MyStructishHash.new({foo: 1.0}) -> {:foo=>1.0, :bar=>1.0}
142
+ MyStructishHash.new({foo: 1.0}).foo -> 1.0
143
+ MyStructishHash.new({foo: 1.0}).bar -> 1.0
144
+ ```
145
+
146
+ #### Type casting
147
+
148
+ `Structish` validations support forced type-casting. This occurs *before* data type validation, which means that we can potentially pass in an object which is not of the required class, but force type casting so that it passes the validation:
149
+
150
+ ```ruby
151
+ class StructishHashWithoutCasting < Structish::Hash
152
+ validate :foo, Float
153
+ end
154
+
155
+ StructishHashWithoutCasting.new({foo: "1"}) -> "Structish::ValidationError: Class mismatch for foo -> String. Should be a Float"
156
+
157
+ class StructishHashWithCasting < Structish::Hash
158
+ validate :foo, Float, cast: true
159
+ end
160
+
161
+ StructishHashWithCasting.new({foo: "1"}) -> {:foo=>1.0}
162
+ StructishHashWithCasting.new({foo: "1"}).foo -> 1.0
163
+ StructishHashWithCasting.new({foo: {}}) -> "NoMethodError: undefined method `to_f' for {}:Hash"
164
+ ```
165
+
166
+ For common Ruby types (specifically `String, Float, Integer, Symbol, Array, Hash`) this uses the relevant `to_x` function, namely `to_s, to_f, to_i, to_sym, to_a, to_h` respectively. For any custom classes, this will call `Klass.new(value)`.
167
+
168
+ #### Specific values
169
+
170
+ `Structish` objects are not limited to validating classes and presence - they can also validate specific values, using the `one_of:` key:
171
+
172
+ ```ruby
173
+ class MyStructishHash < Structish::Hash
174
+ validate :foo, Float, one_of: [0.0, 1.0, 2.0]
175
+ end
176
+
177
+ MyStructishHash.new({foo: 1.0}) -> {:foo=>1.0}
178
+ MyStructishHash.new({foo: 5.0}) -> "Structish::ValidationError: Value not one of 0.0, 1.0, 2.0"
179
+ ```
180
+
181
+ #### Global validations
182
+
183
+ `Structable` objects allow for validations at the global level, i.e. validations that apply to every value in the object. Global validations are defined almost identically to individual validations, with the exception that they do not specify the key:
184
+
185
+ ```ruby
186
+ class MyStructishHash < Structish::Hash
187
+ validate_all Float
188
+ end
189
+
190
+ MyStructishHash.new({foo: 1.0, bar: 2.0}) -> {:foo=>1.0, :bar=>2.0}
191
+ MyStructishHash.new({foo: 1.0, bar: "2.0"}) -> "Structish::ValidationError: Class mismatch for bar -> String. Should be a Float"
192
+ ```
193
+
194
+ The `validate_all` function can perform all the same validations as the individual validations, and can also be mixed and matched with individual validations.
195
+
196
+ #### Accessor block mutations
197
+
198
+ A nifty feature of `Structish` object is, within the validation, we can define a block which mutates the output of the dynamic accessor method:
199
+
200
+ ```ruby
201
+ class MyStructishHash < Structish::Hash
202
+ validate :foo, Float, do |num|
203
+ num * 2
204
+ end
205
+ end
206
+
207
+ MyStructishHash.new(validated_key: 5.0)[:validated_key] -> 5.0
208
+ MyStructishHash.new(validated_key: 5.0).validated_key -> 10.0
209
+ ```
210
+
211
+ It is important to realize that the mutation *only applies to the dynamically created accessor method*. We still want to allow access to the original data - the idea here is that the accessor method can perform any operations on the original value, while the hash version stores the original data.
212
+
213
+ #### Function delegations
214
+
215
+ Along with dynamically created accessor methods, we can create delegations, so that calling a function on the `Structish` object returns the result of calling it on the specified attribute.
216
+
217
+ ```ruby
218
+ class MyStructishHash < Structish::Hash
219
+ validate :foo, String
220
+
221
+ delegate :downcase, :foo
222
+ delegate :upcase, :foo
223
+ end
224
+
225
+ MyStructishHash.new(validated_key: "HeLlo").downcase -> "hello"
226
+ MyStructishHash.new(validated_key: "HeLlo").upcase -> "HELLO"
227
+ ```
228
+
229
+ ### Custom validations
230
+
231
+ We can define custom validations, which may contain any logic that returns a truthy or falsey value. The validation class must inherit from `Structish::Validation` and must override the `validate` method. The accessible attribute to the class are `value` and `conditions` - `value` is the value detected for that key, and `conditions` are the conditions defined in the validation on the class.
232
+
233
+ ```ruby
234
+ class PositiveNumber < Structish::Validation
235
+ def validate
236
+ value > 0
237
+ end
238
+ end
239
+
240
+ class MyStructishHash < Structish::Hash
241
+ validate :foo, Float, validation: PositiveNumber
242
+ end
243
+
244
+ MyStructishHash.new({foo: 0.0}) -> "Structish::ValidationError: Custom validation PositiveNumberStructishValidation not met"
245
+ MyStructishHash.new({foo: 1.0}) -> {:foo=>1.0}
246
+ ```
247
+
248
+ ### Custom Structish data types
249
+
250
+ There are four custom 'classes' available for validation:
251
+ - `Structish::Number` passes for `Integer` or `Float`
252
+ - `Structish::Boolean` passes for `TrueClass` or `FalseClass`
253
+ - `Structish::Primitive` passes for `TrueClass`, `FalseClass`, `String`, `Float`, or `Integer`
254
+ - `Structish::Any` passes for any class
255
+
256
+ Although these are used like classes, they are in fact constants in the `Structish` which are given values of an array of the relevant classes (or `nil` in the case of `Structish::Any`). This is for simplicity, since the standard validations can accept arrays of classes.
257
+
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "structish"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,27 @@
1
+
2
+ require 'active_support/core_ext/hash'
3
+
4
+ require "structish/version"
5
+ require "structish_object_extensions"
6
+ require "structish/validation_error"
7
+ require "structish/validation"
8
+ require "structish/validations"
9
+ require "structish/hash"
10
+ require "structish/array"
11
+
12
+ module Structish
13
+ Any = nil
14
+ Boolean = [TrueClass, FalseClass].freeze
15
+ Number = [Integer, Float].freeze
16
+ Primitive = [String, Float, Integer, TrueClass, FalseClass, Symbol].freeze
17
+
18
+ CAST_METHODS = {
19
+ "String" => :to_s,
20
+ "Float" => :to_f,
21
+ "Integer" => :to_i,
22
+ "Hash" => :to_h,
23
+ "Symbol" => :to_sym,
24
+ "Array" => :to_a
25
+ }.freeze
26
+
27
+ end
@@ -0,0 +1,18 @@
1
+ module Structish
2
+ class Array < ::Array
3
+
4
+ include Structish::Validations
5
+
6
+ def initialize(constructor)
7
+ raise(ArgumentError, "Only array-like objects can be used as constructors for Structish::Array") unless constructor.class <= ::Array
8
+ validate_structish(constructor)
9
+ super(constructor)
10
+ end
11
+
12
+ def <<(entry)
13
+ super(entry)
14
+ validate_structish(self)
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ module Structish
2
+ class Hash < ::Hash
3
+
4
+ include Structish::Validations
5
+
6
+ def initialize(raw_constructor = {})
7
+ raise(ArgumentError, "Only hash-like objects can be used as constructors for Structish::Hash") unless raw_constructor.respond_to?(:to_hash)
8
+
9
+ constructor = self.class.symbolize? ? raw_constructor.symbolize_keys : raw_constructor
10
+ hash = constructor.to_h
11
+ validate_structish(hash)
12
+ super()
13
+ update(hash)
14
+ self.default = hash.default if hash.default
15
+ self.default_proc = hash.default_proc if hash.default_proc
16
+ end
17
+
18
+ def merge(other)
19
+ self.class.new(to_h.merge(other))
20
+ end
21
+
22
+ def merge!(other)
23
+ super(other)
24
+ validate_structish(self)
25
+ end
26
+
27
+ def except(*except_keys)
28
+ self.class.new(to_h.except(*except_keys))
29
+ end
30
+
31
+ def except!(*except_keys)
32
+ super(*except_keys)
33
+ validate_structish(self)
34
+ end
35
+
36
+ def compact
37
+ self.class.new(to_h.compact)
38
+ end
39
+
40
+ protected
41
+
42
+ def self.symbolize(sym)
43
+ @symbolize = sym
44
+ end
45
+
46
+ def self.symbolize?
47
+ @symbolize
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ module Structish
2
+ class Validation
3
+
4
+ attr_reader :value, :conditions, :constructor
5
+
6
+ def initialize(value, conditions, constructor)
7
+ @value = value
8
+ @conditions = conditions
9
+ @constructor = constructor
10
+ end
11
+
12
+ def validate
13
+ raise(NotImplementedError, "Validation conditions function must be defined")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ module Structish
2
+ class ValidationError < RuntimeError
3
+ def initialize(message, klass)
4
+ super("#{message} in class #{klass.to_s}")
5
+ set_backtrace(caller)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,230 @@
1
+ module Structish
2
+ module Validations
3
+
4
+ def self.included base
5
+ base.send :include, InstanceMethods
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module InstanceMethods
10
+
11
+ def validate_structish(constructor)
12
+ validate_key_restriction(constructor)
13
+ apply_defaults(constructor)
14
+ cast_values(constructor)
15
+ validate_constructor(constructor)
16
+ define_accessor_methods(constructor)
17
+ define_delegated_methods
18
+ end
19
+
20
+ def define_delegated_methods
21
+ self.class.delegations.each do |function, object|
22
+ define_singleton_method(function.to_s) { self.send(object.to_sym)&.send(function.to_sym) }
23
+ end
24
+ end
25
+
26
+ def validate_key_restriction(constructor)
27
+ if self.class.restrict?
28
+ allowed_keys = validations.map { |attribute| attribute[:key] }
29
+ valid = (constructor.keys - allowed_keys).empty?
30
+ raise(Structish::ValidationError.new("Keys are restricted to #{allowed_keys.join(", ")}", self.class)) unless valid
31
+ end
32
+ end
33
+
34
+ def apply_defaults(constructor)
35
+ validations.select { |v| v[:optional] }.each do |attribute|
36
+ key = attribute[:key]
37
+ default_value = if attribute[:default].is_a?(::Array) && attribute[:default].first == :other_attribute
38
+ constructor[attribute[:default][1]]
39
+ else
40
+ attribute[:default]
41
+ end
42
+ constructor[key] = default_value if constructor[key].nil?
43
+ end
44
+ end
45
+
46
+ def cast_values(constructor)
47
+ (validations + global_attributes_for(constructor)).each do |attribute|
48
+ key = attribute[:key]
49
+ if attribute[:cast] && constructor[key]
50
+ if attribute[:klass] == ::Array && attribute[:of]
51
+ constructor[key] = constructor[key].map { |v| cast_single(v, attribute[:of]) }
52
+ else
53
+ constructor[key] = cast_single(constructor[key], attribute[:klass])
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def cast_single(value, klass)
60
+ if value.is_a?(klass)
61
+ value
62
+ else
63
+ if cast_method = Structish::CAST_METHODS[klass.to_s]
64
+ value.send(cast_method)
65
+ else
66
+ klass.new(value)
67
+ end
68
+ end
69
+ end
70
+
71
+ def define_accessor_methods(constructor)
72
+ validations.each do |attribute|
73
+ if accessor = attribute[:accessor]
74
+ value = attribute[:proc] ? attribute[:proc].call(constructor[attribute[:key]]) : constructor[attribute[:key]]
75
+ instance_variable_set "@#{accessor}", value
76
+ end
77
+ end
78
+ end
79
+
80
+ def validate_constructor(constructor)
81
+ (validations + global_attributes_for(constructor)).each do |attribute|
82
+ value = constructor[attribute[:key]]
83
+ if attribute[:optional] && value.nil?
84
+ true
85
+ else
86
+ validate_presence(attribute, value)
87
+ validate_class(attribute, value)
88
+ validate_one_of(attribute, value)
89
+ validate_custom(attribute, value, constructor)
90
+ end
91
+ end
92
+ end
93
+
94
+ def global_attributes_for(constructor)
95
+ global_attributes_hash[constructor] = begin
96
+ constructor_keys = constructor.keys
97
+ global_validations.each_with_object([]) do |validation, arr|
98
+ constructor_keys.each { |key| arr << validation.merge(key: key) }
99
+ end
100
+ end
101
+ end
102
+
103
+ def validate_presence(attribute, value)
104
+ raise(Structish::ValidationError.new("Required value #{attribute[:key]} not present", self.class)) unless !value.nil?
105
+ end
106
+
107
+ def validate_class(attribute, value)
108
+ if attribute[:klass].nil?
109
+ return
110
+ elsif attribute[:of]
111
+ valid = if attribute[:klass] <= ::Array
112
+ value.class <= ::Array && value.all? { |v| v.class <= attribute[:of] }
113
+ elsif attribute[:klass] <= ::Hash
114
+ value.class <= ::Hash && value.values.all? { |v| v.class <= attribute[:of] }
115
+ end
116
+ raise(Structish::ValidationError.new("Class mismatch for #{attribute[:key]}. All values should be of type #{attribute[:of].to_s}", self.class)) unless valid
117
+ else
118
+ valid_klasses = [attribute[:klass]].flatten.compact
119
+ valid = valid_klasses.any? { |klass| value.class <= klass }
120
+ raise(Structish::ValidationError.new("Class mismatch for #{attribute[:key]} -> #{value.class}. Should be a #{valid_klasses.join(", ")}", self.class)) unless valid
121
+ end
122
+ end
123
+
124
+ def validate_one_of(attribute, value)
125
+ valid = attribute[:one_of] ? attribute[:one_of].include?(value) : true
126
+ raise(Structish::ValidationError.new("Value not one of #{attribute[:one_of].join(", ")}", self.class)) unless valid
127
+ end
128
+
129
+ def validate_custom(attribute, value, constructor)
130
+ valid = attribute[:validation] ? attribute[:validation].new(value, attribute, constructor).validate : true
131
+ raise(Structish::ValidationError.new("Custom validation #{attribute[:validation].to_s} not met", self.class)) unless valid
132
+ end
133
+
134
+ def []=(key, value)
135
+ super(key, value)
136
+ validate_structish(self)
137
+ end
138
+
139
+ def global_attributes_hash
140
+ @global_attributes_hash ||= {}
141
+ end
142
+
143
+ def validations
144
+ @validations ||= self.class.attributes + parent_attributes(self.class)
145
+ end
146
+
147
+ def global_validations
148
+ @global_validations ||= self.class.global_validations + parent_global_validations(self.class)
149
+ end
150
+
151
+ def parent_attributes(klass)
152
+ if klass.superclass.respond_to?(:structish?) && klass.superclass.structish?
153
+ klass.superclass.attributes + parent_attributes(klass.superclass)
154
+ end || []
155
+ end
156
+
157
+ def parent_global_validations(klass)
158
+ if klass.superclass.respond_to?(:structish?) && klass.superclass.structish?
159
+ klass.superclass.global_validations + parent_global_validations(klass.superclass)
160
+ end || []
161
+ end
162
+
163
+ end
164
+
165
+ module ClassMethods
166
+
167
+ def structish?
168
+ true
169
+ end
170
+
171
+ def delegate(function, object)
172
+ delegations << [function, object]
173
+ end
174
+
175
+ def assign(key)
176
+ [:other_attribute, key]
177
+ end
178
+
179
+ def restrict_attributes
180
+ @restrict_attributes = true
181
+ end
182
+
183
+ def validate(key, klass = nil, kwargs = {}, &block)
184
+ accessor_name = kwargs[:alias_to] ? kwargs[:alias_to] : key
185
+ accessor = accessor_name.to_s if (accessor_name.is_a?(String) || accessor_name.is_a?(Symbol))
186
+ attr_reader(accessor) if accessor
187
+ attribute_array = kwargs[:optional] ? optional_attributes : required_attributes
188
+ attribute_array << attribute_hash(key, klass, kwargs.merge(accessor: accessor), block)
189
+ end
190
+
191
+ def validate_all(klass = nil, kwargs = {}, &block)
192
+ global_validations << attribute_hash(nil, klass, kwargs, block)
193
+ end
194
+
195
+ def attribute_hash(key, klass = nil, kwargs = {}, block)
196
+ {
197
+ key: key,
198
+ klass: klass,
199
+ proc: block,
200
+ }.merge(kwargs.except(:key, :klass, :proc))
201
+ end
202
+
203
+ def global_validations
204
+ @global_validations ||= []
205
+ end
206
+
207
+ def required_attributes
208
+ @required_attributes ||= []
209
+ end
210
+
211
+ def optional_attributes
212
+ @optional_attributes ||= []
213
+ end
214
+
215
+ def attributes
216
+ required_attributes + optional_attributes
217
+ end
218
+
219
+ def delegations
220
+ @delegations ||= []
221
+ end
222
+
223
+ def restrict?
224
+ @restrict_attributes
225
+ end
226
+
227
+ end
228
+
229
+ end
230
+ end
@@ -0,0 +1,3 @@
1
+ module Structish
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,21 @@
1
+ class ::Hash
2
+ def to_structish(structish_klass)
3
+ raise(ArgumentError, "Class is not a child of Structish::Hash") unless structish_klass < Structish::Hash
4
+ structish_klass.new(self)
5
+ end
6
+ end
7
+
8
+ class ::Array
9
+ def values
10
+ self.to_a
11
+ end
12
+
13
+ def keys
14
+ [*0..self.size-1]
15
+ end
16
+
17
+ def to_structish(structish_klass)
18
+ raise(ArgumentError, "Class is not a child of Structish::Array") unless structish_klass < Structish::Array
19
+ structish_klass.new(self)
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "structish/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "structish"
8
+ spec.version = Structish::VERSION
9
+ spec.authors = ["Dylan Blakemore"]
10
+ spec.email = ["dylan.blakemore@gmail.com"]
11
+
12
+ spec.summary = %q{Adding struct-like properties to Ruby Arrays and Hashes}
13
+ spec.description = %q{Adds validations, function creation, function delegation,
14
+ and key restrictions to arrays and hashes so that they may
15
+ function similarly to Structs}
16
+
17
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata)
18
+
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_runtime_dependency "activesupport", "~> 5.0"
27
+
28
+ spec.add_development_dependency "bundler"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: structish
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dylan Blakemore
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-01-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
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: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description: |-
56
+ Adds validations, function creation, function delegation,
57
+ and key restrictions to arrays and hashes so that they may
58
+ function similarly to Structs
59
+ email:
60
+ - dylan.blakemore@gmail.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - ".github/workflows/structable-rspec.yml"
66
+ - ".gitignore"
67
+ - Gemfile
68
+ - README.md
69
+ - Rakefile
70
+ - bin/console
71
+ - bin/setup
72
+ - lib/structish.rb
73
+ - lib/structish/array.rb
74
+ - lib/structish/hash.rb
75
+ - lib/structish/validation.rb
76
+ - lib/structish/validation_error.rb
77
+ - lib/structish/validations.rb
78
+ - lib/structish/version.rb
79
+ - lib/structish_object_extensions.rb
80
+ - structish.gemspec
81
+ homepage:
82
+ licenses: []
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.0.3
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Adding struct-like properties to Ruby Arrays and Hashes
103
+ test_files: []