jet-contract 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: fcbe0e6e51fb3e7a5f0938d993c0b95242e0ab3a0a2f61166e71f675c242066b
4
+ data.tar.gz: 802842c9c22218e87e0e57ccc24d7fc00b4f4028ec526a8a74eae6f97e36e81b
5
+ SHA512:
6
+ metadata.gz: ea07b6d7c5786f3b196a4dd77c812dac7fe2d500b1c86d0a11276c9ec48b49e543d361be7358f2b4ce8a1ee892799c761cf82a2e76cf2579eab74443b4a9f929
7
+ data.tar.gz: 765fb5fcb4db348647445aa7d6e3fa282e8b6ac1fbab0d4923a285ab63c21442834d19438c66df88bded01f569d4f002dcfac134ad2c7e9f54e698644774def1
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2019 Joshua Hansen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ # Jet: A toolkit aimed at web apps.
2
+
3
+ TODO: Write.
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jet/contract"
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jet/type"
4
+ require "jet/contract/attribute"
5
+
6
+ module Jet
7
+ class Contract
8
+ FLATTEN_ERROR_TYPES = %i[
9
+ contract_validation_failure
10
+ check_each_failure
11
+ ].freeze
12
+
13
+ @checks = Contract::Check::BuiltIn
14
+ @types = Type::JSON
15
+
16
+ class << self
17
+ attr_reader :checks, :types
18
+
19
+ def build(*args, &blk)
20
+ raise ArgumentError, "no block given" unless block_given?
21
+ Builder.new.tap { |b| b.instance_eval(&blk) }.call(*args)
22
+ end
23
+
24
+ def checks!(checks)
25
+ validate_registry!("checks", checks, Check, :eql)
26
+ end
27
+
28
+ def checks=(checks)
29
+ @checks = checks!(checks)
30
+ end
31
+
32
+ def types!(types)
33
+ case types
34
+ when :http
35
+ Type::HTTP
36
+ when :json
37
+ Type::JSON
38
+ when :strict
39
+ Type::Strict
40
+ else
41
+ validate_registry!("types", types, Type, :string)
42
+ end
43
+ end
44
+
45
+ def types=(types)
46
+ @types = types!(types)
47
+ end
48
+
49
+ private
50
+
51
+ def validate_registry!(name, registry, type, key)
52
+ return registry if registry.respond_to?(:[]) && registry[key].is_a?(type)
53
+ raise ArgumentError, "`#{name}` must be a registry of #{type}"
54
+ end
55
+ end
56
+
57
+ def initialize(attributes, keys_in: [String, Symbol], keys_out: :to_sym, **)
58
+ @attributes = Jet.type_check_hash!("`attributes`", attributes, Attribute)
59
+ .transform_keys(&:to_s)
60
+
61
+ @opts = { keys_in: _keys_in(keys_in), keys_out: _keys_out(keys_out) }
62
+ end
63
+
64
+ def call(input, **)
65
+ results = check_attributes(filter_keys(input.to_h))
66
+ failure(results, input) || success(results)
67
+ end
68
+
69
+ def [](key)
70
+ @attributes[key]
71
+ end
72
+
73
+ def attributes
74
+ @attributes.dup
75
+ end
76
+
77
+ def opts
78
+ @opts.dup
79
+ end
80
+
81
+ def rebuild(*args)
82
+ to_builder.(*args)
83
+ end
84
+
85
+ def to_builder
86
+ Builder.new(@attributes.transform_values(&:to_builder))
87
+ end
88
+
89
+ def with(*other_contracts, **opts)
90
+ Jet.type_check_each!("`other_contracts`", other_contracts, Contract)
91
+
92
+ self.class.new(
93
+ other_contracts.each_with_object(attributes) { |c, atts| atts.merge!(c.attributes) },
94
+ **self.opts.merge(opts)
95
+ )
96
+ end
97
+
98
+ private
99
+
100
+ def _keys_in(classes)
101
+ Array(classes).map do |c|
102
+ next String if c == :string
103
+ next Symbol if c == :symbol
104
+ Jet.type_check!(":keys_in element #{c}", Class, Module)
105
+ c
106
+ end.uniq
107
+ end
108
+
109
+ def _keys_out(key_type)
110
+ if [Symbol, :symbol, :to_sym].include?(key_type)
111
+ :to_sym
112
+ elsif [String, :string, :to_s].include?(key_type)
113
+ :to_s
114
+ else
115
+ raise ArgumentError, ":keys_out must equal either :symbol or :string"
116
+ end
117
+ end
118
+
119
+ def check_attributes(input)
120
+ @attributes.each_with_object({}) do |(k, att), h|
121
+ if input.key?(k)
122
+ h[k] = att.(input[k], k.to_sym)
123
+ else
124
+ next if att.optional?
125
+ h[k] = Result.failure(:key_missing_failure, at: k.to_sym)
126
+ end
127
+ end
128
+ end
129
+
130
+ def failure(results, input)
131
+ return unless results.values.any?(&:failure?)
132
+ Result.failure(
133
+ :contract_validation_failure,
134
+ errors: flatten_errors(results.values),
135
+ input: input
136
+ )
137
+ end
138
+
139
+ def filter_keys(input)
140
+ input.select { |k, _| @opts[:keys_in].any? { |t| k.is_a?(t) } }
141
+ .transform_keys(&:to_s)
142
+ .select { |k, _| attributes.keys.include?(k) }
143
+ end
144
+
145
+ def flatten_errors(results)
146
+ results.select(&:failure?).each_with_object([]) do |r, errs|
147
+ next errs.concat(flatten_errors(r.errors)) if FLATTEN_ERROR_TYPES.include?(r.output)
148
+ errs << r
149
+ end
150
+ end
151
+
152
+ def success(results)
153
+ results
154
+ .each_with_object({}) { |(k, r), h| h[k.send(opts[:keys_out])] = r.output }
155
+ .yield_self { |output| Result.success(output) }
156
+ end
157
+ end
158
+ end
159
+
160
+ require "jet/contract/builder"
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jet/contract/check"
4
+ require "jet/contract/check/set"
5
+
6
+ module Jet
7
+ class Contract
8
+ class Attribute
9
+ attr_reader :checks, :type
10
+
11
+ def self.build(*args, &blk)
12
+ raise ArgumentError, "no block given" unless block_given?
13
+ Builder.new.instance_eval(&blk).call(*args)
14
+ end
15
+
16
+ def initialize(type, checks = nil, contract: nil, each: nil, required: true, **)
17
+ @type = Jet.type_check!("`type`", type, Type)
18
+ @checks = Jet.type_check!("`checks`", checks, Check::Set, NilClass)
19
+
20
+ raise ArgumentError, "cannot set both :contract and :each" if contract && each
21
+
22
+ @opts = {
23
+ contract: Jet.type_check!(":contract", contract, Contract, NilClass),
24
+ each: Jet.type_check!(":each", each, Attribute, NilClass),
25
+ required: required ? true : false
26
+ }
27
+ end
28
+
29
+ def call(input, at = [])
30
+ coerce(input).yield_self { |r| result_at(Jet.failure?(r) ? r : check(r.output), at) }
31
+ end
32
+
33
+ def check(output)
34
+ return Result.success if output.nil?
35
+ checks&.(output)&.tap { |r| return r if r.failure? }
36
+ check_contract(output) || check_each(output) || Result.success(output)
37
+ end
38
+
39
+ def coerce(input)
40
+ type.(input)
41
+ end
42
+
43
+ def is?
44
+ !maybe?
45
+ end
46
+
47
+ def maybe?
48
+ type.maybe?
49
+ end
50
+
51
+ def optional?
52
+ !required?
53
+ end
54
+
55
+ def opts
56
+ @opts.dup
57
+ end
58
+
59
+ def required?
60
+ @opts[:required]
61
+ end
62
+
63
+ def to_builder
64
+ Builder.new(
65
+ checks: @checks&.to_builder,
66
+ contract: @opts[:contract]&.to_builder,
67
+ each: @opts[:each]&.to_builder,
68
+ is: is?,
69
+ required: required?,
70
+ type: type.name
71
+ )
72
+ end
73
+
74
+ def to_sym
75
+ name
76
+ end
77
+
78
+ private
79
+
80
+ def check_contract(output)
81
+ @opts[:contract]&.(output)
82
+ end
83
+
84
+ def check_each(output)
85
+ return unless @opts[:each]
86
+ results = output.map.with_index { |v, i| @opts[:each].(v).with(at: [i]) }
87
+ return Result.success(results.map(&:output)) if results.all?(&:success?)
88
+ Result.failure(
89
+ :check_each_failure,
90
+ errors: results.select(&:failure?),
91
+ input: output
92
+ )
93
+ end
94
+
95
+ def result_at(result, at)
96
+ new_at = Array(at) + Array(result.at)
97
+ result.with(
98
+ at: new_at,
99
+ errors: result.errors.map { |r| result_at(r, new_at) }
100
+ )
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ require "jet/contract/attribute/builder"
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Contract
5
+ class Attribute
6
+ class Builder
7
+ def initialize(opts = {})
8
+ @opts = { required: true }.merge(opts)
9
+ checks(opts[:checks]) if opts[:checks]
10
+ contract(opts[:contract]) if opts[:contract]
11
+ each(*opts[:each]) if opts[:each]
12
+ end
13
+
14
+ def call(types = nil, checks = nil)
15
+ types = types.nil? ? Contract.types : Contract.types!(types)
16
+ checks = checks.nil? ? Contract.checks : Contract.checks!(checks)
17
+
18
+ Attribute.new(
19
+ type_with(types),
20
+ check_set_with(checks),
21
+ contract: @opts[:contract]&.(types, checks),
22
+ each: @opts[:each]&.(types, checks),
23
+ required: @opts[:required]
24
+ )
25
+ end
26
+
27
+ def checks(checks)
28
+ @opts[:checks] = checks.each_with_object([]) do |check, a|
29
+ case check
30
+ when Hash
31
+ check.each { |(k, v)| a << [k.to_sym, v] }
32
+ when Array
33
+ a << [check.first.to_sym].concat(check[1..-1])
34
+ else
35
+ a << [check.to_sym]
36
+ end
37
+ end
38
+ self
39
+ end
40
+
41
+ def contract(contract = nil, &blk)
42
+ raise ArgumentError, "cannot provide :contract if :each is set" if @opts[:each]
43
+ auto_type!("contract", :hash)
44
+ raise ArgumentError, "must provide either `contract` or a block" unless
45
+ !contract.nil? ^ block_given?
46
+
47
+ @opts[:contract] =
48
+ if block_given?
49
+ Contract::Builder.new.tap { |b| b.instance_eval(&blk) }
50
+ else
51
+ Jet.type_check!(":contract", contract, Contract, Contract::Builder)
52
+ end
53
+ self
54
+ end
55
+
56
+ def each(*args, &blk)
57
+ raise ArgumentError, "cannot provide :each if :contract is set" if @opts[:contract]
58
+ auto_type!("each", :array)
59
+ raise ArgumentError, "must provide either `args` or a block" unless
60
+ args.any? ^ block_given?
61
+
62
+ @opts[:each] =
63
+ if block_given?
64
+ self.class.new.instance_eval(&blk)
65
+ elsif args.size == 1 && args.first.is_a?(self.class)
66
+ args.first
67
+ else
68
+ self.class.new.is(*args)
69
+ end
70
+ self
71
+ end
72
+
73
+ def is(type, *checks)
74
+ @opts[:maybe] = false
75
+ type(type, *checks)
76
+ self
77
+ end
78
+
79
+ def maybe(type, *checks)
80
+ @opts[:maybe] = true
81
+ type(type, *checks)
82
+ self
83
+ end
84
+
85
+ def type(type, *checks)
86
+ @opts[:type] = Jet.type_check!("`type`", type, Symbol, Type).to_sym
87
+ checks(checks)
88
+ self
89
+ end
90
+
91
+ def opts
92
+ @opts.dup
93
+ end
94
+
95
+ private
96
+
97
+ def auto_type!(method, type)
98
+ type(type) unless @opts[:type]
99
+ raise ArgumentError, "##{method} can only be used with type :#{type}" unless
100
+ @opts[:type] == type
101
+ type
102
+ end
103
+
104
+ def check_set_with(checks)
105
+ return unless @opts[:checks]&.any?
106
+ Check::Set.new(*@opts[:checks].map { |name, *args| [checks[name]].concat(args) })
107
+ end
108
+
109
+ def type_with(types)
110
+ type = types.fetch(@opts[:type].to_sym)
111
+ return type.maybe if @opts[:maybe]
112
+ raise "#{type.inspect} is a maybe? type (hint: use #maybe instead of #is)" if
113
+ type.maybe?
114
+ type
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Contract
5
+ class Builder
6
+ attr_reader :checks, :types
7
+
8
+ def initialize(attribute_builders = {})
9
+ @attribute_builders = attribute_builders.dup
10
+ end
11
+
12
+ def [](key)
13
+ @attribute_builders[key]
14
+ end
15
+
16
+ def call(*args)
17
+ Contract.new(@attribute_builders.transform_values { |ab| ab.(*args) })
18
+ end
19
+
20
+ def attribute_builders
21
+ @attribute_builders.dup
22
+ end
23
+ alias to_h attribute_builders
24
+
25
+ def optional(key)
26
+ attribute_builder(key, false)
27
+ end
28
+
29
+ def required(key)
30
+ attribute_builder(key, true)
31
+ end
32
+
33
+ private
34
+
35
+ def attribute_builder(key, required)
36
+ @attribute_builders[key.to_sym] = Attribute::Builder.new(required: required)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Contract
5
+ class Check
6
+ def self.[](key)
7
+ BuiltIn[key]
8
+ end
9
+
10
+ attr_reader :check, :name
11
+
12
+ def initialize(name, &check)
13
+ raise ArgumentError, "no block given" unless block_given?
14
+ @check = lambda(&check)
15
+ @name = name
16
+ end
17
+
18
+ def call(output, *args)
19
+ result = check.(output, *args)
20
+ return Result.success(output, args: args) if Jet.success?(result)
21
+ Result.failure(error(result), Jet.context(result, args: args, input: output))
22
+ end
23
+
24
+ def inspect
25
+ "#<#{self.class.name}:#{name}>"
26
+ end
27
+
28
+ def to_sym
29
+ name
30
+ end
31
+
32
+ private
33
+
34
+ def error(result)
35
+ [:check_failure, name].tap { |errors| errors << result.output if result }
36
+ end
37
+
38
+ module BuiltIn
39
+ extend Core::InstanceRegistry
40
+ type Check
41
+
42
+ [
43
+ Check.new(:any?, &:any?),
44
+ Check.new(:empty?, &:empty?),
45
+ Check.new(:eql) { |output, other| output == other },
46
+ Check.new(:gt) { |output, other| output > other },
47
+ Check.new(:gte) { |output, other| output >= other },
48
+ Check.new(:in) { |output, collection| collection.include?(output) },
49
+ Check.new(:lt) { |output, other| output < other },
50
+ Check.new(:lte) { |output, other| output <= other },
51
+ Check.new(:match) { |output, regex| output.match?(regex) },
52
+ Check.new(:max_size) { |output, size| output.size <= size },
53
+ Check.new(:min_size) { |output, size| output.size >= size },
54
+ Check.new(:negative?, &:negative?),
55
+ Check.new(:nin) { |output, collection| !collection.include?(output) },
56
+ Check.new(:not) { |output, other| output != other },
57
+ Check.new(:positive?, &:positive?),
58
+ Check.new(:size) do |output, size_or_range|
59
+ case size_or_range
60
+ when Range
61
+ return true if size_or_range.include?(output.size)
62
+ Result.failure(:range, max: size_or_range.max, min: size_or_range.min)
63
+ else
64
+ return true if output.size == size_or_range
65
+ Result.failure(:exact, size: size_or_range)
66
+ end
67
+ end
68
+ ].map { |c| [c.name, c] }.to_h.tap { |checks| register(checks).freeze }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Contract
5
+ class Check
6
+ class Set
7
+ attr_reader :checks
8
+
9
+ def initialize(*checks)
10
+ @checks = []
11
+ checks.each { |c| add!(c) }
12
+ @checks.freeze
13
+ end
14
+
15
+ def call(output)
16
+ @checks.each do |(check, *args)|
17
+ check.(output, *args).tap { |r| return r if Jet.failure?(r) }
18
+ end
19
+ Result.success(output)
20
+ end
21
+
22
+ def to_builder
23
+ @checks.map { |(c, *args)| [c.name].concat(args) }
24
+ end
25
+
26
+ private
27
+
28
+ def add!(check)
29
+ case check
30
+ when Array
31
+ add_with_args!(check.first, *check[1..-1])
32
+ when Hash
33
+ check.each { |c, args| add_with_args!(c, args) }
34
+ when Check
35
+ add_with_args!(check)
36
+ else
37
+ Jet.type_check!(check.inspect, check, Array, Hash, Check)
38
+ end
39
+ @checks
40
+ end
41
+
42
+ def add_with_args!(check, *args)
43
+ Jet.type_check!(check.inspect, check, Check)
44
+ @checks << [check].concat(args)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Contract
5
+ MAJOR = 0
6
+ MINOR = 1
7
+ TINY = 0
8
+ VERSION = [MAJOR, MINOR, TINY].join(".").freeze
9
+
10
+ def self.version
11
+ VERSION
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jet-contract
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joshua
8
+ - Hansen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-11-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: jet-type
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 0.1.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 0.1.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: m
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.5'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.5'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '5.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '5.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '10.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rubocop
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '0.56'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '0.56'
98
+ description: Input validation DSL and support classes for the Jet Toolkit.
99
+ email:
100
+ - joshua@epicbanality.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - LICENSE.txt
106
+ - README.md
107
+ - lib/jet-contract.rb
108
+ - lib/jet/contract.rb
109
+ - lib/jet/contract/attribute.rb
110
+ - lib/jet/contract/attribute/builder.rb
111
+ - lib/jet/contract/builder.rb
112
+ - lib/jet/contract/check.rb
113
+ - lib/jet/contract/check/set.rb
114
+ - lib/jet/contract/version.rb
115
+ homepage: https://github.com/binarypaladin/jet-contract
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 2.5.0
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.7.6.2
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Input validation DSL and support classes for the Jet Toolkit.
139
+ test_files: []