shaped 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +142 -0
- data/.ruby-version +1 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +135 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +367 -0
- data/Rakefile +8 -0
- data/bin/_guard-core +31 -0
- data/bin/console +15 -0
- data/bin/guard +31 -0
- data/bin/release +16 -0
- data/bin/rspec +34 -0
- data/bin/rubocop +30 -0
- data/bin/setup +8 -0
- data/lib/shaped.rb +45 -0
- data/lib/shaped/shape.rb +15 -0
- data/lib/shaped/shapes/array.rb +21 -0
- data/lib/shaped/shapes/callable.rb +18 -0
- data/lib/shaped/shapes/class.rb +54 -0
- data/lib/shaped/shapes/equality.rb +15 -0
- data/lib/shaped/shapes/hash.rb +37 -0
- data/lib/shaped/shapes/or.rb +25 -0
- data/lib/shaped/version.rb +5 -0
- data/shaped.gemspec +31 -0
- metadata +102 -0
data/bin/release
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
set -euo pipefail
|
3
|
+
|
4
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
5
|
+
if [[ "$BRANCH" != "master" ]]; then
|
6
|
+
echo "Aborting release! Not on master. Current branch is '$BRANCH'.";
|
7
|
+
exit 1;
|
8
|
+
fi
|
9
|
+
|
10
|
+
COMMIT_MESSAGE=$(git log --format=%B -n 1)
|
11
|
+
if ! [[ $COMMIT_MESSAGE =~ ^Prepare[[:space:]]to[[:space:]]release[[:space:]]v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
12
|
+
echo "Aborting release! Commit message is improperly formatted. Commit message is '$COMMIT_MESSAGE'.";
|
13
|
+
exit 1;
|
14
|
+
fi
|
15
|
+
|
16
|
+
bundle exec rake build release:guard_clean release:source_control_push
|
data/bin/rspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||=
|
13
|
+
File.expand_path(
|
14
|
+
'../../Gemfile',
|
15
|
+
Pathname.new(__FILE__).realpath,
|
16
|
+
)
|
17
|
+
|
18
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
19
|
+
|
20
|
+
if File.file?(bundle_binstub)
|
21
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
22
|
+
load(bundle_binstub)
|
23
|
+
else
|
24
|
+
abort(<<~ERROR)
|
25
|
+
Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
26
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.
|
27
|
+
ERROR
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'rubygems'
|
32
|
+
require 'bundler/setup'
|
33
|
+
|
34
|
+
load Gem.bin_path('rspec-core', 'rspec')
|
data/bin/rubocop
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
15
|
+
|
16
|
+
if File.file?(bundle_binstub)
|
17
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
18
|
+
load(bundle_binstub)
|
19
|
+
else
|
20
|
+
abort(<<~ERROR)
|
21
|
+
Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.
|
23
|
+
ERROR
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'rubygems'
|
28
|
+
require 'bundler/setup'
|
29
|
+
|
30
|
+
load Gem.bin_path('rubocop', 'rubocop')
|
data/bin/setup
ADDED
data/lib/shaped.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shaped ; end
|
4
|
+
module Shaped::Shapes ; end
|
5
|
+
|
6
|
+
require 'active_support/all'
|
7
|
+
require 'active_model'
|
8
|
+
require_relative './shaped/shape.rb'
|
9
|
+
Dir[File.dirname(__FILE__) + '/**/*.rb'].sort.each { |file| require file }
|
10
|
+
|
11
|
+
module Shaped
|
12
|
+
# rubocop:disable Naming/MethodName, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
13
|
+
def self.Shape(*shape_descriptions)
|
14
|
+
validation_options = shape_descriptions.extract_options!
|
15
|
+
if shape_descriptions.size >= 2
|
16
|
+
Shaped::Shapes::Or.new(*shape_descriptions, validation_options)
|
17
|
+
else
|
18
|
+
# If the shape_descriptions argument list was just one hash, then `extract_options!` would
|
19
|
+
# have removed it, making `shape_descriptions` an empty array, so we need to "restore" the
|
20
|
+
# "validation options" to their actual role of a Hash `shape_description` here.
|
21
|
+
shape_description =
|
22
|
+
case
|
23
|
+
when shape_descriptions.empty? && validation_options.is_a?(Hash)
|
24
|
+
validation_options
|
25
|
+
else
|
26
|
+
shape_descriptions.first
|
27
|
+
end
|
28
|
+
|
29
|
+
case shape_description
|
30
|
+
when Hash then Shaped::Shapes::Hash.new(shape_description)
|
31
|
+
when Array then Shaped::Shapes::Array.new(shape_description)
|
32
|
+
when Class then Shaped::Shapes::Class.new(shape_description, validation_options)
|
33
|
+
else
|
34
|
+
if shape_description.respond_to?(:call)
|
35
|
+
Shaped::Shapes::Callable.new(shape_description)
|
36
|
+
else
|
37
|
+
Shaped::Shapes::Equality.new(shape_description)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
# rubocop:enable Naming/MethodName, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
43
|
+
end
|
44
|
+
|
45
|
+
class Shaped::InvalidShapeDescription < StandardError ; end
|
data/lib/shaped/shape.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shaped::Shape
|
4
|
+
def initialize(_shape_description)
|
5
|
+
raise("`#initialize(shape_description)` must be implemented for #{self.class}!")
|
6
|
+
end
|
7
|
+
|
8
|
+
def matched_by?(_tested_object)
|
9
|
+
raise("`#matched_by?(tested_object)` must be implemented for #{self.class}!")
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
raise("`#to_s` must be implemented for #{self.class}!")
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shaped::Shapes::Array < Shaped::Shape
|
4
|
+
def initialize(shape_description)
|
5
|
+
if !shape_description.is_a?(Array)
|
6
|
+
raise(Shaped::InvalidShapeDescription, "A #{self.class} description must be an array.")
|
7
|
+
end
|
8
|
+
|
9
|
+
@element_test = Shaped::Shape(*shape_description)
|
10
|
+
end
|
11
|
+
|
12
|
+
def matched_by?(array)
|
13
|
+
return false if !array.is_a?(Array)
|
14
|
+
|
15
|
+
array.all? { |element| @element_test.matched_by?(element) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"[#{@element_test}]"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shaped::Shapes::Callable < Shaped::Shape
|
4
|
+
def initialize(callable)
|
5
|
+
@callable = callable
|
6
|
+
end
|
7
|
+
|
8
|
+
def matched_by?(object)
|
9
|
+
!!@callable.call(object)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
case @callable
|
14
|
+
when Proc then "Proc test defined at #{@callable.source_location.map(&:to_s).join(':')}"
|
15
|
+
else "#call test defined at #{@callable.method(:call).source_location.map(&:to_s).join(':')}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shaped::Shapes::Class < Shaped::Shape
|
4
|
+
def initialize(expected_klass, validations = {})
|
5
|
+
if !expected_klass.is_a?(Class)
|
6
|
+
raise(Shaped::InvalidShapeDescription, "A #{self.class} description must be a Class.")
|
7
|
+
end
|
8
|
+
|
9
|
+
@expected_klass = expected_klass
|
10
|
+
@validations = validations
|
11
|
+
@validator_klass = validator_klass(validations)
|
12
|
+
end
|
13
|
+
|
14
|
+
def matched_by?(object)
|
15
|
+
object.is_a?(@expected_klass) && validations_satisfied?(object)
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
if @validations.empty?
|
20
|
+
@expected_klass.name
|
21
|
+
else
|
22
|
+
"#{@expected_klass} validating #{@validations}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def validator_klass(validations)
|
29
|
+
return nil if validations.empty?
|
30
|
+
|
31
|
+
Class.new do
|
32
|
+
include ActiveModel::Validations
|
33
|
+
|
34
|
+
attr_accessor :value
|
35
|
+
|
36
|
+
validates :value, validations
|
37
|
+
|
38
|
+
class << self
|
39
|
+
# ActiveModel requires the class to have a `name`
|
40
|
+
def name
|
41
|
+
'Shaped::Shapes::Class::AnonymousValidator'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def validations_satisfied?(object)
|
48
|
+
return true if @validator_klass.nil?
|
49
|
+
|
50
|
+
validator_instance = @validator_klass.new
|
51
|
+
validator_instance.value = object
|
52
|
+
validator_instance.valid?
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shaped::Shapes::Equality < Shaped::Shape
|
4
|
+
def initialize(shape_description)
|
5
|
+
@expected_value = shape_description
|
6
|
+
end
|
7
|
+
|
8
|
+
def matched_by?(object)
|
9
|
+
object == @expected_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
@expected_value.inspect
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shaped::Shapes::Hash < Shaped::Shape
|
4
|
+
def initialize(shape_description)
|
5
|
+
unless shape_description.is_a?(Hash)
|
6
|
+
raise(Shaped::InvalidShapeDescription, "A #{self.class} description must be a Hash.")
|
7
|
+
end
|
8
|
+
|
9
|
+
@hash_description = shape_description.transform_values { |value| Shaped::Shape(value) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def matched_by?(hash)
|
13
|
+
return false if !hash.is_a?(Hash)
|
14
|
+
|
15
|
+
missing_keys = expected_keys - hash.keys
|
16
|
+
return false if missing_keys.any?
|
17
|
+
|
18
|
+
@hash_description.all? do |key, expected_value_shape|
|
19
|
+
expected_value_shape.matched_by?(hash[key])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
printable_shape_description =
|
25
|
+
@hash_description.map do |key, value|
|
26
|
+
"#{key.inspect} => #{value}"
|
27
|
+
end.join(', ')
|
28
|
+
|
29
|
+
"{ #{printable_shape_description} }"
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def expected_keys
|
35
|
+
@hash_description.keys
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shaped::Shapes::Or < Shaped::Shape
|
4
|
+
def initialize(*shape_descriptions)
|
5
|
+
validation_options = shape_descriptions.extract_options!
|
6
|
+
if shape_descriptions.size <= 1
|
7
|
+
raise(Shaped::InvalidShapeDescription, <<~ERROR.squish)
|
8
|
+
A #{self.class} description must be a list of two or more shape descriptions.
|
9
|
+
ERROR
|
10
|
+
end
|
11
|
+
|
12
|
+
@shapes =
|
13
|
+
shape_descriptions.map do |description|
|
14
|
+
Shaped::Shape(description, validation_options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def matched_by?(object)
|
19
|
+
@shapes.any? { |shape| shape.matched_by?(object) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
@shapes.map(&:to_s).join(' OR ')
|
24
|
+
end
|
25
|
+
end
|
data/shaped.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/shaped/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'shaped'
|
7
|
+
spec.version = Shaped::VERSION
|
8
|
+
spec.authors = ['David Runger']
|
9
|
+
spec.email = ['davidjrunger@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Validate the "shape" of Ruby objects.'
|
12
|
+
spec.description = 'Validate the "shape" of Ruby objects.'
|
13
|
+
spec.homepage = 'https://github.com/davidrunger/shaped'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/davidrunger/shaped'
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/davidrunger/shaped/blob/master/CHANGELOG.md'
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files =
|
24
|
+
Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
end
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
spec.add_runtime_dependency('activemodel', '~> 6.0')
|
30
|
+
spec.add_runtime_dependency('activesupport', '~> 6.0')
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shaped
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.6.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Runger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-06-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '6.0'
|
41
|
+
description: Validate the "shape" of Ruby objects.
|
42
|
+
email:
|
43
|
+
- davidjrunger@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- ".rspec"
|
50
|
+
- ".rubocop.yml"
|
51
|
+
- ".ruby-version"
|
52
|
+
- ".travis.yml"
|
53
|
+
- CHANGELOG.md
|
54
|
+
- Gemfile
|
55
|
+
- Gemfile.lock
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- bin/_guard-core
|
60
|
+
- bin/console
|
61
|
+
- bin/guard
|
62
|
+
- bin/release
|
63
|
+
- bin/rspec
|
64
|
+
- bin/rubocop
|
65
|
+
- bin/setup
|
66
|
+
- lib/shaped.rb
|
67
|
+
- lib/shaped/shape.rb
|
68
|
+
- lib/shaped/shapes/array.rb
|
69
|
+
- lib/shaped/shapes/callable.rb
|
70
|
+
- lib/shaped/shapes/class.rb
|
71
|
+
- lib/shaped/shapes/equality.rb
|
72
|
+
- lib/shaped/shapes/hash.rb
|
73
|
+
- lib/shaped/shapes/or.rb
|
74
|
+
- lib/shaped/version.rb
|
75
|
+
- shaped.gemspec
|
76
|
+
homepage: https://github.com/davidrunger/shaped
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata:
|
80
|
+
homepage_uri: https://github.com/davidrunger/shaped
|
81
|
+
source_code_uri: https://github.com/davidrunger/shaped
|
82
|
+
changelog_uri: https://github.com/davidrunger/shaped/blob/master/CHANGELOG.md
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: 2.3.0
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubygems_version: 3.1.2
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: Validate the "shape" of Ruby objects.
|
102
|
+
test_files: []
|