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 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
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :test do
4
+ gem 'cutest', '~> 1.2'
5
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,15 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ clap (1.0.0)
5
+ cutest (1.2.3)
6
+ clap
7
+
8
+ PLATFORMS
9
+ ruby
10
+
11
+ DEPENDENCIES
12
+ cutest (~> 1.2)
13
+
14
+ BUNDLED WITH
15
+ 1.15.4
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
@@ -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: []