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.
@@ -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
@@ -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')
@@ -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')
@@ -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,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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shaped
4
+ VERSION = '0.6.1'
5
+ end
@@ -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: []