magic-resource 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 88c7e7680c76cf4d34bb5b067c01a80d731fb55d
4
+ data.tar.gz: 211494d054116ea4373f63f49f31eeba5b20bcca
5
+ SHA512:
6
+ metadata.gz: 0f22e8a2cc3675a69c97453792b260b5abd919878e3304e6b48b91535b87ed11a779c083e7ad762e0da1822184e141f8ffa8aac23a52d58875f2a09f467ce353
7
+ data.tar.gz: f2f8eafac949a968111a00cd084d198c307f82faae122bdc07d6339fa5e13afbf79eadc3b8373e6fbf1f01b219b34618c84b1b276b878c19e857580002286d2f
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Sergey Tokarenko
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,475 @@
1
+ MagicResource
2
+ ==============
3
+
4
+ MagicResource is a Rails plugin which introduces the resource-based ideology for WEB applications development.
5
+
6
+ ## Getting Started
7
+
8
+ Add to your Gemfile:
9
+
10
+ ```ruby
11
+ gem 'magic-resource', git: 'git@gitlab.anahoret.com:anadea/magic-resource.git', tag: 'v0.0.3'
12
+ ```
13
+
14
+ Run the bundle command to install it.
15
+
16
+ Run the generator:
17
+ ```console
18
+ rails generate magic_resource:install
19
+ ```
20
+
21
+ Include to controller, typically to ApplicationController:
22
+ ```ruby
23
+ class ApplicationController < ActionController::Base
24
+ include MagicResource::Controller
25
+ ```
26
+
27
+ ## Resource abstraction
28
+ It is a quite naturally when we affecting to something, we always affecting to specific object or to the scope of alike objects. Just like the calling the instance method or class method. Even when we want to do something like `turn off all electrical devices in the house`, it's better and more natural to trigger the `turn_off_the_devices` method on `House.belongs_to(me)` object, rather than to implement inplace logic which will find all the devices, and do something with them.
29
+
30
+ Lets think like that when building the RoR application. Any time when the browser makes the request to the server, it affects the specific object, or to the scope of alike object. In RoR terms, each call to the server affects to specific Model instance, or to the scope of such Model.
31
+
32
+ Each Model has `resource_name`, typically that is the model's class name, underscored and pluralized. For Property model that will be `:properties`.
33
+
34
+ Then, lets add the `resource_context` when we affecting to the Model. It is just a Symbol identity. It should have just a logical sense, but not tied to any terms from the application like user roles, application areas, menu items etc.
35
+
36
+ For example, lets say that `admin` users should have an access to all crud actions for the Property model, and `guest` users should have acccess to show and filterable index actions only. It's better to say that Property model can be affected under `:crud` and `:search` contexts, rather than `:admin` and `:guest` ones. Then, if `admin` role will be renamed to `superadmin`, if crud actions will be available for `manager` as well etc - `:crud` context name will not loose the sense, rather than `:admin` context.
37
+
38
+ The affecting to the Model is always has some context, it can't be blank. Lets say that default context for any Model is `:crud`.
39
+
40
+ Each resource context means the separated controller and the special folder for templates. To finish the picture, lets add the single localization file, then say:
41
+ > Resource is the container-like abstraction, which includes the Model, the contexts to be applied when affecting the Model, the scope of one-per-context Controllers, the scope of templates to be rendered with the Model, and single localization file.
42
+
43
+ ## Sources structure
44
+ ### Controllers
45
+ Controllers should be placed to `app/controllers/#{resource_name}/#{resource_context}_controller.rb` and named accordingly. For Property model and :crud context it should be placed to `app/controllers/properties/crud_controller.rb` and contains something like:
46
+ ```ruby
47
+ module Properties
48
+ class CrudController < ApplicationController
49
+ end
50
+ end
51
+ ```
52
+
53
+ ### Models
54
+ Nothing specific, place and name them as always.
55
+
56
+ ### Views
57
+ There are the general folder for resource templates, named `app/views/#{resource_name}/`. In the root of this folter should be placed the templates which you normally want to name `shared`. In folters like `app/views/#{resource_name}/#{resource_context}/` should be placed the context-specific templates.
58
+
59
+ The typical scope of templates for Property model under :crud context should looks like:
60
+ * app/views/properties/crud/edit.html.haml
61
+ * app/views/properties/crud/index.html.haml
62
+ * app/views/properties/crud/new.html.haml
63
+ * app/views/properties/crud/show.html.haml
64
+ * app/views/properties/_form_attrs.html.haml
65
+ * app/views/properties/_list_item.html.haml
66
+ * app/views/properties/_form_search_attrs.html.haml
67
+ * app/views/properties/_view.html.haml
68
+
69
+ ### Locales
70
+ The single locale file should be placed to `config/locales/en/#{resource_name}.yml`. For Property model it should be placed to `config/locales/en/properties.yml` and contains something like:
71
+ ```yml
72
+ en:
73
+ properties:
74
+ button_edit: 'Edit Property'
75
+ ```
76
+
77
+ ## Magic
78
+ When we speaking about the `magic`, we mean some smart and powerful instruments. Just like:
79
+ ```ruby
80
+ redirect_to user
81
+ ```
82
+
83
+ But, uncontrolled usage of such things is not good:
84
+ ```ruby
85
+ redirect_to my_favorite_variable
86
+ ```
87
+ Where we go? Lets guess that to `users#show` action, and we going to rename the action or controller - how we can find all places in the sources which redirects to `users#show` in this way?
88
+
89
+ Lets define, that the `magic` is good when it:
90
+ * invariantly predictable
91
+ * searchable in the sources
92
+ * can be banned partially or totally in whole project
93
+
94
+ ## General rules
95
+ Lets separate the controller's action and templates to two group - collection and member, dependent when we working with the single Model instance of with Model's scope.
96
+
97
+ ### Set the resource object
98
+ In `collection` controller actions call `resources=` method:
99
+ ```ruby
100
+ def index
101
+ self.resources = Property.all
102
+ end
103
+ ```
104
+
105
+ In `member` controller actions call `resource=` method:
106
+ ```ruby
107
+ def show
108
+ self.resource = Property.find(params[:id])
109
+ end
110
+ ```
111
+
112
+ Controller will set the resource context automatically by controller's class name.
113
+
114
+ ### Get the resource object
115
+ In 'collection' controller actions and templates call 'resources' method:
116
+ ```ruby
117
+ resources.size
118
+ ```
119
+
120
+ In 'member' controller actions and templates call 'resource' method:
121
+ ```ruby
122
+ resource.full_name
123
+ ```
124
+
125
+ ### Magic patterns
126
+
127
+ The full pattern looks like `#{resource_name}::#{resource_context}#{separator}#{identifier}`. Dependent on the magic method, it changes the separator and identifier sense:
128
+ ```ruby
129
+ # the way to controller's action
130
+ r.path_to 'users::search#show'
131
+
132
+ # the template path
133
+ r.render 'users::search/view'
134
+
135
+ # the translation key
136
+ r.t 'users::search.button_search'
137
+ ```
138
+
139
+ When the context name is skipped, it means the inheritance of current context for current object, or apllying the default context to other object:
140
+ ```ruby
141
+ r.path_to 'users#show' # inherit the current context
142
+
143
+ r(resource).path_to 'users#show' # the same as
144
+ r(resource).path_to 'users::crud#show'
145
+ ```
146
+
147
+ MagicResource will check is the object's type matching to specified resource name:
148
+ ```ruby
149
+ r(Property).path_to 'users#show' # raise exception
150
+ ```
151
+
152
+ For current object is possible to skip the resource name:
153
+ ```ruby
154
+ r.path_to '#index'
155
+ ```
156
+
157
+ For current object is possible to specify just identifier:
158
+ ```ruby
159
+ r.path_to :show
160
+ ```
161
+
162
+ ### Access to the magic
163
+ Typically you want to inherit the current resource object and resource_context for the magic. To do this, in controller or template call `r` method. You are able to use short patterns in magic calls, as well as combined and full patterns:
164
+ ```ruby
165
+ r.path_to :show
166
+ r.path_to 'properties#show'
167
+ r.path_to '::search#show'
168
+ r.path_to 'properties::search#show'
169
+ ```
170
+
171
+ Sometimes, you want to make a magic with some `other object`. To do this, in controller or template call `r` method with object as parameter. Once the `other object` is specified, you are forced to use only full patterns in magic calls:
172
+ ```ruby
173
+ r(resource.user).path_to 'users#show'
174
+ r(resource.user).path_to 'users::search#show'
175
+ ```
176
+
177
+ When you pass the current resource as parameter, it anyway means that the `other object` is specified - you loose the current context and must use the full patterns in magic calls:
178
+ ```ruby
179
+ r(resource).path_to 'properties#show'
180
+ r(resource).path_to 'properties::search#show'
181
+ ```
182
+
183
+ When you don't have the specific object, pass the class as parameter. It again means the `other object` rules:
184
+ ```ruby
185
+ r(User).path_to 'users#index'
186
+ ```
187
+
188
+ You can pass the form object as parameter, in this case the object inside it will be a the `other object`:
189
+ ```haml
190
+ = r.form_for :update do |f|
191
+ = f.simple_fields_for :user do |uf|
192
+ = r(ff).render 'users/form_attrs', f: ff
193
+ ```
194
+
195
+ ## Magic methods
196
+ ### r.context?
197
+ Check is the resource context is in the args list:
198
+ ```ruby
199
+ r.context?(:crud, :search) # true
200
+ ```
201
+
202
+ ### r.t
203
+ The call looks like:
204
+ ```ruby
205
+ r.t :button_edit
206
+ r(user).t 'users::crud.button_edit', {interpolation_option: 'foo'}
207
+ ```
208
+
209
+ It will search the translation in next order:
210
+ * users.crud.button_edit
211
+ * users.button_edit
212
+ * resources.crud.button_edit
213
+ * resources.button_edit
214
+
215
+ The method will not accept dotted keys like `.my.dotted.key`. But why do you want it actually? '.my_dotted_key' has the same count of chars, and it's better to separate the locales section by white space rather than to produce the one more subtree for nothing.
216
+
217
+ ### r.render
218
+ The call looks like:
219
+ ```ruby
220
+ r.render :form_attrs, f: f
221
+ r(user).render 'users::crud/form_attrs', f: f
222
+ ```
223
+
224
+ It will search the template in next order:
225
+ * users/crud/_form_attrs
226
+ * users/_form_attrs
227
+ * resources/crud/_form_attrs
228
+ * resources/_form_attrs
229
+
230
+ The method will apply the specified object and context as `current` inside the target template.
231
+
232
+ The method will not accept the subfolder `/form/attrs` templates just like r.t method.
233
+
234
+ The accepts the form object as second param, and passes it as `f` local parameter:
235
+ ```haml
236
+ = r.form_for :update do |f|
237
+ = r.render :form_attrs, f
238
+ ```
239
+
240
+ It's good to render the other resource template, rather than mass calling to it's attributes or magic methods:
241
+ ```haml
242
+ that is bad:
243
+ = r(resource.user).link_to 'users#show', resource.user.name
244
+ = resource.user.role
245
+ = resource.user.last_logged_in
246
+
247
+ that is good:
248
+ = r(resource.user).render 'users#view'
249
+
250
+ and then in 'app/views/users/_view'
251
+ = r.link_to :show, resource.name
252
+ = resource.role
253
+ = resource.last_logged_in
254
+ ```
255
+
256
+ In general, when you see in your partial the massive definition of full magic pattern - it's time to move this part to other resource's partial.
257
+
258
+ ### r.render_collection
259
+ The call looks like:
260
+ ```ruby
261
+ r.render_collection :list_item
262
+ r(users).render_collection 'users::crud/list_item'
263
+ ```
264
+
265
+ Just calls r.render for collection items, with current context inheritance in first sample.
266
+
267
+ ### r.render_collection_build
268
+ The call looks like:
269
+ ```ruby
270
+ r.render_collection_build :new_form
271
+ r(users).render_collection_build 'users::crud/new_form'
272
+ ```
273
+
274
+ Calls the r.render for 'object.soft_build' object (look at `stokarenko/association-soft-build` gem), with current context inheritance in first sample.
275
+
276
+ ### r.path_to
277
+ The call looks like:
278
+ ```ruby
279
+ r.path_to :show
280
+ r(user).path_to 'users::crud#show'
281
+ ```
282
+
283
+ Returns the URL to specific controller/action.
284
+
285
+ To pass the URL parameters call it in hashable way:
286
+ ```ruby
287
+ r.path_to show: {foo: :bar}
288
+ r(user).path_to 'users::crud#show' => {foo: :bar}
289
+ ```
290
+
291
+ ### r.redirect_to
292
+ Can be used only in controller.
293
+
294
+ The call looks like:
295
+ ```ruby
296
+ r.redirect_to :show
297
+ r(user).redirect_to 'users::crud#show'
298
+ ```
299
+
300
+ Redirects to specific controller/action.
301
+
302
+ To pass the URL parameters call it in hashable way:
303
+ ```ruby
304
+ r.redirect_to show: {foo: :bar}
305
+ r(user).redirect_to 'users::crud#show' => {foo: :bar}
306
+ ```
307
+
308
+ ### r.link_to
309
+ The call looks like:
310
+ ```ruby
311
+ r.link_to :edit
312
+ r(user).link_to 'users::crud#edit', 'Special edit button'
313
+ ```
314
+
315
+ The method will build the link html tag to specific controller/action.
316
+
317
+ It will accept the URL parameters in hashable way, just like `r.path_to` do.
318
+
319
+ It will automatically apply the HTTP verb for the link.
320
+
321
+ It will use `:button_#{action}` as the label param by default, then...
322
+
323
+ It will apply `r.t` method if the label param is a Symbol.
324
+
325
+ It will use string label param for label as it is.
326
+
327
+ It will automatically set `:confirm` param to `true` for `:destroy` actions, then
328
+
329
+ It will set `:confirm` param to `:confirm_destroy` if it is true, then
330
+
331
+ It will apply r.t method for confirmation message if `:confirm` param if is a Symbol.
332
+
333
+ It will apply `:confirm` string param as confirmation message as it is.
334
+
335
+ It will accept the block to be used as label parameter.
336
+
337
+ ### r.link_to_if
338
+ The call looks like:
339
+ ```ruby
340
+ r.link_to_if true, :edit
341
+ r(user).link_to_if false, 'users::crud#edit'
342
+ ```
343
+
344
+ Works just like regular `link_to_if` but uses r.link_to on success.
345
+
346
+ It will accept the URL parameters in hashable way, just like `r.path_to` do.
347
+
348
+ It will accept the block to be used as label parameter.
349
+
350
+ ### r.link_to_unless
351
+ The call looks like:
352
+ ```ruby
353
+ r.link_to_unless false, :edit
354
+ r(user).link_to_unless true, 'users::crud#edit'
355
+ ```
356
+
357
+ Works just like regular `link_to_unless` but uses r.link_to on success.
358
+
359
+ It will accept the URL parameters in hashable way, just like `r.path_to` do.
360
+
361
+ It will accept the block to be used as label parameter.
362
+
363
+ ### r.form_for
364
+ The call looks like:
365
+ ```ruby
366
+ r.form_for :update do |f|
367
+ end
368
+
369
+ r(user).form_for 'users::crud#update' do |f|
370
+ end
371
+ ```
372
+
373
+ Build the form to specific controller/action.
374
+
375
+ It will accept the URL parameters in hashable way, just like `r.path_to` do.
376
+
377
+ It automatically set the HTTP verb.
378
+
379
+ It uses the default `:simple` form helper prefix, so the form will be built with `simple_form_for` helper.
380
+
381
+ It takes the second param as the form helper prefix to be used.
382
+
383
+ It supports Ransack, just pass Ransack::Search object as second param.
384
+
385
+ It takes the logical `#save` action, which will be transformed to `#create` or `#update` actions dependent on the object's persistency status.
386
+
387
+ ### r.content_for
388
+ This method is too much magical, and disabled by default. Activate it in configuration if you really need it.
389
+
390
+ The usage looks like:
391
+ ```haml
392
+ - r(resource.user).content_for 'users:header_class', 'strong'
393
+ - r(resource.user).content_for 'users:header_prefix' do
394
+ some super prefix
395
+
396
+ = r(resource.user).render 'users::crud/view'
397
+ ```
398
+
399
+ Then, in `app/views/users/crud/_view` template:
400
+ ```haml
401
+ .header{class: r.content(:header_class) || :default}
402
+ = r.content(:header_prefix)
403
+ the body
404
+ ```
405
+
406
+ The same can be done by locals passing to render, but sometimes we need to forward such locals deeply and deeply to other render calls. Looks like `r.content_for` method is a better solution for that...
407
+
408
+ ## Configuration
409
+ MagicResource generator will prepare the `config/initializers/magic-resource.rb` config file.
410
+ Please take a look on it:
411
+ ```ruby
412
+ MagicResource.setup do |config|
413
+ ## Uncoment the line to change the context to be used as default.
414
+ ## Default value is `:crud`.
415
+ # config.default_context = :manage
416
+
417
+ ## Uncoment the line to change default form helper prefix.
418
+ ## Default value is `:simple`.
419
+ # config.default_form_type = :default
420
+
421
+ ## Change the method to adjust the level or resource magic.
422
+ ## possible methods are `no_magic`, `try_magic?` and `full_magic!`
423
+ ## Thechnically such methods just setting all following parameters to
424
+ ## `:by_exception`, `:by_warning` and `false` respectively.
425
+ config.no_magic
426
+
427
+ ############################
428
+ #### For all next parameters
429
+ #### the correct values are: `[false, :by_exception, :by_warning]`.
430
+
431
+ ## Will require to specify the resource name in helper's pattern when resource is "other".
432
+ ## Can be useful for fast development or experiments. But typically it's better
433
+ ## to switch it off later and define the resource names everywhere,
434
+ ## because the magic becames to be not predictable and searchable.
435
+ # config.force_resource_name_definition = :by_exception
436
+
437
+ ## Will require the correct resource assignation in controller
438
+ ## Means the situation when inside Users::CrudController you trying to do
439
+ ## self.resource = Property.first
440
+ # config.assert_resource_name_in_controller = :by_exception
441
+
442
+ ## Will disable content_for helper.
443
+ ## It is too much magical...
444
+ ## But in some cases it is the better than other solutions.
445
+ ## Try to avoid it.
446
+ ## Activate it if you really need it.
447
+ # config.disable_content_for_helper = :by_exception
448
+
449
+ end
450
+ ```
451
+
452
+ ## Changes
453
+ ### v0.0.3
454
+ * Added the possibility to pass URL params to routable helpers.
455
+ * Not ActiveModel-like classes can be a resource.
456
+ * `r` method will extract the `object` and use it as resource when form object received.
457
+ * `r.render` accepts the form object as second param, and passes it as `f` local parameter.
458
+ * Added `r.link_to_unless` helper.
459
+ * `r.link_to`-like helpers accept the block as label.
460
+ * Added `r.redirect_to` controller helper.
461
+
462
+ ## TODO
463
+ * Implement resourcable Coccon, and make it less painful.
464
+ * Leave r object in controller as it was set there (?).
465
+ * Apply the passing of translation interpolation params as Hash (?).
466
+ * Separate the helpers for controller and view.
467
+ * Resource controllers.
468
+ * Automatic preload in controllers, bases on statistic of association calls in templates.
469
+ * inverse_of as default in ActiveModel (?).
470
+ * Resource tests (?).
471
+ * ActiveRecord translations.
472
+ * cancancan integration.
473
+ * Single-file navigation for whole application (?).
474
+ * Routes definition helper.
475
+ * Implement resources default templates, and configuration to turn them off.
@@ -0,0 +1,17 @@
1
+ module MagicResource
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ desc 'Copy MagicResource default files'
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ def copy_initializer
8
+ template 'magic-resource.rb', 'config/initializers/magic-resource.rb'
9
+ end
10
+
11
+ def copy_locales
12
+ directory 'config/locales'
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ en:
2
+ resources:
3
+ button_index: 'List'
4
+ button_show: 'Show'
5
+ button_new: 'New'
6
+ button_create: 'Create'
7
+ button_edit: 'Edit'
8
+ button_update: 'Update'
9
+ button_destroy: 'Destroy'
10
+
11
+ confirm_destroy: 'Are you sure?'
@@ -0,0 +1,38 @@
1
+ MagicResource.setup do |config|
2
+ ## Uncoment the line to change the context to be used as default.
3
+ ## Default value is `:crud`.
4
+ # config.default_context = :manage
5
+
6
+ ## Uncoment the line to change default form helper prefix.
7
+ ## Default value is `:simple`.
8
+ # config.default_form_type = :default
9
+
10
+ ## Change the method to adjust the level or resource magic.
11
+ ## possible methods are `no_magic`, `try_magic?` and `full_magic!`
12
+ ## Thechnically such methods just setting all following parameters to
13
+ ## `:by_exception`, `:by_warning` and `false` respectively.
14
+ config.no_magic
15
+
16
+ ############################
17
+ #### For all next parameters
18
+ #### the correct values are: `<%= MagicResource::MAGIC_VALID_VALUES.inspect %>`.
19
+
20
+ ## Will require to specify the resource name in helper's pattern when resource is "other".
21
+ ## Can be useful for fast development or experiments. But typically it's better
22
+ ## to switch it off later and define the resource names everywhere,
23
+ ## because the magic becames to be not predictable and searchable.
24
+ # config.force_resource_name_definition = :by_exception
25
+
26
+ ## Will require the correct resource assignation in controller
27
+ ## Means the situation when inside Users::CrudController you trying to do
28
+ ## self.resource = Property.first
29
+ # config.assert_resource_name_in_controller = :by_exception
30
+
31
+ ## Will disable content_for helper.
32
+ ## It is too much magical...
33
+ ## But in some cases it is the better than other solutions.
34
+ ## Try to avoid it.
35
+ ## Activate it if you really need it.
36
+ # config.disable_content_for_helper = :by_exception
37
+
38
+ end
@@ -0,0 +1,76 @@
1
+ require 'magic-resource/version'
2
+
3
+ require 'ruby-features'
4
+ require 'activerecord-devkit'
5
+
6
+ require 'magic-resource/railtie'
7
+
8
+ module MagicResource
9
+ autoload :Controller, 'magic-resource/controller'
10
+ autoload :Helper, 'magic-resource/helper'
11
+ autoload :Container, 'magic-resource/container'
12
+ autoload :PathWithHttpVerb, 'magic-resource/path_with_http_verb'
13
+
14
+ module Generators
15
+ autoload :InstallGenerator, 'generators/magic_resource/install_generator'
16
+ end
17
+
18
+ MAGIC_PARAMETERS = %w(
19
+ force_resource_name_definition
20
+ assert_resource_name_in_controller
21
+ disable_content_for_helper
22
+ ).freeze
23
+ MAGIC_VALID_VALUES = [false, :by_exception, :by_warning].freeze
24
+
25
+ mattr_accessor :default_context
26
+ @@default_context = :crud
27
+
28
+ mattr_accessor :default_form_type
29
+ @@default_form_type = :simple
30
+
31
+ MAGIC_PARAMETERS.each do |param|
32
+ mattr_reader param.to_sym
33
+ class_variable_set(:"@@#{param}", :by_exception)
34
+
35
+ define_singleton_method(:"#{param}=") do |value|
36
+ raise ArgumentError.new(
37
+ "Unknown value: #{value.inspect}. Valid values are: #{MAGIC_VALID_VALUES.inspect}"
38
+ ) unless MAGIC_VALID_VALUES.include?(value)
39
+ class_variable_set(:"@@#{param}", value)
40
+ end
41
+ end
42
+
43
+ def self.setup
44
+ yield self
45
+ end
46
+
47
+ def self.no_magic
48
+ set_all_magic_parameters(:by_exception)
49
+ end
50
+
51
+ def self.try_magic?
52
+ set_all_magic_parameters(:by_warning)
53
+ end
54
+
55
+ def self.full_magic!
56
+ set_all_magic_parameters(false)
57
+ end
58
+
59
+ def self.set_all_magic_parameters(value)
60
+ MAGIC_PARAMETERS.each do |param|
61
+ public_send(:"#{param}=", value)
62
+ end
63
+ end
64
+
65
+ def self.logger
66
+ ::Rails.logger
67
+ end
68
+
69
+ def self.assert_restriction(restriction_type, message)
70
+ message.prepend('MagicResource: ')
71
+ case class_variable_get(:"@@#{restriction_type}")
72
+ when :by_exception then raise(message)
73
+ when :by_warning then logger.warn(message)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,288 @@
1
+ module MagicResource
2
+ class Container
3
+
4
+ CONTEXT_PATTERNS = {
5
+ route: '#',
6
+ translation: '.',
7
+ template: '/',
8
+ content: ':'
9
+ }.inject({}){ |mem, (type, separator)|
10
+ mem[type] = {
11
+ separator: separator,
12
+ regexp: /^(?<name>\w+)?#{'(?:::(?<context>\w+))?' unless type == :content}(?:#{Regexp.quote(separator)}(?<target>\w+#{'\??' if type == :translation}))$/
13
+ }; mem
14
+ }.freeze
15
+
16
+ attr_reader :name, :context, :collection, :item
17
+ alias resources collection
18
+ alias resource item
19
+
20
+ def context?(*args)
21
+ args.include?(context)
22
+ end
23
+
24
+ def render(partial, *args)
25
+ options = args.extract_options!
26
+ if args.present? && form = args.shift
27
+ raise 'Form object containing wrong resource' unless form.object == rs
28
+ options[:f] = form unless options.has_key?(:f)
29
+ end
30
+
31
+ partial, ct = extract_context(partial, :template)
32
+
33
+ lookup_templates = [name, :resources].
34
+ flat_map{|lc| ["#{lc}/#{ct}", lc] }.
35
+ map{|l| "#{l}/#{partial}"}
36
+
37
+ template = lookup_templates.find{|t| helper.lookup_context.template_exists?(t, [], true)}
38
+ raise("Can't find #{lookup_templates[0]} template for #{name} resource") unless template
39
+
40
+ with_context(ct){helper.with_resource_container(self){helper.render(template, options)}}
41
+ end
42
+
43
+ def render_collection(partial, options = {})
44
+ partial, ct = extract_context(partial, :template)
45
+ collection.map{|r| helper.r(r).render("#{name}::#{ct}/#{partial}", options)}.join.html_safe
46
+ end
47
+
48
+ def t(key, options = {})
49
+ key, ct = extract_context(key, :translation)
50
+
51
+ lookup_keys = [name, :resources].
52
+ flat_map{|lc| ["#{lc}.#{ct}", lc] }.
53
+ map{|l| :"#{l}.#{key}"}
54
+
55
+ helper.t(
56
+ lookup_keys[0],
57
+ options.merge(
58
+ default: lookup_keys[1..-1] + Array.wrap(options[:default])
59
+ )
60
+ )
61
+ end
62
+
63
+ def render_collection_build(partial, options = {})
64
+ partial, ct = extract_context(partial, :template)
65
+ helper.r(resources.soft_build).render("#{name}::#{ct}/#{partial}", options)
66
+ end
67
+
68
+ def link_to(action, *args, &block)
69
+ options = args.extract_options!
70
+
71
+ action, ct, route_args = extract_route_context(action)
72
+ with_context(ct) do
73
+ url, method = route_to(action, route_args)
74
+ link_options = {}
75
+
76
+ link_options[:method] = method unless method == :get
77
+
78
+ confirm = options.delete(:confirm){method === :delete}
79
+ confirm = :confirm_destroy if confirm === true
80
+ confirm = t(confirm) if confirm.kind_of?(Symbol)
81
+ link_options[:data] = {confirm: confirm} if confirm
82
+
83
+ label = label_for_link(action, args.first, &block)
84
+
85
+ helper.link_to(label, url, link_options.deep_merge(options))
86
+ end
87
+ end
88
+
89
+ #Used in controller and view
90
+ def path_to(action)
91
+ action, ct, route_args = extract_route_context(action)
92
+ with_context(ct) do
93
+ route_to(action, route_args).first
94
+ end
95
+ end
96
+
97
+ #Used in controller
98
+ def redirect_to(action, options = {})
99
+ helper.redirect_to path_to(action), options
100
+ end
101
+
102
+ def link_to_if(condition, action, *args, &block)
103
+ if condition
104
+ link_to(action, *args, &block)
105
+ else
106
+ action, ct = extract_route_context(action)
107
+ args.extract_options!
108
+ with_context(ct) do
109
+ label_for_link(action, args.first, &block)
110
+ end
111
+ end
112
+ end
113
+
114
+ def link_to_unless(condition, *args, &block)
115
+ link_to_if(!condition, *args, &block)
116
+ end
117
+
118
+ def form_for(action, *args, &block)
119
+ options = args.extract_options!
120
+ form_type = args.shift
121
+
122
+ action, ct, route_args = extract_route_context(action)
123
+ with_context(ct) do
124
+ url, method = route_to(action, route_args)
125
+
126
+ form_type ||= MagicResource.default_form_type
127
+ form_type, obj = form_type.kind_of?(Ransack::Search) ?
128
+ [:search, form_type] :
129
+ [form_type, item]
130
+ helper.public_send(:"#{form_type}_form_for", obj, {url: url, method: method}.merge(options), &block)
131
+ end
132
+ end
133
+
134
+ def content_for(index, *args, &block)
135
+ MagicResource.assert_restriction(
136
+ :disable_content_for_helper,
137
+ "#content_for helper is too much magical...: #{index}"
138
+ )
139
+
140
+ content = if block_given?
141
+ raise ArgumentError.new("Both content param and block specified: #{index}") if args.present?
142
+ helper.capture(&block)
143
+ else
144
+ raise ArgumentError.new("Need to specify the content by second param or block: #{index}") unless args.present?
145
+ args.first
146
+ end
147
+
148
+ index = extract_context(index, :content)
149
+
150
+ helper.resource_contents[rs] ||= {}
151
+ helper.resource_contents[rs][index] = content
152
+ end
153
+
154
+ def content(index)
155
+ MagicResource.assert_restriction(
156
+ :disable_content_for_helper,
157
+ "`#content helper is too much magical...: #{index}"
158
+ )
159
+
160
+ index = extract_context(index, :content)
161
+
162
+ helper.resource_contents[rs].try(:[], index)
163
+ end
164
+
165
+ def _launch(helper, changed = false)
166
+ self.helper = helper
167
+ self.changed = changed
168
+ self
169
+ end
170
+
171
+ def self._launch(helper, *args)
172
+ args.present? ?
173
+ self.new(args.first)._launch(helper, true) :
174
+ # Let's try to keep resource_container to be private..
175
+ helper.send(:resource_container)._launch(helper)
176
+ end
177
+
178
+ private
179
+
180
+ attr_accessor :helper, :changed
181
+ alias changed? changed
182
+ attr_reader :rs
183
+ attr_writer :name, :collection, :item
184
+
185
+ def initialize(rs, context = nil)
186
+ rs = rs.object if rs.respond_to?(:object)
187
+ self.rs, self.context = rs, context
188
+ end
189
+
190
+ def rs=(new_rs)
191
+ @rs, self.name = new_rs.kind_of?(Symbol) ?
192
+ [nil, new_rs] :
193
+ [new_rs, nil]
194
+
195
+ self.collection, self.item = rs.respond_to?(:to_ary) ?
196
+ [rs, nil] :
197
+ [nil, rs]
198
+
199
+ # Just `||=` not working due to private method
200
+ self.name = self.name || self.class.resource_name(rs)
201
+ end
202
+
203
+ def context=(new_context)
204
+ @context = new_context || MagicResource.default_context
205
+ end
206
+
207
+ def extract_context(pattern, context_type)
208
+ parse_params = CONTEXT_PATTERNS[context_type]
209
+
210
+ pattern = pattern.to_s
211
+ pattern.prepend(parse_params[:separator]) unless pattern.include?(parse_params[:separator])
212
+ pattern_parts = pattern.match(parse_params[:regexp])
213
+ raise("Wrong resource target pattern: #{pattern}") unless pattern_parts
214
+
215
+ target_name = pattern_parts[:name].try(:to_sym)
216
+ MagicResource.assert_restriction(
217
+ :force_resource_name_definition,
218
+ "Need to specify resource name when switching resource: #{pattern}"
219
+ ) if changed? && target_name.nil?
220
+
221
+ if target_name
222
+ raise("Wrong resource name, expected #{name.inspect} but got #{target_name.inspect}") unless name == target_name
223
+ end
224
+
225
+ #Damn, string keys...
226
+ pattern_parts.names.include?('context') ?
227
+ [pattern_parts[:target].to_sym, pattern_parts[:context].try(:to_sym) || context] :
228
+ pattern_parts[:target].to_sym
229
+ end
230
+
231
+ def extract_route_context(action)
232
+ action, route_args = action.kind_of?(Hash) ? action.first : [action, {}]
233
+
234
+ extract_context(action, :route) << route_args
235
+ end
236
+
237
+ def with_context(context)
238
+ same_context = self.context == context
239
+ old_context, self.context = self.context, context unless same_context
240
+
241
+ yield
242
+ ensure
243
+ self.context = old_context unless same_context
244
+ end
245
+
246
+ def self.resource_name(duck)
247
+ raise 'Undefined resource name' if duck.nil?
248
+ return duck if duck.kind_of?(Symbol)
249
+ return duck.to_sym if duck.kind_of?(String)
250
+ return duck.model_name.route_key.to_sym if duck.respond_to?(:model_name)
251
+ return resource_name(duck.klass) if duck.respond_to?(:klass)
252
+ return resource_name(duck.class) unless duck.kind_of?(Class)
253
+ duck.name.underscore.pluralize.to_sym
254
+ end
255
+
256
+ def route_to(action, route_args = {}, only_path = true)
257
+ action = (item.try(:persisted?) ? :update : :create) if action == :save
258
+
259
+ options = helper.url_options.deep_merge(
260
+ action: action,
261
+ only_path: only_path,
262
+ _recall: {
263
+ controller: [name, context].compact.join('/'),
264
+ # Need to confuse Rails..
265
+ action: 'hope_not_existing_action'
266
+ }
267
+ ).deep_merge(route_args)
268
+
269
+ resource.try(:persisted?) ?
270
+ options[:_recall][:id] = resource :
271
+ options[:_recall].delete(:id)
272
+
273
+ url = helper._routes.url_for(options)
274
+ [url, url.http_verb]
275
+ end
276
+
277
+ def label_for_link(action, label, &block)
278
+ if block_given?
279
+ raise 'Both label and block are specified for link_to' if label
280
+ return helper.capture(&block)
281
+ end
282
+
283
+ label ||= :"button_#{action}"
284
+ label = t(label) if label.kind_of?(Symbol)
285
+ label
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,73 @@
1
+ module MagicResource
2
+ module Controller
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper MagicResource::Helper
7
+ helper_method :resource_container, :with_resource_container, :resource_contents
8
+
9
+ prepend_before_action :prepare_resource_controller
10
+
11
+ protected
12
+ attr_reader :resources, :resource
13
+
14
+ private
15
+ attr_accessor :resource_name, :resource_context, :resource_container
16
+ end
17
+
18
+ protected
19
+
20
+ def resources=(resources)
21
+ @resources, @resource = resources, nil
22
+ prepare_resource_container(resources)
23
+ end
24
+
25
+ def resource=(resource)
26
+ @resources, @resource = nil, resource
27
+ prepare_resource_container(resource)
28
+ end
29
+
30
+ def r(*args)
31
+ MagicResource::Container._launch(self, *args)
32
+ end
33
+
34
+ private
35
+
36
+ def prepare_resource_controller
37
+ resource_config = self.class.name.match(/^(?<name>\w+?)::(?<context>\w+)Controller$/)
38
+ raise "Wrong resource controller name: #{self.class.name}" unless resource_config
39
+
40
+ self.resource_name = resource_config[:name].underscore.to_sym
41
+ self.resource_context = resource_config[:context].underscore.to_sym
42
+
43
+ prepare_resource_container
44
+ end
45
+
46
+ def prepare_resource_container(rs = nil)
47
+ _prepare_resource_container(rs)
48
+
49
+ MagicResource.assert_restriction(
50
+ :assert_resource_name_in_controller,
51
+ "Wrong resource name in controller, expected #{resource_name.inspect} but got #{resource_container.name.inspect}"
52
+ ) unless resource_container.name == resource_name
53
+ end
54
+
55
+ def _prepare_resource_container(rs)
56
+ self.resource_container = MagicResource::Container.new(rs || resource_name, resource_context)
57
+ end
58
+
59
+ def with_resource_container(new_resource_container)
60
+ same_resource_container = resource_container == new_resource_container
61
+ old_resource_container, self.resource_container = resource_container, new_resource_container unless same_resource_container
62
+
63
+ yield
64
+ ensure
65
+ self.resource_container = old_resource_container unless same_resource_container
66
+ end
67
+
68
+ def resource_contents
69
+ @_resource_contents ||= {}
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,33 @@
1
+ RubyFeatures.define('magic_resource') do
2
+ dependency 'activerecord_devkit/association_soft_build'
3
+
4
+ apply_to 'ActionDispatch::Http::URL' do
5
+ applied do |variable|
6
+ class << self
7
+ alias_method_chain :url_for, :http_verb
8
+ end
9
+ end
10
+
11
+ class_methods do
12
+ def url_for_with_http_verb(options, *args)
13
+ path = url_for_without_http_verb(options, *args)
14
+ options[:path].kind_of?(MagicResource::PathWithHttpVerb) ?
15
+ MagicResource::PathWithHttpVerb.new(options[:path], path) :
16
+ path
17
+ end
18
+ end
19
+ end
20
+
21
+ apply_to 'ActionDispatch::Journey::Route' do
22
+ applied do
23
+ alias_method_chain :format, :http_verb
24
+ end
25
+
26
+ instance_methods do
27
+ def format_with_http_verb(*args)
28
+ MagicResource::PathWithHttpVerb.new(self.verb, format_without_http_verb(*args))
29
+ end
30
+ end
31
+ end
32
+
33
+ end.apply
@@ -0,0 +1,11 @@
1
+ module MagicResource
2
+ module Helper
3
+
4
+ delegate :resource, :resources, to: :resource_container
5
+
6
+ def r(*args)
7
+ MagicResource::Container._launch(self, *args)
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module MagicResource
2
+ class PathWithHttpVerb < String
3
+ attr_reader :http_verb
4
+
5
+ def initialize(http_verb, path)
6
+ @http_verb = http_verb.kind_of?(self.class) ?
7
+ http_verb.http_verb :
8
+ http_verb.source.scan(/\w+/).first.try(:downcase).try(:to_sym)
9
+
10
+ super(path)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ require 'magic-resource/feature'
@@ -0,0 +1,3 @@
1
+ module MagicResource
2
+ VERSION = '0.0.4'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: magic-resource
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Sergey Tokarenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-features
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord-devkit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Rails plugin which introduces the resource-based ideology for WEB applications
84
+ development.
85
+ email: private.tokarenko.sergey@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - LICENSE
91
+ - README.md
92
+ - lib/generators/magic_resource/install_generator.rb
93
+ - lib/generators/magic_resource/templates/config/locales/en/resources.yml
94
+ - lib/generators/magic_resource/templates/magic-resource.rb
95
+ - lib/magic-resource.rb
96
+ - lib/magic-resource/container.rb
97
+ - lib/magic-resource/controller.rb
98
+ - lib/magic-resource/feature.rb
99
+ - lib/magic-resource/helper.rb
100
+ - lib/magic-resource/path_with_http_verb.rb
101
+ - lib/magic-resource/railtie.rb
102
+ - lib/magic-resource/version.rb
103
+ homepage: https://github.com/stokarenko/magic-resource
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.4.5
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Rails plugin which introduces the resource-based ideology for WEB applications
127
+ development.
128
+ test_files: []
129
+ has_rdoc: