autoforme 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +226 -0
- data/Rakefile +79 -0
- data/autoforme.js +77 -0
- data/lib/autoforme/action.rb +629 -0
- data/lib/autoforme/framework.rb +145 -0
- data/lib/autoforme/frameworks/rails.rb +80 -0
- data/lib/autoforme/frameworks/sinatra.rb +59 -0
- data/lib/autoforme/model.rb +377 -0
- data/lib/autoforme/models/sequel.rb +344 -0
- data/lib/autoforme/opts_attributes.rb +29 -0
- data/lib/autoforme/request.rb +53 -0
- data/lib/autoforme/table.rb +70 -0
- data/lib/autoforme/version.rb +9 -0
- data/lib/autoforme.rb +57 -0
- data/spec/associations_spec.rb +505 -0
- data/spec/basic_spec.rb +661 -0
- data/spec/mtm_spec.rb +333 -0
- data/spec/rails_spec_helper.rb +75 -0
- data/spec/sequel_spec_helper.rb +44 -0
- data/spec/sinatra_spec_helper.rb +53 -0
- data/spec/spec_helper.rb +51 -0
- data/spec/unit_spec.rb +449 -0
- metadata +129 -0
@@ -0,0 +1,344 @@
|
|
1
|
+
module AutoForme
|
2
|
+
module Models
|
3
|
+
# Sequel specific model class for AutoForme
|
4
|
+
class Sequel < Model
|
5
|
+
# Short reference to top level Sequel module, for easily calling methods
|
6
|
+
S = ::Sequel
|
7
|
+
|
8
|
+
# What association types to recognize. Other association types are ignored.
|
9
|
+
SUPPORTED_ASSOCIATION_TYPES = [:many_to_one, :one_to_one, :one_to_many, :many_to_many]
|
10
|
+
|
11
|
+
# Make sure the forme plugin is loaded into the model.
|
12
|
+
def initialize(*)
|
13
|
+
super
|
14
|
+
@model.plugin :forme
|
15
|
+
end
|
16
|
+
|
17
|
+
# The base class for the underlying model, ::Sequel::Model.
|
18
|
+
def base_class
|
19
|
+
S::Model
|
20
|
+
end
|
21
|
+
|
22
|
+
# A completely empty search object, with no defaults.
|
23
|
+
def new_search
|
24
|
+
@model.call({})
|
25
|
+
end
|
26
|
+
|
27
|
+
# The name of the form param for the given association.
|
28
|
+
def form_param_name(assoc)
|
29
|
+
"#{model.send(:underscore, model.name)}[#{association_key(assoc)}]"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set the fields for the given action type to the object based on the request params.
|
33
|
+
def set_fields(obj, type, request, params)
|
34
|
+
columns_for(type, request).each do |col|
|
35
|
+
column = col
|
36
|
+
|
37
|
+
if association?(col)
|
38
|
+
ref = model.association_reflection(col)
|
39
|
+
ds = ref.associated_dataset
|
40
|
+
if model_class = associated_model_class(col)
|
41
|
+
ds = model_class.apply_filter(:association, request, ds)
|
42
|
+
end
|
43
|
+
|
44
|
+
if v = params[ref[:key]]
|
45
|
+
v = ds.first!(S.qualify(ds.model.table_name, ref.primary_key)=>v)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
v = params[col]
|
49
|
+
end
|
50
|
+
|
51
|
+
obj.send("#{column}=", v)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Whether the column represents an association.
|
56
|
+
def association?(column)
|
57
|
+
case column
|
58
|
+
when String
|
59
|
+
model.associations.map{|x| x.to_s}.include?(column)
|
60
|
+
else
|
61
|
+
model.association_reflection(column)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# The associated class for the given association
|
66
|
+
def associated_class(assoc)
|
67
|
+
model.association_reflection(assoc).associated_class
|
68
|
+
end
|
69
|
+
|
70
|
+
# A short type for the association, either :one for a
|
71
|
+
# singular association, :new for an association where
|
72
|
+
# you can create new objects, or :edit for association
|
73
|
+
# where you can add/remove members from the association.
|
74
|
+
def association_type(assoc)
|
75
|
+
case model.association_reflection(assoc)[:type]
|
76
|
+
when :many_to_one, :one_to_one
|
77
|
+
:one
|
78
|
+
when :one_to_many
|
79
|
+
:new
|
80
|
+
when :many_to_many
|
81
|
+
:edit
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# The foreign key column for the given many to one association.
|
86
|
+
def association_key(assoc)
|
87
|
+
model.association_reflection(assoc)[:key]
|
88
|
+
end
|
89
|
+
|
90
|
+
# An array of pairs mapping foreign keys in associated class
|
91
|
+
# to primary key value of current object
|
92
|
+
def associated_new_column_values(obj, assoc)
|
93
|
+
ref = model.association_reflection(assoc)
|
94
|
+
ref[:keys].zip(ref[:primary_keys].map{|k| obj.send(k)})
|
95
|
+
end
|
96
|
+
|
97
|
+
# Array of many to many association name strings.
|
98
|
+
def mtm_association_names
|
99
|
+
association_names([:many_to_many])
|
100
|
+
end
|
101
|
+
|
102
|
+
# Array of association name strings for given association types
|
103
|
+
def association_names(types=SUPPORTED_ASSOCIATION_TYPES)
|
104
|
+
model.all_association_reflections.select{|r| types.include?(r[:type])}.map{|r| r[:name]}.sort_by{|n| n.to_s}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Save the object, returning the object if successful, or nil if not.
|
108
|
+
def save(obj)
|
109
|
+
obj.raise_on_save_failure = false
|
110
|
+
obj.save
|
111
|
+
end
|
112
|
+
|
113
|
+
# The primary key value for the given object.
|
114
|
+
def primary_key_value(obj)
|
115
|
+
obj.pk
|
116
|
+
end
|
117
|
+
|
118
|
+
# The namespace for form parameter names for this model, needs to match
|
119
|
+
# the ones automatically used by Forme.
|
120
|
+
def params_name
|
121
|
+
@model.send(:underscore, @model.name)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Retrieve underlying model instance with matching primary key
|
125
|
+
def with_pk(type, request, pk)
|
126
|
+
dataset_for(type, request).with_pk!(pk)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Retrieve all matching rows for this model.
|
130
|
+
def all_rows_for(type, request)
|
131
|
+
all_dataset_for(type, request).all
|
132
|
+
end
|
133
|
+
|
134
|
+
# Return the default columns for this model
|
135
|
+
def default_columns
|
136
|
+
columns = model.columns - Array(model.primary_key)
|
137
|
+
model.all_association_reflections.each do |reflection|
|
138
|
+
next unless reflection[:type] == :many_to_one
|
139
|
+
if i = columns.index(reflection[:key])
|
140
|
+
columns[i] = reflection[:name]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
columns.sort_by{|s| s.to_s}
|
144
|
+
end
|
145
|
+
|
146
|
+
# Add a filter restricting access to only rows where the column name
|
147
|
+
# matching the session value. Also add a before_create hook that sets
|
148
|
+
# the column value to the session value.
|
149
|
+
def session_value(column)
|
150
|
+
filter do |ds, type, req|
|
151
|
+
ds.where(S.qualify(model.table_name, column)=>req.session[column])
|
152
|
+
end
|
153
|
+
before_create do |obj, req|
|
154
|
+
obj.send("#{column}=", req.session[column])
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Returning array of matching objects for the current search page using the given parameters.
|
159
|
+
def search_results(type, request)
|
160
|
+
params = request.params
|
161
|
+
ds = apply_associated_eager(:search, request, all_dataset_for(type, request))
|
162
|
+
columns_for(:search_form, request).each do |c|
|
163
|
+
if (v = params[c]) && !v.empty?
|
164
|
+
if association?(c)
|
165
|
+
ref = model.association_reflection(c)
|
166
|
+
ads = ref.associated_dataset
|
167
|
+
if model_class = associated_model_class(c)
|
168
|
+
ads = model_class.apply_filter(:association, request, ads)
|
169
|
+
end
|
170
|
+
primary_key = S.qualify(ref.associated_class.table_name, ref.primary_key)
|
171
|
+
ds = ds.where(ref[:key]=>ads.where(primary_key=>v).select(primary_key))
|
172
|
+
elsif column_type(c) == :string
|
173
|
+
ds = ds.where(S.ilike(c, "%#{ds.escape_like(v.to_s)}%"))
|
174
|
+
else
|
175
|
+
ds = ds.where(c=>v.to_s)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
paginate(type, request, ds)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Return array of matching objects for the current page.
|
183
|
+
def browse(type, request)
|
184
|
+
paginate(type, request, apply_associated_eager(:browse, request, all_dataset_for(type, request)))
|
185
|
+
end
|
186
|
+
|
187
|
+
# Do very simple pagination, by selecting one more object than necessary,
|
188
|
+
# and noting if there is a next page by seeing if more objects are returned than the limit.
|
189
|
+
def paginate(type, request, ds)
|
190
|
+
limit = limit_for(type, request)
|
191
|
+
offset = ((request.id.to_i||1)-1) * limit
|
192
|
+
objs = ds.limit(limit+1, (offset if offset > 0)).all
|
193
|
+
next_page = false
|
194
|
+
if objs.length > limit
|
195
|
+
next_page = true
|
196
|
+
objs.pop
|
197
|
+
end
|
198
|
+
[next_page, objs]
|
199
|
+
end
|
200
|
+
|
201
|
+
# On the browse/search results pages, in addition to eager loading based on the current model's eager
|
202
|
+
# loading config, also eager load based on the associated models config.
|
203
|
+
def apply_associated_eager(type, request, ds)
|
204
|
+
columns_for(type, request).each do |col|
|
205
|
+
if association?(col)
|
206
|
+
if model = associated_model_class(col)
|
207
|
+
eager = model.eager_for(:association, request) || model.eager_graph_for(:association, request)
|
208
|
+
ds = ds.eager(col=>eager)
|
209
|
+
else
|
210
|
+
ds = ds.eager(col)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
ds
|
215
|
+
end
|
216
|
+
|
217
|
+
# The schema type for the column
|
218
|
+
def column_type(column)
|
219
|
+
(sch = model.db_schema[column]) && sch[:type]
|
220
|
+
end
|
221
|
+
|
222
|
+
# Apply the model's filter to the given dataset
|
223
|
+
def apply_filter(type, request, ds)
|
224
|
+
if filter = filter_for
|
225
|
+
ds = filter.call(ds, type, request)
|
226
|
+
end
|
227
|
+
ds
|
228
|
+
end
|
229
|
+
|
230
|
+
# Apply the model's filter, eager, and order to the given dataset
|
231
|
+
def apply_dataset_options(type, request, ds)
|
232
|
+
ds = apply_filter(type, request, ds)
|
233
|
+
if order = order_for(type, request)
|
234
|
+
ds = ds.order(*order)
|
235
|
+
end
|
236
|
+
if eager = eager_for(type, request)
|
237
|
+
ds = ds.eager(eager)
|
238
|
+
end
|
239
|
+
if eager_graph = eager_graph_for(type, request)
|
240
|
+
ds = ds.eager_graph(eager_graph)
|
241
|
+
end
|
242
|
+
ds
|
243
|
+
end
|
244
|
+
|
245
|
+
# Whether to autocomplete for the given association.
|
246
|
+
def association_autocomplete?(assoc, request)
|
247
|
+
(c = associated_model_class(assoc)) && c.autocomplete_options_for(:association, request)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Return array of autocompletion strings for the request. Options:
|
251
|
+
# :type :: Action type symbol
|
252
|
+
# :request :: AutoForme::Request instance
|
253
|
+
# :association :: Association symbol
|
254
|
+
# :query :: Query string submitted by the user
|
255
|
+
# :exclude :: Primary key value of current model, excluding already associated values (used when
|
256
|
+
# editing many to many associations)
|
257
|
+
def autocomplete(opts={})
|
258
|
+
type, request, assoc, query, exclude = opts.values_at(:type, :request, :association, :query, :exclude)
|
259
|
+
if assoc
|
260
|
+
if exclude && association_type(assoc) == :edit
|
261
|
+
ref = model.association_reflection(assoc)
|
262
|
+
block = lambda do |ds|
|
263
|
+
ds.exclude(S.qualify(ref.associated_class.table_name, ref.right_primary_key)=>model.db.from(ref[:join_table]).where(ref[:left_key]=>exclude).select(ref[:right_key]))
|
264
|
+
end
|
265
|
+
end
|
266
|
+
return associated_model_class(assoc).autocomplete(opts.merge(:type=>:association, :association=>nil), &block)
|
267
|
+
end
|
268
|
+
opts = autocomplete_options_for(type, request)
|
269
|
+
callback_opts = {:type=>type, :request=>request, :query=>query}
|
270
|
+
ds = all_dataset_for(type, request)
|
271
|
+
ds = opts[:callback].call(ds, callback_opts) if opts[:callback]
|
272
|
+
display = opts[:display] || S.qualify(model.table_name, :name)
|
273
|
+
display = display.call(callback_opts) if display.respond_to?(:call)
|
274
|
+
limit = opts[:limit] || 10
|
275
|
+
limit = limit.call(callback_opts) if limit.respond_to?(:call)
|
276
|
+
opts[:filter] ||= lambda{|ds, opts| ds.where(S.ilike(display, "%#{ds.escape_like(query)}%"))}
|
277
|
+
ds = opts[:filter].call(ds, callback_opts)
|
278
|
+
ds = ds.select(S.join([S.qualify(model.table_name, model.primary_key), display], ' - ').as(:v)).
|
279
|
+
limit(limit)
|
280
|
+
ds = yield ds if block_given?
|
281
|
+
ds.map(:v)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Update the many to many association. add and remove should be arrays of primary key values
|
285
|
+
# of associated objects to add to the association.
|
286
|
+
def mtm_update(request, assoc, obj, add, remove)
|
287
|
+
ref = model.association_reflection(assoc)
|
288
|
+
assoc_class = associated_model_class(assoc)
|
289
|
+
ret = nil
|
290
|
+
model.db.transaction do
|
291
|
+
[[add, ref.add_method], [remove, ref.remove_method]].each do |ids, meth|
|
292
|
+
if ids
|
293
|
+
ids.each do |id|
|
294
|
+
next if id.to_s.empty?
|
295
|
+
ret = assoc_class ? assoc_class.with_pk(:association, request, id) : ref.associated_dataset.with_pk!(id)
|
296
|
+
obj.send(meth, ret)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
ret
|
302
|
+
end
|
303
|
+
|
304
|
+
# The currently associated many to many objects for the association
|
305
|
+
def associated_mtm_objects(request, assoc, obj)
|
306
|
+
ds = obj.send("#{assoc}_dataset")
|
307
|
+
if assoc_class = associated_model_class(assoc)
|
308
|
+
ds = assoc_class.apply_dataset_options(:association, request, ds)
|
309
|
+
end
|
310
|
+
ds
|
311
|
+
end
|
312
|
+
|
313
|
+
# All objects in the associated table that are not currently associated to the given object.
|
314
|
+
def unassociated_mtm_objects(request, assoc, obj)
|
315
|
+
ref = model.association_reflection(assoc)
|
316
|
+
assoc_class = associated_model_class(assoc)
|
317
|
+
lambda do |ds|
|
318
|
+
subquery = model.db.from(ref[:join_table]).
|
319
|
+
select(ref.qualified_right_key).
|
320
|
+
where(ref.qualified_left_key=>obj.pk)
|
321
|
+
ds = ds.exclude(S.qualify(ref.associated_class.table_name, model.primary_key)=>subquery)
|
322
|
+
ds = assoc_class.apply_dataset_options(:association, request, ds) if assoc_class
|
323
|
+
ds
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
private
|
328
|
+
|
329
|
+
def dataset_for(type, request)
|
330
|
+
ds = @model.dataset
|
331
|
+
if filter = filter_for
|
332
|
+
ds = filter.call(ds, type, request)
|
333
|
+
end
|
334
|
+
ds
|
335
|
+
end
|
336
|
+
|
337
|
+
def all_dataset_for(type, request)
|
338
|
+
apply_dataset_options(type, request, @model.dataset)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
register_model(:sequel, Models::Sequel)
|
344
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AutoForme
|
2
|
+
module OptsAttributes
|
3
|
+
# Setup methods for each given argument such that if the method is called with an argument or
|
4
|
+
# block, it sets the value of the related option to that argument or block. If called without
|
5
|
+
# an argument or block, it returns the stored option value.
|
6
|
+
def opts_attribute(*meths)
|
7
|
+
meths.each do |meth|
|
8
|
+
define_method(meth) do |*args, &block|
|
9
|
+
if block
|
10
|
+
if args.empty?
|
11
|
+
opts[meth] = block
|
12
|
+
else
|
13
|
+
raise ArgumentError, "No arguments allowed if passing a block"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
case args.length
|
18
|
+
when 0
|
19
|
+
opts[meth]
|
20
|
+
when 1
|
21
|
+
opts[meth] = args.first
|
22
|
+
else
|
23
|
+
raise ArgumentError, "Only 0-1 arguments allowed"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module AutoForme
|
2
|
+
# Request wraps a specific web request for a given framework.
|
3
|
+
class Request
|
4
|
+
# The underlying web framework request instance for the request
|
5
|
+
attr_reader :controller
|
6
|
+
|
7
|
+
# The request method (GET or POST) for the request
|
8
|
+
attr_reader :method
|
9
|
+
|
10
|
+
# A string representing the model for the request
|
11
|
+
attr_reader :model
|
12
|
+
|
13
|
+
# A string representing the action type for the request
|
14
|
+
attr_reader :action_type
|
15
|
+
|
16
|
+
# A string representing the path that the root of
|
17
|
+
# the application is mounted at
|
18
|
+
attr_reader :path
|
19
|
+
|
20
|
+
# The id related to the request, which is usually the primary
|
21
|
+
# key of the related model instance, but for browse/search
|
22
|
+
# pages is used as the page
|
23
|
+
attr_reader :id
|
24
|
+
|
25
|
+
# The params for the current request
|
26
|
+
attr_reader :params
|
27
|
+
|
28
|
+
# The session variables for the current request
|
29
|
+
attr_reader :session
|
30
|
+
|
31
|
+
# Whether the current request used the POST HTTP method.
|
32
|
+
def post?
|
33
|
+
method == 'POST'
|
34
|
+
end
|
35
|
+
|
36
|
+
# The query string for the current request
|
37
|
+
def query_string
|
38
|
+
@env['QUERY_STRING']
|
39
|
+
end
|
40
|
+
|
41
|
+
# Set the flash at notice level when redirecting, so it shows
|
42
|
+
# up on the redirected page.
|
43
|
+
def set_flash_notice(message)
|
44
|
+
@controller.flash[:notice] = message
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the current flash at error level, used when displaying
|
48
|
+
# pages when there is an error.
|
49
|
+
def set_flash_now_error(message)
|
50
|
+
@controller.flash.now[:error] = message
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module AutoForme
|
2
|
+
# Helper class for formating HTML tables used for the browse/search results pages.
|
3
|
+
class Table
|
4
|
+
# The AutoForme::Action for the current table
|
5
|
+
attr_reader :action
|
6
|
+
|
7
|
+
# The AutoForme::Model for the current table
|
8
|
+
attr_reader :model
|
9
|
+
|
10
|
+
# The AutoForme::Request for the current table
|
11
|
+
attr_reader :request
|
12
|
+
|
13
|
+
# The action type for the current table
|
14
|
+
attr_reader :type
|
15
|
+
|
16
|
+
# The data columns for the current table
|
17
|
+
attr_reader :columns
|
18
|
+
|
19
|
+
# An array of objects to show in the table
|
20
|
+
attr_reader :objs
|
21
|
+
|
22
|
+
# Any options for the table
|
23
|
+
attr_reader :opts
|
24
|
+
|
25
|
+
def initialize(action, objs, opts={})
|
26
|
+
@action = action
|
27
|
+
@request = action.request
|
28
|
+
@model = action.model
|
29
|
+
@type = action.normalized_type
|
30
|
+
@columns = model.columns_for(type, request)
|
31
|
+
@objs = objs
|
32
|
+
@opts = opts
|
33
|
+
end
|
34
|
+
|
35
|
+
def h(s)
|
36
|
+
action.h(s)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return an HTML string for the table.
|
40
|
+
def to_s
|
41
|
+
html = "<table class=\"#{model.table_class_for(type, request)}\">"
|
42
|
+
if caption = opts[:caption]
|
43
|
+
html << "<caption>#{h caption}</caption>"
|
44
|
+
end
|
45
|
+
|
46
|
+
html << "<thead><tr>"
|
47
|
+
columns.each do |column|
|
48
|
+
html << "<th>#{h action.column_label_for(type, request, model, column)}</th>"
|
49
|
+
end
|
50
|
+
html << "<th>Show</th>" if show = model.supported_action?(:show, request)
|
51
|
+
html << "<th>Edit</th>" if edit = model.supported_action?(:edit, request)
|
52
|
+
html << "<th>Delete</th>" if delete = model.supported_action?(:delete, request)
|
53
|
+
html << "</tr></thead>"
|
54
|
+
|
55
|
+
html << "<tbody>"
|
56
|
+
objs.each do |obj|
|
57
|
+
html << "<tr>"
|
58
|
+
columns.each do |column|
|
59
|
+
html << "<td>#{h model.column_value(type, request, obj, column)}</td>"
|
60
|
+
end
|
61
|
+
html << "<td><a href=\"#{action.url_for("show/#{model.primary_key_value(obj)}")}\" class=\"btn btn-mini btn-info\">Show</a></td>" if show
|
62
|
+
html << "<td><a href=\"#{action.url_for("edit/#{model.primary_key_value(obj)}")}\" class=\"btn btn-mini btn-primary\">Edit</a></td>" if edit
|
63
|
+
html << "<td><a href=\"#{action.url_for("delete/#{model.primary_key_value(obj)}")}\" class=\"btn btn-mini btn-danger\">Delete</a></td>" if delete
|
64
|
+
html << "</tr>"
|
65
|
+
end
|
66
|
+
html << "</tbody></table>"
|
67
|
+
html
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/autoforme.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'forme'
|
2
|
+
require 'thread'
|
3
|
+
require 'rack/utils'
|
4
|
+
|
5
|
+
module AutoForme
|
6
|
+
# Map of framework type symbols to framework classes
|
7
|
+
FRAMEWORKS = {}
|
8
|
+
|
9
|
+
# Map of model type symbols to model classes
|
10
|
+
MODELS = {}
|
11
|
+
@mutex = Mutex.new
|
12
|
+
|
13
|
+
# AutoForme specific error class
|
14
|
+
class Error < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
[[:framework, FRAMEWORKS], [:model, MODELS]].each do |map_type, map|
|
18
|
+
singleton_class = class << self; self; end
|
19
|
+
|
20
|
+
singleton_class.send(:define_method, :"register_#{map_type}") do |type, klass|
|
21
|
+
@mutex.synchronize{map[type] = klass}
|
22
|
+
end
|
23
|
+
|
24
|
+
singleton_class.send(:define_method, :"#{map_type}_class_for") do |type|
|
25
|
+
unless klass = @mutex.synchronize{map[type]}
|
26
|
+
require "autoforme/#{map_type}s/#{type}"
|
27
|
+
unless klass = @mutex.synchronize{map[type]}
|
28
|
+
raise Error, "unsupported framework: #{type.inspect}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
klass
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create a new set of model forms. Arguments:
|
36
|
+
# type :: A type symbol for the type of framework in use (:sinatra or :rails)
|
37
|
+
# controller :: The controller class in which to load the forms
|
38
|
+
# opts :: Options hash. Current supports a :prefix option if you want to mount
|
39
|
+
# the forms in a different prefix.
|
40
|
+
#
|
41
|
+
# Example:
|
42
|
+
#
|
43
|
+
# AutoForme.for(:sinatra, Sinatra::Application, :prefix=>'/path') do
|
44
|
+
# model Artist
|
45
|
+
# end
|
46
|
+
def self.for(type, controller, opts={}, &block)
|
47
|
+
Framework.for(type, controller, opts, &block)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
require 'autoforme/opts_attributes'
|
52
|
+
require 'autoforme/model'
|
53
|
+
require 'autoforme/framework'
|
54
|
+
require 'autoforme/request'
|
55
|
+
require 'autoforme/action'
|
56
|
+
require 'autoforme/table'
|
57
|
+
require 'autoforme/version'
|