shaped 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []