dao 3.3.0 → 4.2.1
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.
- 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
|
+
}
|