definition 0.1.0.rc1
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/.approvals +0 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +77 -0
- data/.travis.yml +14 -0
- data/Gemfile +6 -0
- data/Guardfile +72 -0
- data/README.md +205 -0
- data/Rakefile +22 -0
- data/definition.gemspec +39 -0
- data/lib/definition.rb +8 -0
- data/lib/definition/conform_error.rb +22 -0
- data/lib/definition/conform_result.rb +20 -0
- data/lib/definition/dsl.rb +74 -0
- data/lib/definition/types.rb +9 -0
- data/lib/definition/types/and.rb +60 -0
- data/lib/definition/types/base.rb +27 -0
- data/lib/definition/types/each.rb +67 -0
- data/lib/definition/types/include.rb +50 -0
- data/lib/definition/types/keys.rb +120 -0
- data/lib/definition/types/lambda.rb +34 -0
- data/lib/definition/types/or.rb +61 -0
- data/lib/definition/types/type.rb +39 -0
- data/lib/definition/version.rb +5 -0
- metadata +281 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5d4178b9d52b38f362ceeefc4e4b593c08bc1db136f39f43791c146890eebeec
|
4
|
+
data.tar.gz: 35a2df7826b4d9e77fa425711fcbff1ba1024438b355240a5aa5ee79a535e2d5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1f249e44ce43c29716f6943a8373399797d928f1796d42f6ead9f13261595e297cf4dc37418ef99d53717f6c57a5988ae78c01bab3c5cd91e227b3e150b7efae
|
7
|
+
data.tar.gz: 413f8ccf960d01faad85221108feda1b3c7ac41a0766c76d36872e58484f65d2c043a1f71d6fdde2de05814908b1325caeb0e2953f46281f625fe26eefc7f3db
|
data/.approvals
ADDED
File without changes
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-rspec
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
TargetRubyVersion: 2.3
|
6
|
+
|
7
|
+
Metrics/LineLength:
|
8
|
+
Max: 119
|
9
|
+
Exclude:
|
10
|
+
- Guardfile
|
11
|
+
- Gemfile
|
12
|
+
IgnoreCopDirectives: true
|
13
|
+
|
14
|
+
Metrics/BlockLength:
|
15
|
+
Exclude:
|
16
|
+
- '*.gemspec'
|
17
|
+
- '**/*.rake'
|
18
|
+
- 'spec/**/*'
|
19
|
+
|
20
|
+
Metrics/MethodLength:
|
21
|
+
Max: 11
|
22
|
+
|
23
|
+
|
24
|
+
Style/Documentation:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Style/AlignHash:
|
28
|
+
EnforcedColonStyle: table
|
29
|
+
EnforcedHashRocketStyle: table
|
30
|
+
|
31
|
+
Layout/AlignHash:
|
32
|
+
EnforcedColonStyle: table
|
33
|
+
EnforcedHashRocketStyle: table
|
34
|
+
|
35
|
+
Style/HashSyntax:
|
36
|
+
EnforcedStyle: ruby19_no_mixed_keys
|
37
|
+
|
38
|
+
Style/MultilineOperationIndentation:
|
39
|
+
Description: Checks indentation of binary operations that span more than one line.
|
40
|
+
EnforcedStyle: indented
|
41
|
+
|
42
|
+
Layout/MultilineOperationIndentation:
|
43
|
+
Description: Checks indentation of binary operations that span more than one line.
|
44
|
+
EnforcedStyle: indented
|
45
|
+
|
46
|
+
Style/RaiseArgs:
|
47
|
+
EnforcedStyle: compact
|
48
|
+
|
49
|
+
Style/SignalException:
|
50
|
+
EnforcedStyle: only_raise
|
51
|
+
|
52
|
+
Style/StringLiterals:
|
53
|
+
EnforcedStyle: double_quotes
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
RSpec/DescribeClass:
|
58
|
+
Exclude:
|
59
|
+
- spec/integration/**/*
|
60
|
+
|
61
|
+
RSpec/MessageSpies:
|
62
|
+
Enabled: false
|
63
|
+
|
64
|
+
RSpec/ExampleLength:
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
RSpec/NamedSubject:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
RSpec/NestedGroups:
|
71
|
+
Enabled: false
|
72
|
+
|
73
|
+
RSpec/MultipleExpectations:
|
74
|
+
Enabled: false
|
75
|
+
|
76
|
+
RSpec/MessageExpectation:
|
77
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A sample Guardfile
|
4
|
+
# More info at https://github.com/guard/guard#readme
|
5
|
+
|
6
|
+
## Uncomment and set this to only include directories you want to watch
|
7
|
+
directories(%w[lib spec])
|
8
|
+
.select { |d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist") }
|
9
|
+
|
10
|
+
## Note: if you are using the `directories` clause above and you are not
|
11
|
+
## watching the project directory ('.'), then you will want to move
|
12
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
13
|
+
#
|
14
|
+
# $ mkdir config
|
15
|
+
# $ mv Guardfile config/
|
16
|
+
# $ ln -s config/Guardfile .
|
17
|
+
#
|
18
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
19
|
+
|
20
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
21
|
+
# rspec may be run, below are examples of the most common uses.
|
22
|
+
# * bundler: 'bundle exec rspec'
|
23
|
+
# * bundler binstubs: 'bin/rspec'
|
24
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
25
|
+
# installed the spring binstubs per the docs)
|
26
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
27
|
+
# * 'just' rspec: 'rspec'
|
28
|
+
|
29
|
+
guard :rspec, cmd: "bundle exec rspec" do # rubocop:disable Metrics/BlockLength
|
30
|
+
require "guard/rspec/dsl"
|
31
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
32
|
+
|
33
|
+
# Feel free to open issues for suggestions and improvements
|
34
|
+
|
35
|
+
# RSpec files
|
36
|
+
rspec = dsl.rspec
|
37
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
38
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
39
|
+
watch(rspec.spec_files)
|
40
|
+
|
41
|
+
# Ruby files
|
42
|
+
ruby = dsl.ruby
|
43
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
44
|
+
|
45
|
+
# Rails files
|
46
|
+
rails = dsl.rails(view_extensions: %w[erb haml slim])
|
47
|
+
dsl.watch_spec_files_for(rails.app_files)
|
48
|
+
dsl.watch_spec_files_for(rails.views)
|
49
|
+
|
50
|
+
watch(rails.controllers) do |m|
|
51
|
+
[
|
52
|
+
rspec.spec.call("routing/#{m[1]}_routing"),
|
53
|
+
rspec.spec.call("controllers/#{m[1]}_controller"),
|
54
|
+
rspec.spec.call("acceptance/#{m[1]}")
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Rails config changes
|
59
|
+
watch(rails.spec_helper) { rspec.spec_dir }
|
60
|
+
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
61
|
+
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
62
|
+
|
63
|
+
# Capybara features specs
|
64
|
+
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
|
65
|
+
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
|
66
|
+
|
67
|
+
# Turnip features and steps
|
68
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
69
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
70
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
|
71
|
+
end
|
72
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
# Definition
|
2
|
+
|
3
|
+
[][travis]
|
4
|
+
[][rubygems]
|
5
|
+
|
6
|
+
Simple and composable validation and coercion of data structures
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'definition'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install definition
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Definitions can be used to validate data structures like for example Hashes:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
schema = Definition.Keys do
|
30
|
+
required :first_name, Definition.Type(String)
|
31
|
+
required :last_name, Definition.Type(String)
|
32
|
+
optional :birthday, Definition.Type(Date)
|
33
|
+
end
|
34
|
+
|
35
|
+
conform_result = schema.conform({first_name: "John", last_name: "Doe", birthday: Date.today})
|
36
|
+
conform_result.passed? # => true
|
37
|
+
|
38
|
+
conform_result = schema.conform({first_name: "John", last_name: "Doe", birthday: "2018/02/09"})
|
39
|
+
conform_result.passed? # => false
|
40
|
+
conform_result.error_message # => hash fails validation for key birthday: { Is of type String instead of Date }
|
41
|
+
```
|
42
|
+
|
43
|
+
But it can also transform those data structures at the same time. The following
|
44
|
+
example shows how a Unix timestamp in milliseconds can be transformed to a Time
|
45
|
+
object while validating:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
milliseconds_time_definition = Definition.Lambda(:milliseconds_time) do |value|
|
49
|
+
conform_with(Time.at(value.to_r / 1000).utc) if value.is_a?(Integer)
|
50
|
+
end
|
51
|
+
|
52
|
+
schema = Definition.Keys do
|
53
|
+
required :title, Definition.Type(String)
|
54
|
+
required :body, Definition.Type(String)
|
55
|
+
optional :publication_date, milliseconds_time_definition
|
56
|
+
end
|
57
|
+
|
58
|
+
conform_result = schema.conform({title: "My first blog post", body: "Shortest one ever!", publication_date: 1546170180339})
|
59
|
+
conform_result.passed? # => true
|
60
|
+
conform_result.value # => {title: "My first blog post", body: "Shortest one ever!", publication_date: 2018-12-30 11:43:00 UTC}
|
61
|
+
```
|
62
|
+
|
63
|
+
Because definitions do not only validate input but also transform input, we use
|
64
|
+
the term `conform` which stands for validation and coercion.
|
65
|
+
|
66
|
+
### Conforming Hashes
|
67
|
+
|
68
|
+
Hashes can be conformed by using the `Keys` definition. It allows you to configure
|
69
|
+
required and optional attributes. The first argument of `required` and `optional`
|
70
|
+
takes either Symbols or Strings. If you use a Symbol, then the validated Hash
|
71
|
+
needs to have a Symbol key with that name, otherwise a string key.
|
72
|
+
|
73
|
+
The key definition will also fail if the input value contains extra keys.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
Definition.Keys do
|
77
|
+
required :title, Definition.Type(String)
|
78
|
+
optional :publication_date, Definition.Type(Date)
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### Validating types
|
83
|
+
|
84
|
+
This will validate that the value is of the specified type.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
Definition.Type(String)
|
88
|
+
Definition.Type(Float)
|
89
|
+
Definition.Type(MyClass)
|
90
|
+
|
91
|
+
Definition.Type(MyClass).conform(0.1).passed? # => false
|
92
|
+
Definition.Type(MyClass).conform(MyClass.new).passed? # => true
|
93
|
+
```
|
94
|
+
|
95
|
+
### Conforming types
|
96
|
+
|
97
|
+
This will validate that the value is of the specified type. But if its not it will
|
98
|
+
try to coerce it into that type. This Definition works only with primitive types.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
Definition.CoercibleType(String) # Uses String() to coerce values
|
102
|
+
Definition.CoercibleType(Float) # Uses Float() to coerce values
|
103
|
+
|
104
|
+
Definition.CoercibleType(Float).conform("0.1").passed? # => true
|
105
|
+
Definition.CoercibleType(Float).conform("0.1").value # => 0.1
|
106
|
+
```
|
107
|
+
|
108
|
+
### Combining multiple definitions with "And"
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
Definition.And(definition1, definition2)
|
112
|
+
```
|
113
|
+
|
114
|
+
This definition will only conform if all definitions conform. The definitions will
|
115
|
+
be processed from left to right and the output of the previous will be the input
|
116
|
+
of the next.
|
117
|
+
|
118
|
+
### Combining multiple definitions with "Or"
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
Definition.Or(definition1, definition2)
|
122
|
+
```
|
123
|
+
|
124
|
+
This definition will conform if at least one definition conforms. The definitions will
|
125
|
+
be processed from left to right and stop as soon as a definition conforms. The output
|
126
|
+
of that definition will be the output of the Or definition.
|
127
|
+
|
128
|
+
### Conforming array values with "Each"
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
Definition.Each(item_definition)
|
132
|
+
|
133
|
+
Definition.Each(Definition.Type(Integer)).conform([1,2,3,"4"]).error_message
|
134
|
+
# => Not all items conform with each: { Item "4" did not conform to each: { Is of type String instead of Integer } }
|
135
|
+
```
|
136
|
+
|
137
|
+
This definition will only conform if all elements of the value conform to the
|
138
|
+
`item_definition`.
|
139
|
+
|
140
|
+
### Conforming with custom lambda functions
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
Definition.Lambda(:password) do |value|
|
144
|
+
matches = Regexp.new(/^
|
145
|
+
(?=.*[a-z]) # should contain at least one lower case letter
|
146
|
+
(?=.*[A-Z]) # should contain at least one upper case letter
|
147
|
+
(?=.*\d) # should contain at least one digit
|
148
|
+
.{6,50} # should be between 6 and 50 characters long
|
149
|
+
$/x).match(value.to_s)
|
150
|
+
conform_with(value) if matches
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
This definition can be used to build any custom validation or coercion you want.
|
155
|
+
The example above makes sure that a password conforms with a set of rules.
|
156
|
+
|
157
|
+
The block gets the input value as argument and you can do any transformation or
|
158
|
+
validation on it that you want. If you determine that the value is valid, then
|
159
|
+
you must call `conform_with` and pass it the value you want to return. This can
|
160
|
+
either be the original value or any transformed version of it. By not calling
|
161
|
+
`conform_with` you tell the definition to fail for the current input value.
|
162
|
+
|
163
|
+
The first argument of `Definition.Lambda` is a name you can give this definition.
|
164
|
+
It will only be used in the error message to make it more readable.
|
165
|
+
|
166
|
+
### Composing Definitions
|
167
|
+
|
168
|
+
Definitions are reusable and can be easily composed:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
country_code_definition = Definition.Lambda(:iso_county_code) do |value|
|
172
|
+
if iso_code = IsoCountryCodes.find(value)
|
173
|
+
conform_with(iso_code.alpha2)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
address_definition = Definition.Keys do
|
178
|
+
required :street, Definition.Type(String)
|
179
|
+
required :postal_code, Definition.Type(String)
|
180
|
+
required :country_code, country_code_definition
|
181
|
+
end
|
182
|
+
|
183
|
+
order = Definition.Keys do
|
184
|
+
required :user, user_definition
|
185
|
+
required :invoice_address, address_definition
|
186
|
+
required :shipping_address, address_definition
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
### Examples
|
191
|
+
|
192
|
+
Check out the [integration specs](./spec/integration) for more usage examples.
|
193
|
+
|
194
|
+
## Development
|
195
|
+
|
196
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
197
|
+
|
198
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
199
|
+
|
200
|
+
## Contributing
|
201
|
+
|
202
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Goltergaul/definition.
|
203
|
+
|
204
|
+
[travis]: https://travis-ci.org/Goltergaul/definition
|
205
|
+
[rubygems]: https://rubygems.org/gems/definition
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
task default: :spec
|
9
|
+
|
10
|
+
task :init do
|
11
|
+
Rake::Task["rubocop:install"].execute
|
12
|
+
end
|
13
|
+
|
14
|
+
require "rubocop/rake_task"
|
15
|
+
RuboCop::RakeTask.new
|
16
|
+
namespace :rubocop do
|
17
|
+
desc "Install Rubocop as pre-commit hook"
|
18
|
+
task :install do
|
19
|
+
require "rubocop_runner"
|
20
|
+
RubocopRunner.install
|
21
|
+
end
|
22
|
+
end
|
data/definition.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "definition/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "definition"
|
9
|
+
spec.version = Definition::VERSION
|
10
|
+
spec.authors = ["Dominik Goltermann"]
|
11
|
+
spec.email = ["dominik@goltermann.cc"]
|
12
|
+
|
13
|
+
spec.summary = "Simple and composable validation and coercion of data structures inspired by clojure specs"
|
14
|
+
spec.homepage = "https://github.com/Goltergaul/definition"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "approvals", "~> 0.0"
|
25
|
+
spec.add_development_dependency "awesome_print"
|
26
|
+
spec.add_development_dependency "benchmark-ips"
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
28
|
+
spec.add_development_dependency "fuubar"
|
29
|
+
spec.add_development_dependency "guard"
|
30
|
+
spec.add_development_dependency "guard-rspec"
|
31
|
+
spec.add_development_dependency "pry"
|
32
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
34
|
+
spec.add_development_dependency "rspec-its", "~> 1.2"
|
35
|
+
spec.add_development_dependency "rubocop"
|
36
|
+
spec.add_development_dependency "rubocop-rspec"
|
37
|
+
spec.add_development_dependency "rubocop_runner"
|
38
|
+
spec.add_development_dependency "timecop"
|
39
|
+
end
|