definition 0.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/Goltergaul/definition.svg?branch=master)][travis]
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/definition.svg)][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
|