dao 3.3.0 → 4.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +7 -0
- data/Rakefile +36 -17
- data/b.rb +38 -0
- data/dao.gemspec +41 -13
- data/lib/dao.rb +44 -13
- data/lib/dao/api.rb +1 -1
- data/lib/dao/api/context.rb +35 -45
- data/lib/dao/api/endpoints.rb +225 -91
- data/lib/dao/conducer.rb +437 -0
- data/lib/dao/conducer/attributes.rb +21 -0
- data/lib/dao/conducer/crud.rb +70 -0
- data/lib/dao/current.rb +66 -0
- data/lib/dao/db.rb +44 -5
- data/lib/dao/endpoint.rb +13 -1
- data/lib/dao/errors.rb +74 -59
- data/lib/dao/exceptions.rb +1 -2
- data/lib/dao/extractor.rb +68 -0
- data/lib/dao/form.rb +139 -46
- data/lib/dao/image_cache.rb +193 -0
- data/lib/dao/instance_exec.rb +1 -1
- data/lib/dao/name.rb +7 -0
- data/lib/dao/params.rb +16 -66
- data/lib/dao/rack.rb +3 -0
- data/lib/dao/rack/middleware.rb +5 -0
- data/lib/dao/rack/middleware/params_parser.rb +24 -0
- data/lib/dao/rails.rb +22 -5
- data/lib/dao/rails/lib/generators/dao/USAGE +2 -6
- data/lib/dao/rails/lib/generators/dao/dao_generator.rb +52 -7
- data/lib/dao/rails/lib/generators/dao/templates/api.rb +23 -7
- data/lib/dao/rails/lib/generators/dao/templates/api_controller.rb +24 -7
- data/lib/dao/rails/lib/generators/dao/templates/conducer.rb +64 -0
- data/lib/dao/rails/lib/generators/dao/templates/conducer_controller.rb +79 -0
- data/lib/dao/rails/lib/generators/dao/templates/dao.js +13 -6
- data/lib/dao/rails/lib/generators/dao/templates/dao_helper.rb +75 -11
- data/lib/dao/result.rb +1 -26
- data/lib/dao/slug.rb +37 -8
- data/lib/dao/status.rb +4 -0
- data/lib/dao/support.rb +155 -0
- data/lib/dao/validations.rb +48 -157
- data/lib/dao/validations/callback.rb +30 -0
- data/lib/dao/validations/common.rb +322 -320
- data/lib/dao/validations/validator.rb +219 -0
- data/test/active_model_conducer_lint_test.rb +19 -0
- data/test/api_test.rb +261 -0
- data/test/conducer_test.rb +205 -0
- data/test/db.yml +9 -0
- data/test/form_test.rb +42 -0
- data/test/support_test.rb +52 -0
- data/test/testing.rb +145 -24
- data/test/validations_test.rb +156 -0
- metadata +138 -21
- data/TODO +0 -33
- data/a.rb +0 -80
- data/db/dao.yml +0 -5
- data/lib/dao/api/interfaces.rb +0 -306
- data/lib/dao/interface.rb +0 -28
- data/lib/dao/presenter.rb +0 -129
- data/lib/dao/rails/lib/generators/dao/api_generator.rb +0 -3
- data/lib/dao/validations/base.rb +0 -68
- data/test/dao_test.rb +0 -506
@@ -0,0 +1,219 @@
|
|
1
|
+
module Dao
|
2
|
+
module Validations
|
3
|
+
class Validator
|
4
|
+
NotBlank = lambda{|value| !value.to_s.strip.empty?} unless defined?(NotBlank)
|
5
|
+
Cleared = 'Cleared'.freeze unless defined?(Cleared)
|
6
|
+
|
7
|
+
class << Validator
|
8
|
+
def for(*args, &block)
|
9
|
+
new(*args, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :object
|
14
|
+
attr_accessor :validations
|
15
|
+
attr_accessor :errors
|
16
|
+
attr_accessor :status
|
17
|
+
|
18
|
+
def initialize(object)
|
19
|
+
@object = object
|
20
|
+
@validations = Map.new
|
21
|
+
@errors = Errors.new
|
22
|
+
@status = Status.new
|
23
|
+
end
|
24
|
+
|
25
|
+
fattr(:attributes) do
|
26
|
+
attributes =
|
27
|
+
catch(:attributes) do
|
28
|
+
if @object.respond_to?(:attributes)
|
29
|
+
throw :attributes, @object.attributes
|
30
|
+
end
|
31
|
+
if @object.instance_variable_defined?('@attributes')
|
32
|
+
throw :attributes, @object.instance_variable_get('@attributes')
|
33
|
+
end
|
34
|
+
if @object.is_a?(Map)
|
35
|
+
throw :attributes, @object
|
36
|
+
end
|
37
|
+
if @object.respond_to?(:to_map)
|
38
|
+
throw :attributes, Map.new(@object.to_map)
|
39
|
+
end
|
40
|
+
if @object.is_a?(Hash)
|
41
|
+
throw :attributes, Map.new(@object)
|
42
|
+
end
|
43
|
+
if @object.respond_to?(:to_hash)
|
44
|
+
throw :attributes, Map.new(@object.to_hash)
|
45
|
+
end
|
46
|
+
raise ArgumentError.new("found no attributes on #{ @object.inspect }(#{ @object.class.name })")
|
47
|
+
end
|
48
|
+
|
49
|
+
case attributes
|
50
|
+
when Map
|
51
|
+
attributes
|
52
|
+
when Hash
|
53
|
+
Map.new(attributes)
|
54
|
+
else
|
55
|
+
raise(ArgumentError.new("#{ attributes.inspect } (#{ attributes.class })"))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add(*args, &block)
|
60
|
+
options = Map.options_for!(args)
|
61
|
+
block = args.pop if args.last.respond_to?(:call)
|
62
|
+
block ||= NotBlank
|
63
|
+
callback = Callback.new(options, &block)
|
64
|
+
validations.set(args => Callback::Chain.new) unless validations.has?(args)
|
65
|
+
validations.get(args).add(callback)
|
66
|
+
callback
|
67
|
+
end
|
68
|
+
alias_method('validates', 'add')
|
69
|
+
|
70
|
+
def run_validations!(*args)
|
71
|
+
run_validations(*args)
|
72
|
+
ensure
|
73
|
+
validated!
|
74
|
+
end
|
75
|
+
|
76
|
+
def validations_search_path
|
77
|
+
@validations_search_path ||= (
|
78
|
+
list = [
|
79
|
+
object,
|
80
|
+
object.class.ancestors.map{|ancestor| ancestor.respond_to?(:validator) ? ancestor : nil}
|
81
|
+
]
|
82
|
+
list.flatten!
|
83
|
+
list.compact!
|
84
|
+
list.reverse!
|
85
|
+
list
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def validations_list
|
90
|
+
validations_search_path.map{|object| object.validator.validations}
|
91
|
+
end
|
92
|
+
|
93
|
+
def run_validations(*args)
|
94
|
+
object = args.first || @object
|
95
|
+
|
96
|
+
attributes.extend(InstanceExec) unless attributes.respond_to?(:instance_exec)
|
97
|
+
|
98
|
+
previous_errors = []
|
99
|
+
new_errors = []
|
100
|
+
|
101
|
+
errors.each_message do |keys, message|
|
102
|
+
previous_errors.push([keys, message])
|
103
|
+
end
|
104
|
+
errors.clear!
|
105
|
+
status.ok!
|
106
|
+
|
107
|
+
list = validations_list
|
108
|
+
|
109
|
+
list.each do |validations|
|
110
|
+
validations.depth_first_each do |keys, chain|
|
111
|
+
chain.each do |callback|
|
112
|
+
next unless callback and callback.respond_to?(:to_proc)
|
113
|
+
|
114
|
+
number_of_errors = errors.size
|
115
|
+
value = attributes.get(keys)
|
116
|
+
|
117
|
+
returned =
|
118
|
+
catch(:validation) do
|
119
|
+
args = [value, attributes].slice(0, callback.arity)
|
120
|
+
attributes.instance_exec(*args, &callback)
|
121
|
+
end
|
122
|
+
|
123
|
+
case returned
|
124
|
+
when Hash
|
125
|
+
map = Map(returned)
|
126
|
+
valid = map[:valid]
|
127
|
+
message = map[:message]
|
128
|
+
|
129
|
+
when TrueClass, FalseClass
|
130
|
+
valid = returned
|
131
|
+
message = nil
|
132
|
+
|
133
|
+
else
|
134
|
+
any_errors_added = errors.size > number_of_errors
|
135
|
+
valid = !any_errors_added
|
136
|
+
message = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
message ||= callback.options[:message]
|
140
|
+
message ||= (value.to_s.strip.empty? ? 'is blank' : 'is invalid')
|
141
|
+
|
142
|
+
unless valid
|
143
|
+
new_errors.push([keys, message])
|
144
|
+
else
|
145
|
+
new_errors.push([keys, Cleared])
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
previous_errors.each do |keys, message|
|
152
|
+
errors.add(keys, message) unless new_errors.assoc(keys)
|
153
|
+
end
|
154
|
+
|
155
|
+
new_errors.each do |keys, value|
|
156
|
+
next if value == Cleared
|
157
|
+
message = value
|
158
|
+
errors.add(keys, message)
|
159
|
+
end
|
160
|
+
|
161
|
+
if status.ok? and !errors.empty?
|
162
|
+
status.update(412)
|
163
|
+
end
|
164
|
+
|
165
|
+
errors
|
166
|
+
end
|
167
|
+
|
168
|
+
def validated?
|
169
|
+
@validated = false unless defined?(@validated)
|
170
|
+
@validated
|
171
|
+
end
|
172
|
+
|
173
|
+
def validated!(boolean = true)
|
174
|
+
@validated = !!boolean
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate
|
178
|
+
run_validations
|
179
|
+
end
|
180
|
+
|
181
|
+
def validate!
|
182
|
+
raise Error.new("#{ object.class.name } is invalid!") unless valid?
|
183
|
+
object
|
184
|
+
end
|
185
|
+
|
186
|
+
def valid!
|
187
|
+
@forcing_validity = true
|
188
|
+
end
|
189
|
+
|
190
|
+
def forcing_validity?
|
191
|
+
defined?(@forcing_validity) and @forcing_validity
|
192
|
+
end
|
193
|
+
|
194
|
+
def forcing_validity!(boolean = true)
|
195
|
+
@forcing_validity = !!boolean
|
196
|
+
end
|
197
|
+
|
198
|
+
def valid?(*args)
|
199
|
+
if forcing_validity?
|
200
|
+
true
|
201
|
+
else
|
202
|
+
options = Map.options_for!(args)
|
203
|
+
validate #if(options[:validate] or !validated?)
|
204
|
+
errors.empty? and status.ok?
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def reset
|
209
|
+
errors.clear!
|
210
|
+
status.update(:ok)
|
211
|
+
forcing_validity!(false)
|
212
|
+
validated!(false)
|
213
|
+
self
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
Validator = Validations::Validator
|
219
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class LintTest < ActiveModel::TestCase
|
2
|
+
include ActiveModel::Lint::Tests
|
3
|
+
|
4
|
+
class LintConducer < Dao::Conducer; end
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@model = LintConducer.new
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
BEGIN {
|
13
|
+
testdir = File.dirname(File.expand_path(__FILE__))
|
14
|
+
rootdir = File.dirname(testdir)
|
15
|
+
libdir = File.join(rootdir, 'lib')
|
16
|
+
|
17
|
+
require File.join(libdir, 'dao')
|
18
|
+
require File.join(testdir, 'testing')
|
19
|
+
}
|
data/test/api_test.rb
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
Testing Dao do
|
4
|
+
## api
|
5
|
+
#
|
6
|
+
testing 'that an api class for your application can be built using a simple dsl' do
|
7
|
+
assert{
|
8
|
+
api_class =
|
9
|
+
Dao.api do
|
10
|
+
### dsl
|
11
|
+
end
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
testing 'that apis can have callable endpoints added to them which accept params and return results' do
|
16
|
+
captured = []
|
17
|
+
|
18
|
+
api_class =
|
19
|
+
assert{
|
20
|
+
Dao.api do
|
21
|
+
endpoint(:foo) do |params, result|
|
22
|
+
captured.push(params, result)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}
|
26
|
+
api = assert{ api_class.new }
|
27
|
+
result = assert{ api.call(:foo, {}) }
|
28
|
+
assert{ result.is_a?(Hash) }
|
29
|
+
end
|
30
|
+
|
31
|
+
testing 'that endpoints are automatically called according to arity' do
|
32
|
+
api = assert{ Class.new(Dao.api) }
|
33
|
+
assert{ api.class_eval{ endpoint(:zero){|| result.update :args => [] } } }
|
34
|
+
assert{ api.class_eval{ endpoint(:one){|a| result.update :args => [a]} } }
|
35
|
+
assert{ api.class_eval{ endpoint(:two){|a,b| result.update :args => [a,b]} } }
|
36
|
+
|
37
|
+
assert{ api.new.call(:zero).args.size == 0 }
|
38
|
+
assert{ api.new.call(:one).args.size == 1 }
|
39
|
+
assert{ api.new.call(:two).args.size == 2 }
|
40
|
+
end
|
41
|
+
|
42
|
+
testing 'that endpoints have an auto-vivifying params/result' do
|
43
|
+
api = assert{ Class.new(Dao.api) }
|
44
|
+
assert{ api.class_eval{ endpoint(:foo){ params; result; } } }
|
45
|
+
result = assert{ api.new.call(:foo) }
|
46
|
+
assert{ result.path.to_s =~ /foo/ }
|
47
|
+
end
|
48
|
+
|
49
|
+
testing 'that an api can be called with different modes' do
|
50
|
+
api_class =
|
51
|
+
assert{
|
52
|
+
Dao.api do
|
53
|
+
call(:foo) do
|
54
|
+
data.modes = []
|
55
|
+
|
56
|
+
Dao::Mode.list.each do |mode|
|
57
|
+
send(mode){ data.modes.push(mode) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
}
|
62
|
+
api = assert{ api_class.new }
|
63
|
+
|
64
|
+
Dao::Mode.list.each do |mode|
|
65
|
+
result = api.mode(mode).call(:foo)
|
66
|
+
assert{ result.data.modes.include?(mode) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
testing 'that options/head/get are considered read modes' do
|
71
|
+
read_mode = assert{ Dao::Mode.read }
|
72
|
+
|
73
|
+
api_class =
|
74
|
+
assert{
|
75
|
+
Dao.api do
|
76
|
+
call(:foo) do
|
77
|
+
data.update :modes => []
|
78
|
+
read { data.modes.push(read_mode) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
}
|
82
|
+
api = assert{ api_class.new }
|
83
|
+
|
84
|
+
Dao::Mode::Read.each do |mode|
|
85
|
+
result = assert{ api.mode(mode).call(:foo) }
|
86
|
+
assert{ result.data.modes == [read_mode] }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
testing 'that post/put/delete/trace/connect are considered write modes' do
|
91
|
+
write_mode = assert{ Dao::Mode.write }
|
92
|
+
|
93
|
+
api_class =
|
94
|
+
assert{
|
95
|
+
Dao.api do
|
96
|
+
call(:foo) do
|
97
|
+
data.update :modes => []
|
98
|
+
write { data.modes.push(write_mode) }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
}
|
102
|
+
api = assert{ api_class.new }
|
103
|
+
|
104
|
+
Dao::Mode::Write.each do |mode|
|
105
|
+
result = assert{ api.mode(mode).call(:foo) }
|
106
|
+
assert{ result.data.modes == [write_mode] }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
testing 'that the first, most specific, mode block encountered fires first' do
|
111
|
+
api_class =
|
112
|
+
assert{
|
113
|
+
Dao.api do
|
114
|
+
call(:foo) do
|
115
|
+
data.update :modes => []
|
116
|
+
Dao::Mode::Read.each do |mode|
|
117
|
+
send(mode){ data.modes.push(mode) }
|
118
|
+
end
|
119
|
+
read { data.modes.push(Dao::Mode.read) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
}
|
123
|
+
api = assert{ api_class.new }
|
124
|
+
|
125
|
+
read = Dao::Mode.read
|
126
|
+
result = assert{ api.mode(read).call(:foo) }
|
127
|
+
assert{ result.data.modes == [read] }
|
128
|
+
|
129
|
+
Dao::Mode::Read.each do |mode|
|
130
|
+
result = assert{ api.mode(mode).call(:foo) }
|
131
|
+
assert{ result.data.modes == [mode] }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
## results
|
136
|
+
#
|
137
|
+
testing 'that results can be created' do
|
138
|
+
result = assert{ Dao::Result.new }
|
139
|
+
assert{ result.path }
|
140
|
+
assert{ result.status }
|
141
|
+
assert{ result.errors }
|
142
|
+
assert{ result.params }
|
143
|
+
assert{ result.data }
|
144
|
+
end
|
145
|
+
|
146
|
+
testing 'that results can be created with a path' do
|
147
|
+
result = assert{ Dao::Result.new('/api/foo/bar') }
|
148
|
+
assert{ result.path == '/api/foo/bar' }
|
149
|
+
end
|
150
|
+
|
151
|
+
## paths
|
152
|
+
#
|
153
|
+
testing 'that simple paths can be contstructed/compiled' do
|
154
|
+
path = assert{ Dao::Path.for('./api/../foo/bar') }
|
155
|
+
assert{ path =~ %r|^/| }
|
156
|
+
assert{ path !~ %r|[.]| }
|
157
|
+
assert{ path.params.is_a?(Hash) }
|
158
|
+
assert{ path.keys.is_a?(Array) }
|
159
|
+
assert{ path.pattern.is_a?(Regexp) }
|
160
|
+
end
|
161
|
+
|
162
|
+
## routes
|
163
|
+
#
|
164
|
+
testing 'that an api has a list of routes' do
|
165
|
+
api_class =
|
166
|
+
assert{
|
167
|
+
Dao.api do
|
168
|
+
end
|
169
|
+
}
|
170
|
+
assert{ api_class.routes.is_a?(Array) }
|
171
|
+
end
|
172
|
+
|
173
|
+
testing 'that routed endpoints call be declared' do
|
174
|
+
api_class =
|
175
|
+
assert{
|
176
|
+
Dao.api do
|
177
|
+
call('/users/:user_id/comments/:comment_id') do
|
178
|
+
data.update(params)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
}
|
182
|
+
api = api_class.new
|
183
|
+
end
|
184
|
+
|
185
|
+
testing 'that routed methods can be called with embedded params' do
|
186
|
+
api_class =
|
187
|
+
assert{
|
188
|
+
Dao.api do
|
189
|
+
call('/users/:user_id/comments/:comment_id') do
|
190
|
+
data.update(params)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
}
|
194
|
+
api = api_class.new
|
195
|
+
|
196
|
+
{
|
197
|
+
'/users/4/comments/2' => {},
|
198
|
+
'/users/:user_id/comments/:comment_id' => {:user_id => 4, :comment_id => 2},
|
199
|
+
}.each do |path, params|
|
200
|
+
result = assert{ api.call(path, params) }
|
201
|
+
assert{ result.data.user_id.to_s =~ /4/ }
|
202
|
+
assert{ result.data.comment_id.to_s =~ /2/ }
|
203
|
+
assert{ result.path == '/users/4/comments/2' }
|
204
|
+
assert{ result.route == '/users/:user_id/comments/:comment_id' }
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
## doc
|
209
|
+
#
|
210
|
+
testing 'that apis can be documented via the api' do
|
211
|
+
api_class =
|
212
|
+
assert {
|
213
|
+
Dao.api {
|
214
|
+
description 'foobar'
|
215
|
+
doc 'signature' => {'read' => '...', 'write' => '...'}
|
216
|
+
endpoint('/barfoo'){}
|
217
|
+
}
|
218
|
+
}
|
219
|
+
api_class_index = assert{ api_class.index.is_a?(Hash) }
|
220
|
+
api = assert{ api_class.new }
|
221
|
+
api_index = assert{ api.index.is_a?(Hash) }
|
222
|
+
assert{ api_class_index==api_index }
|
223
|
+
end
|
224
|
+
|
225
|
+
# aliases
|
226
|
+
#
|
227
|
+
testing 'that apis can alias methods' do
|
228
|
+
api_class =
|
229
|
+
assert {
|
230
|
+
Dao.api {
|
231
|
+
call('/barfoo'){ data.update(:k => :v) }
|
232
|
+
call('/foobar', :alias => '/barfoo')
|
233
|
+
}
|
234
|
+
}
|
235
|
+
api = assert{ api_class.new }
|
236
|
+
assert{ api.call('/barfoo').data.k == :v }
|
237
|
+
assert{ api.call('/foobar').data.k == :v }
|
238
|
+
end
|
239
|
+
|
240
|
+
protected
|
241
|
+
def hash_equal(a, b)
|
242
|
+
array = lambda{|h| h.to_a.map{|k,v| [k.to_s, v]}.sort}
|
243
|
+
array[a] == array[b]
|
244
|
+
end
|
245
|
+
|
246
|
+
def api(&block)
|
247
|
+
api_class = assert{ Dao.api(&block) }
|
248
|
+
api = assert{ api_class.new }
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
BEGIN {
|
254
|
+
testdir = File.dirname(File.expand_path(__FILE__))
|
255
|
+
rootdir = File.dirname(testdir)
|
256
|
+
libdir = File.join(rootdir, 'lib')
|
257
|
+
|
258
|
+
require File.join(libdir, 'dao')
|
259
|
+
require File.join(testdir, 'testing')
|
260
|
+
require File.join(testdir, 'helper')
|
261
|
+
}
|