shaped 0.6.1
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.
- 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: []
|