constrain 0.1.0
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 +19 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/README.md +103 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/constrain.gemspec +47 -0
- data/lib/constrain.rb +73 -0
- data/lib/constrain/version.rb +3 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b37ca58d51de33d6c5d6810c5b8e4b77e0d1d19355d4e381241b923f44918817
|
4
|
+
data.tar.gz: c64f19d94bb2e4dae6e4fbaf61a74d1fb6caee56f58a9006915a5749ee35b906
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bd6114ec7c7b5516c409ef41beae62d979c582f8f53c149634a1fc8ed8b4c595a7b3fd2d12d07a08fd2b7b587515adef890e6f68cdf6dd897d6ef79e8bad9c50
|
7
|
+
data.tar.gz: 9eec4bc8dac6976bbe2eb22e45bcf57589b5f1e04092596e1cb99f658c0b9144a68182eab73e0e5ae3eb9be9bb9434184f8c71122aaddba284d15f90cac4600e
|
data/.gitignore
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
/.bundle/
|
2
|
+
/.yardoc
|
3
|
+
/_yardoc/
|
4
|
+
/coverage/
|
5
|
+
/doc/
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/tmp/
|
9
|
+
|
10
|
+
# rspec failure tracking
|
11
|
+
.rspec_status
|
12
|
+
|
13
|
+
# Ignore auto-generated main file
|
14
|
+
/main
|
15
|
+
|
16
|
+
# Ignore Gemfile.lock. See https://stackoverflow.com/questions/4151495/should-gemfile-lock-be-included-in-gitignore
|
17
|
+
/Gemfile.lock
|
18
|
+
|
19
|
+
# Put your personal ignore files in /home/clr/.config/git/ignore
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.7.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Constrain
|
2
|
+
|
3
|
+
`Constrain` allows you to check if an object match a class expression. It is
|
4
|
+
typically used to check the type of method parameters and is an alternative to
|
5
|
+
using Ruby-3 .rbs files but with a different syntax and only dynamic checks
|
6
|
+
|
7
|
+
Constrain works with ruby-2 (and maybe ruby-3)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'constrain'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle install
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install constrain
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
You'll typically include the Constrain module and use the #constrain method to chech values:
|
28
|
+
|
29
|
+
include Constrain
|
30
|
+
|
31
|
+
# f takes a String and an array of Integer objects
|
32
|
+
def f(a, b)
|
33
|
+
constrain a, String
|
34
|
+
constrain b, [Integer]
|
35
|
+
end
|
36
|
+
|
37
|
+
The constrain instance method raises a Constrain::TypeError if the value
|
38
|
+
doesn't match the class expression. Constrain also defines the Constrain::check
|
39
|
+
class method that returns true/false depending on if the value match the
|
40
|
+
expression. Both methods raise a Constrain::Error if the expression is invalid
|
41
|
+
|
42
|
+
### Class Expressions
|
43
|
+
|
44
|
+
Constrain#constrain and Constrain::check use class expressions composed of
|
45
|
+
Class objects, Proc objects, or arrays and hashes of class objects. Class
|
46
|
+
objects match if the value is an instance of the class:
|
47
|
+
|
48
|
+
constrain 42, Integer # Success
|
49
|
+
constrain 42, String # Failure
|
50
|
+
|
51
|
+
Note that NilClass and TrueClass and FalseClass are valid arguments and allows
|
52
|
+
you to do value comparison for those types:
|
53
|
+
|
54
|
+
constrain nil, Integer # Failure
|
55
|
+
constrain nil, Integer, NilClass # Success
|
56
|
+
|
57
|
+
Proc objects are called with the value as argument and should return truish or falsy:
|
58
|
+
|
59
|
+
proc = lambda { |value| value > 1 }
|
60
|
+
constrain 42, proc # Success
|
61
|
+
constrain 0, proc # Failure
|
62
|
+
|
63
|
+
Proc objects can check every aspect of an object and but you should not overuse
|
64
|
+
them as `Constrain` is throught of as a poor-man's type checker. More elaborate
|
65
|
+
constraints should be checked explicitly
|
66
|
+
|
67
|
+
Arrays match if the value is an Array and all its element match the given class expression:
|
68
|
+
|
69
|
+
constrain [42], [Integer] # Success
|
70
|
+
constrain [42], [String] # Failure
|
71
|
+
|
72
|
+
Arrays can be nested
|
73
|
+
|
74
|
+
constrain [[42]], [[Integer]]
|
75
|
+
|
76
|
+
Note that arrays are treated specially in hashes
|
77
|
+
|
78
|
+
Hashes match if value is a hash and every key/value pair match one of the given
|
79
|
+
key-class/value-class expressions:
|
80
|
+
|
81
|
+
constrain({"str" => 42}, String => Integer) # Success
|
82
|
+
constrain({"str" => 42}, String => String) # Failure
|
83
|
+
|
84
|
+
Note that the parenthesis are needed because otherwise the Ruby parser would
|
85
|
+
interpret the hash as block argument to #constrain
|
86
|
+
|
87
|
+
Hash keys or values can also be lists of class expressions that match if any
|
88
|
+
expression match. List are annotated as an array but contains more than one
|
89
|
+
element so that `[String, Symbol]` matches either a String or a Symbol value
|
90
|
+
while `[String]` matches an array of String objects:
|
91
|
+
|
92
|
+
constrain({ sym: 42 }, [Symbol, String] => Integer) # Success
|
93
|
+
constrain({ [sym] => 42 }, [Symbol, String] => Integer) # Failure
|
94
|
+
|
95
|
+
To specify an array of Symbol or String objects in hash keys or values, make
|
96
|
+
sure the list expression is enclosed in an array:
|
97
|
+
|
98
|
+
constrain({ [sym] => 42 }, [[Symbol, String]] => Integer) # Success
|
99
|
+
|
100
|
+
## Contributing
|
101
|
+
|
102
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/clrgit/constrain.
|
103
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "constrain"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/constrain.gemspec
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'lib/constrain/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "constrain"
|
5
|
+
spec.version = Constrain::VERSION
|
6
|
+
spec.authors = ["Claus Rasmussen"]
|
7
|
+
spec.email = ["claus.l.rasmussen@gmail.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{Dynamic in-file type checking}
|
10
|
+
spec.description = %q{
|
11
|
+
Allows you check if an object match a class expression. It is typically
|
12
|
+
used to check the type of method paraameters. It is an alternative to using
|
13
|
+
Ruby-3 .rbs files but with a different syntax and only dynamic checks
|
14
|
+
|
15
|
+
Typically you'll include the Constrain module and use #constrain to check
|
16
|
+
the type of method parameters:
|
17
|
+
|
18
|
+
include Constrain
|
19
|
+
|
20
|
+
# f takes a String and an array of Integer objects. Raise a Constrain::Error
|
21
|
+
# if parameters doesn't have the expected types
|
22
|
+
def f(a, b)
|
23
|
+
constrain a, String
|
24
|
+
constrain b, [Integer]
|
25
|
+
end
|
26
|
+
|
27
|
+
Constrain works with ruby-2 (and maybe ruby-3)
|
28
|
+
}
|
29
|
+
spec.homepage = "http://www.nowhere.com/"
|
30
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
31
|
+
|
32
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
33
|
+
|
34
|
+
# Specify which files should be added to the gem when it is released.
|
35
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
36
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
37
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
38
|
+
end
|
39
|
+
spec.bindir = "exe"
|
40
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
41
|
+
spec.require_paths = ["lib"]
|
42
|
+
|
43
|
+
# spec.add_dependency GEM [, VERSION]
|
44
|
+
|
45
|
+
# spec.add_development_dependency GEM [, VERSION]
|
46
|
+
spec.add_development_dependency "simplecov"
|
47
|
+
end
|
data/lib/constrain.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require "constrain/version"
|
2
|
+
|
3
|
+
module Constrain
|
4
|
+
# Raised on any error
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Raised if types doesn't match a class expression
|
8
|
+
class TypeError < Error
|
9
|
+
def initialize(value, exprs)
|
10
|
+
super "Expected #{value.inspect} to match #{Constrain.fmt_exprs(exprs)}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Check that value matches one of the class expressions. Raises a
|
15
|
+
# Constrain::Error if the expression is invalid and a Constrain::TypeError if
|
16
|
+
# the value doesn't match
|
17
|
+
def constrain(value, *exprs)
|
18
|
+
return if exprs.any? { |expr| Constrain.check(value, expr) }
|
19
|
+
error = TypeError.new(value, exprs)
|
20
|
+
error.set_backtrace(caller[1..-1])
|
21
|
+
raise error
|
22
|
+
end
|
23
|
+
|
24
|
+
# Return true if the value matches the class expression. Raises a
|
25
|
+
# Constrain::Error if the expression is invalid
|
26
|
+
def self.check(value, expr)
|
27
|
+
case expr
|
28
|
+
when Class
|
29
|
+
value.is_a?(expr)
|
30
|
+
when Array
|
31
|
+
!expr.empty? or raise Error, "Empty array"
|
32
|
+
value.is_a?(Array) && value.all? { |elem| expr.any? { |e| check(elem, e) } }
|
33
|
+
when Hash
|
34
|
+
value.is_a?(Hash) && value.all? { |key, value|
|
35
|
+
expr.any? { |key_expr, value_expr|
|
36
|
+
[[key, key_expr], [value, value_expr]].all? { |value, expr|
|
37
|
+
if expr.is_a?(Array) && (expr.size > 1 || expr.first.is_a?(Array))
|
38
|
+
expr.any? { |e| check(value, e) }
|
39
|
+
else
|
40
|
+
check(value, expr)
|
41
|
+
end
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
when Proc
|
46
|
+
expr.call(value)
|
47
|
+
else
|
48
|
+
raise Error, "Illegal expression #{expr.inspect}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Render a class expression as a String. Same as
|
53
|
+
# <tt>exprs.map(&:inspect).join(", ")</tt> except that Proc objects are rendered as
|
54
|
+
# "Proc@<sourcefile>:<linenumber>"
|
55
|
+
def self.fmt_exprs(exprs)
|
56
|
+
exprs.map { |expr| fmt_expr(expr) }.join(", ")
|
57
|
+
end
|
58
|
+
|
59
|
+
# Render a class expression as a String. Same as +expr.inspect+ except that
|
60
|
+
# Proc objects are rendered as "Proc@<sourcefile>>:<linenumber>"
|
61
|
+
#
|
62
|
+
def self.fmt_expr(expr)
|
63
|
+
case expr
|
64
|
+
when Class; expr.to_s
|
65
|
+
when Array; "[" + expr.map { |expr| fmt_expr(expr) }.join(", ") + "]"
|
66
|
+
when Hash; "{" + expr.map { |k,v| "#{fmt_expr(k)} => #{fmt_expr(v)}" }.join(", ") + "}"
|
67
|
+
when Proc; "Proc@#{expr.source_location.first}:#{expr.source_location.last}"
|
68
|
+
else
|
69
|
+
raise Error, "Illegal expression"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: constrain
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Claus Rasmussen
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: simplecov
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: "\n Allows you check if an object match a class expression. It is
|
28
|
+
typically\n used to check the type of method paraameters. It is an alternative
|
29
|
+
to using\n Ruby-3 .rbs files but with a different syntax and only dynamic checks\n
|
30
|
+
\ \n Typically you'll include the Constrain module and use #constrain to check\n
|
31
|
+
\ the type of method parameters:\n\n include Constrain\n\n # f takes
|
32
|
+
a String and an array of Integer objects. Raise a Constrain::Error\n # if parameters
|
33
|
+
doesn't have the expected types\n def f(a, b)\n constrain a, String\n
|
34
|
+
\ constrain b, [Integer]\n end\n\n Constrain works with ruby-2 (and
|
35
|
+
maybe ruby-3)\n "
|
36
|
+
email:
|
37
|
+
- claus.l.rasmussen@gmail.com
|
38
|
+
executables: []
|
39
|
+
extensions: []
|
40
|
+
extra_rdoc_files: []
|
41
|
+
files:
|
42
|
+
- ".gitignore"
|
43
|
+
- ".rspec"
|
44
|
+
- ".ruby-version"
|
45
|
+
- ".travis.yml"
|
46
|
+
- Gemfile
|
47
|
+
- README.md
|
48
|
+
- Rakefile
|
49
|
+
- bin/console
|
50
|
+
- bin/setup
|
51
|
+
- constrain.gemspec
|
52
|
+
- lib/constrain.rb
|
53
|
+
- lib/constrain/version.rb
|
54
|
+
homepage: http://www.nowhere.com/
|
55
|
+
licenses: []
|
56
|
+
metadata:
|
57
|
+
homepage_uri: http://www.nowhere.com/
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 2.3.0
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubygems_version: 3.1.4
|
74
|
+
signing_key:
|
75
|
+
specification_version: 4
|
76
|
+
summary: Dynamic in-file type checking
|
77
|
+
test_files: []
|