robert 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 +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +15 -0
- data/LICENSE +21 -0
- data/README.md +91 -0
- data/Rakefile +20 -0
- data/bin/gs +74 -0
- data/lib/robert.rb +92 -0
- data/robert.gemspec +14 -0
- data/test/robert_test.rb +160 -0
- metadata +68 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f0ca595b26c5a3f3e3b633cb9bee8bade4944d5c
|
4
|
+
data.tar.gz: e84ab645d934bb08b3acd16e4ca977b600faf29d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5318c53a44551c098e1b9ed069192a089f52a41af039b0e2d252c91d9a37854e00ecb00ffd0186b7a64da3af056c4e842b357d9bd1370b5ea5074ebe7b7bf3ac
|
7
|
+
data.tar.gz: b289fcee70b6410a98dd690d8c002f6097cd266b0041ff90acfade14a9c1b8e70236ecbb4b45dea5251590b0a1c473059d92f4e44491f7f4515ba57e8a0ca160
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.gs/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Steven Weiss
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Robert
|
2
|
+
|
3
|
+
Robert is an implementation of the builder pattern that first runs the given
|
4
|
+
arguments through a validation process.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
`gem install robert`
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
Robert is helpful in creating form objects (validating Rack params),
|
13
|
+
implementing business logic (service objects, if you will) and much more.
|
14
|
+
Basically if you do not trust the data, run it through Robert.
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
class Post
|
18
|
+
attr_accessor :title, :body
|
19
|
+
|
20
|
+
def initialize(attributes)
|
21
|
+
attributes.each do |key, value|
|
22
|
+
send(:"#{key}=", value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class PostBuilder < Robert
|
28
|
+
attr_accessor :title, :body
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def validate
|
33
|
+
error :title, :is_empty { empty? }
|
34
|
+
error :body, :is_empty { empty? }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
builder = PostBuilder.new(title: 'Title', body: '')
|
39
|
+
|
40
|
+
result = builder.build(Post)
|
41
|
+
|
42
|
+
result # false
|
43
|
+
|
44
|
+
builder.errors # { body: [:is_empty] }
|
45
|
+
|
46
|
+
builder.body = 'body'
|
47
|
+
|
48
|
+
result = builder.build(Post)
|
49
|
+
|
50
|
+
result.is_a?(Post) # true
|
51
|
+
```
|
52
|
+
|
53
|
+
Check out the tests for more examples.
|
54
|
+
|
55
|
+
## API
|
56
|
+
|
57
|
+
`::build` - Convenience for `::new(params).build(klass)`
|
58
|
+
|
59
|
+
`::build!` - Convenience for `::new(params).build!(klass)`
|
60
|
+
|
61
|
+
`#attributes` - Hash of attributes that have been validated (successfully) and "cleaned".
|
62
|
+
|
63
|
+
`#errors` - Hash of errors.
|
64
|
+
|
65
|
+
`#build` - Initializes the given class if valid or returns false.
|
66
|
+
|
67
|
+
`#build!` - Initializes the given class if valid or raises.
|
68
|
+
|
69
|
+
`#clean` - Template method for cleaning values before assigning them the attributes hash.
|
70
|
+
|
71
|
+
`#error` - Bread and butter of the library. For a given attribute, adds error if the given block returns false. The block is evaluated in context of the attribute itself.
|
72
|
+
|
73
|
+
`#validate` - Template method for implementing your validation logic.
|
74
|
+
|
75
|
+
`#valid?` - Checks to see if the builder is valid.
|
76
|
+
|
77
|
+
### Gotchas
|
78
|
+
|
79
|
+
Sending `#errors` before `#valid?` will return the default errors
|
80
|
+
(an empty Hash by default), which will give the appearance of a valid object.
|
81
|
+
The reason for this is that if you have an html view and are rendering
|
82
|
+
a form/builder's errors, you don't want to `#errors` to trigger validation
|
83
|
+
on the initial page load.
|
84
|
+
|
85
|
+
## Prior Art
|
86
|
+
|
87
|
+
Robert is inspired by the following libraries:
|
88
|
+
|
89
|
+
* [Scrivener](https://github.com/soveran/scrivener)
|
90
|
+
* [Hatch](https://github.com/tonchis/hatch)
|
91
|
+
* [Django's Forms](https://www.djangoproject.com)
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
desc 'open irb in gs context'
|
2
|
+
task :console do
|
3
|
+
sh './bin/gs irb -r ./app.rb'
|
4
|
+
end
|
5
|
+
|
6
|
+
task :default do
|
7
|
+
sh 'rake -T'
|
8
|
+
end
|
9
|
+
|
10
|
+
desc 'installs gems'
|
11
|
+
task :install do
|
12
|
+
sh 'mkdir -p .gs & ./bin/gs bundle install --system'
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'tests the given [test]_test.rb'
|
16
|
+
task :test, :name do |t, args|
|
17
|
+
name = args[:name] || "*"
|
18
|
+
|
19
|
+
sh "./bin/gs cutest -r ./lib/robert ./test/#{name}_test.rb"
|
20
|
+
end
|
data/bin/gs
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright (c) 2012 Michel Martens
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
#
|
23
|
+
# https://github.com/soveran/gs
|
24
|
+
|
25
|
+
help = <<EOS
|
26
|
+
GS(1)
|
27
|
+
|
28
|
+
NAME
|
29
|
+
gs -- Gemset management
|
30
|
+
|
31
|
+
SYNOPSIS
|
32
|
+
gs init
|
33
|
+
gs help
|
34
|
+
gs [command]
|
35
|
+
|
36
|
+
DESCRIPTION
|
37
|
+
When called with no arguments, it starts a shell session and
|
38
|
+
configures the variables GEM_HOME, GEM_PATH and PATH to point
|
39
|
+
to the $PWD/.gs directory. In addition, it sets the GS_NAME
|
40
|
+
variable with the name of the current gemset (useful for PS1).
|
41
|
+
|
42
|
+
init
|
43
|
+
Creates the $PWD/.gs directory.
|
44
|
+
|
45
|
+
help
|
46
|
+
Displays this message.
|
47
|
+
|
48
|
+
[command]
|
49
|
+
An optional command that will be executed under gs shell
|
50
|
+
session and return to the caller session once finished.
|
51
|
+
EOS
|
52
|
+
|
53
|
+
case ARGV[0]
|
54
|
+
when "init" then Dir.mkdir(".gs"); exec($0)
|
55
|
+
when "help" then puts help
|
56
|
+
else
|
57
|
+
if File.directory?(".gs")
|
58
|
+
pwd = Dir.pwd
|
59
|
+
env = ENV.to_hash
|
60
|
+
env["GS_NAME"] = File.basename(pwd)
|
61
|
+
env["GEM_HOME"] = pwd + "/.gs"
|
62
|
+
env["GEM_PATH"] = pwd + "/.gs:#{`gem env path`.strip}"
|
63
|
+
env["PATH"] = pwd + "/.gs/bin:#{ENV['PATH']}"
|
64
|
+
|
65
|
+
if ARGV.length > 0
|
66
|
+
exec env, *ARGV
|
67
|
+
else
|
68
|
+
exec env, ENV["SHELL"] || ENV["COMSPEC"]
|
69
|
+
end
|
70
|
+
else
|
71
|
+
puts "Directory .gs not found. Try `gs help`."
|
72
|
+
exit 1
|
73
|
+
end
|
74
|
+
end
|
data/lib/robert.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
class Robert
|
2
|
+
VERSION = '0.1.0'
|
3
|
+
|
4
|
+
class ValidationError < StandardError; end
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def build(klass, args = {})
|
8
|
+
new(args).build(klass)
|
9
|
+
end
|
10
|
+
|
11
|
+
def build!(klass, args = {})
|
12
|
+
new(args).build!(klass)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :attributes, :errors
|
17
|
+
|
18
|
+
def initialize(args = {})
|
19
|
+
args.each do |key, value|
|
20
|
+
writer = :"#{key}="
|
21
|
+
|
22
|
+
if respond_to?(writer)
|
23
|
+
send(writer, value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
reset
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_attributes
|
31
|
+
{}
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_errors
|
35
|
+
Hash.new { |h, k| h[k] = [] }
|
36
|
+
end
|
37
|
+
|
38
|
+
def build(klass)
|
39
|
+
valid? && klass.new(attributes)
|
40
|
+
end
|
41
|
+
|
42
|
+
def build!(klass)
|
43
|
+
build(klass) || raise(ValidationError, errors)
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid?
|
47
|
+
reset
|
48
|
+
|
49
|
+
validate
|
50
|
+
|
51
|
+
errors.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def add_attribute(key, value)
|
57
|
+
attributes[key] = clean(value) unless errors.has_key?(key)
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_error(key, error)
|
61
|
+
attributes.delete(key)
|
62
|
+
|
63
|
+
errors[key] << error
|
64
|
+
end
|
65
|
+
|
66
|
+
def clean(value)
|
67
|
+
value
|
68
|
+
end
|
69
|
+
|
70
|
+
def error(key, error, &condition)
|
71
|
+
value = send(key)
|
72
|
+
|
73
|
+
if value.instance_eval &condition
|
74
|
+
add_error(key, error)
|
75
|
+
|
76
|
+
true
|
77
|
+
else
|
78
|
+
add_attribute(key, value)
|
79
|
+
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def reset
|
85
|
+
@attributes = default_attributes
|
86
|
+
@errors = default_errors
|
87
|
+
end
|
88
|
+
|
89
|
+
def validate
|
90
|
+
raise NotImplementedError
|
91
|
+
end
|
92
|
+
end
|
data/robert.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative './lib/robert'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'robert'
|
5
|
+
s.summary = 'Robert'
|
6
|
+
s.version = Robert::VERSION
|
7
|
+
s.authors = ['Steve Weiss']
|
8
|
+
s.email = ['weissst@mail.gvsu.edu']
|
9
|
+
s.homepage = 'https://github.com/sirscriptalot/robert'
|
10
|
+
s.license = 'MIT'
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
|
13
|
+
s.add_development_dependency 'cutest', '~> 1.2'
|
14
|
+
end
|
data/test/robert_test.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
class Email
|
2
|
+
LENGTH = 5..255
|
3
|
+
|
4
|
+
attr_reader :email
|
5
|
+
|
6
|
+
def initialize(email)
|
7
|
+
@email = email
|
8
|
+
end
|
9
|
+
|
10
|
+
def length
|
11
|
+
email.length
|
12
|
+
end
|
13
|
+
|
14
|
+
def too_short?
|
15
|
+
length < LENGTH.min
|
16
|
+
end
|
17
|
+
|
18
|
+
def too_long?
|
19
|
+
length > LENGTH.max
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
email.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class User
|
28
|
+
attr_accessor :email
|
29
|
+
|
30
|
+
def initialize(attributes = {})
|
31
|
+
attributes.each do |key, value|
|
32
|
+
send(:"#{key}=", value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class UserBuilder < Robert
|
38
|
+
attr_writer :email
|
39
|
+
|
40
|
+
def email
|
41
|
+
Email.new(@email.to_s)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def clean(value)
|
47
|
+
value.to_s
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate
|
51
|
+
error :email, :too_short { too_short? } or
|
52
|
+
error :email, :too_long { too_long? }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
test '::build builds object when valid' do
|
57
|
+
params = { email: 'valid@example.com' }
|
58
|
+
|
59
|
+
assert UserBuilder.build(User, params).is_a?(User)
|
60
|
+
end
|
61
|
+
|
62
|
+
test '::build returns false when invalid' do
|
63
|
+
params = { email: '' }
|
64
|
+
|
65
|
+
assert !UserBuilder.build(User, params)
|
66
|
+
end
|
67
|
+
|
68
|
+
test '::build! builds object when valid' do
|
69
|
+
params = { email: 'valid@example.com' }
|
70
|
+
|
71
|
+
assert UserBuilder.build!(User, params).is_a?(User)
|
72
|
+
end
|
73
|
+
|
74
|
+
test '::build! raises when invalid' do
|
75
|
+
params = { email: '' }
|
76
|
+
|
77
|
+
assert_raise Robert::ValidationError do
|
78
|
+
UserBuilder.build!(User, params)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
test '#build builds object when valid' do
|
83
|
+
params = { email: 'valid@example.com' }
|
84
|
+
|
85
|
+
builder = UserBuilder.new(params)
|
86
|
+
|
87
|
+
assert builder.build(User).is_a?(User)
|
88
|
+
end
|
89
|
+
|
90
|
+
test '#build returns false when invalid' do
|
91
|
+
params = { email: '' }
|
92
|
+
|
93
|
+
builder = UserBuilder.new(params)
|
94
|
+
|
95
|
+
assert !builder.build(User)
|
96
|
+
end
|
97
|
+
|
98
|
+
test '#build! builds object when valid' do
|
99
|
+
params = { email: 'valid@example.com' }
|
100
|
+
|
101
|
+
builder = UserBuilder.new(params)
|
102
|
+
|
103
|
+
assert builder.build!(User).is_a?(User)
|
104
|
+
end
|
105
|
+
|
106
|
+
test '#build! raises when invalid' do
|
107
|
+
params = { email: '' }
|
108
|
+
|
109
|
+
builder = UserBuilder.new(params)
|
110
|
+
|
111
|
+
assert_raise Robert::ValidationError do
|
112
|
+
builder.build!(User)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
test '#valid? captures valid attributes' do
|
117
|
+
params = { email: 'valid@example.com' }
|
118
|
+
|
119
|
+
builder = UserBuilder.new(params)
|
120
|
+
|
121
|
+
assert !builder.attributes.include?(:email)
|
122
|
+
|
123
|
+
assert builder.valid?
|
124
|
+
|
125
|
+
assert builder.attributes.include?(:email)
|
126
|
+
|
127
|
+
builder.email = ''
|
128
|
+
|
129
|
+
assert !builder.valid?
|
130
|
+
|
131
|
+
assert !builder.attributes.include?(:email)
|
132
|
+
end
|
133
|
+
|
134
|
+
test '#valid? captured attributes are cleaned' do
|
135
|
+
params = { email: 'valid@example.com' }
|
136
|
+
|
137
|
+
builder = UserBuilder.new(params)
|
138
|
+
|
139
|
+
assert builder.valid?
|
140
|
+
|
141
|
+
assert builder.attributes.include?(:email)
|
142
|
+
|
143
|
+
assert builder.attributes[:email].is_a?(String)
|
144
|
+
|
145
|
+
assert !builder.attributes[:email].is_a?(Email)
|
146
|
+
end
|
147
|
+
|
148
|
+
test '#valid? adds errors for conditions' do
|
149
|
+
params = { email: '' }
|
150
|
+
|
151
|
+
builder = UserBuilder.new(params)
|
152
|
+
|
153
|
+
assert !builder.errors.has_key?(:email)
|
154
|
+
|
155
|
+
assert !builder.valid?
|
156
|
+
|
157
|
+
assert builder.errors.has_key?(:email)
|
158
|
+
|
159
|
+
assert builder.errors[:email].include?(:too_short)
|
160
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: robert
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Steve Weiss
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-12-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: cutest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- weissst@mail.gvsu.edu
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".gitignore"
|
35
|
+
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- LICENSE
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- bin/gs
|
41
|
+
- lib/robert.rb
|
42
|
+
- robert.gemspec
|
43
|
+
- test/robert_test.rb
|
44
|
+
homepage: https://github.com/sirscriptalot/robert
|
45
|
+
licenses:
|
46
|
+
- MIT
|
47
|
+
metadata: {}
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements: []
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 2.6.11
|
65
|
+
signing_key:
|
66
|
+
specification_version: 4
|
67
|
+
summary: Robert
|
68
|
+
test_files: []
|