wrap_it 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/.yardopts +3 -0
  4. data/README.md +67 -66
  5. data/lib/wrap_it.rb +16 -16
  6. data/lib/wrap_it/arguments.rb +368 -0
  7. data/lib/wrap_it/base.rb +56 -47
  8. data/lib/wrap_it/callbacks.rb +24 -5
  9. data/lib/wrap_it/capture_array.rb +140 -0
  10. data/lib/wrap_it/container.rb +69 -25
  11. data/lib/wrap_it/derived_attributes.rb +22 -6
  12. data/lib/wrap_it/enums.rb +44 -34
  13. data/lib/wrap_it/frameworks.rb +7 -3
  14. data/lib/wrap_it/helpers.rb +66 -1
  15. data/lib/wrap_it/html.rb +149 -0
  16. data/lib/wrap_it/html_class.rb +164 -183
  17. data/lib/wrap_it/html_data.rb +28 -15
  18. data/lib/wrap_it/link.rb +40 -17
  19. data/lib/wrap_it/sections.rb +90 -2
  20. data/lib/wrap_it/switches.rb +33 -29
  21. data/lib/wrap_it/text_container.rb +83 -10
  22. data/lib/wrap_it/version.rb +2 -1
  23. data/spec/frameworks/log/development.log +2108 -0
  24. data/spec/integration/base_spec.rb +2 -2
  25. data/spec/integration/container_spec.rb +3 -3
  26. data/spec/integration/examples_spec.rb +16 -15
  27. data/spec/integration/text_container_spec.rb +3 -3
  28. data/spec/lib/arguments_array_spec.rb +37 -27
  29. data/spec/lib/arguments_spec.rb +153 -0
  30. data/spec/lib/base_spec.rb +2 -25
  31. data/spec/lib/callbacks_spec.rb +1 -1
  32. data/spec/lib/container_spec.rb +1 -1
  33. data/spec/lib/derived_attributes_spec.rb +1 -1
  34. data/spec/lib/enums_spec.rb +2 -3
  35. data/spec/lib/html_class_spec.rb +269 -80
  36. data/spec/lib/html_data_spec.rb +18 -12
  37. data/spec/lib/html_spec.rb +124 -0
  38. data/spec/lib/link_spec.rb +2 -2
  39. data/spec/lib/sections_spec.rb +1 -1
  40. data/spec/lib/switches_spec.rb +3 -3
  41. data/spec/lib/text_container_spec.rb +2 -2
  42. data/spec/support/example_groups/{wrap_it_example_group.rb → wrapped_example_group.rb} +5 -5
  43. data/wrap_it.gemspec +2 -0
  44. metadata +15 -8
  45. data/lib/wrap_it/arguments_array.rb +0 -128
  46. data/lib/wrap_it/module_helpers.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 505e7bd562594a96b7765484ddd4332d6d6142e4
4
- data.tar.gz: 82ff97e143fb9867e5cc0c3bb743ae28eb34bab3
3
+ metadata.gz: 995784a5858c83bfefb7ec041d0019e87535b279
4
+ data.tar.gz: 0e68c3329c7e692bbc685c09caf13981b7dcaa2a
5
5
  SHA512:
6
- metadata.gz: bad8623fbebe0f97ab7ca7045f9c3a167254af82240147e282ee7d7d599925106bd1ec3d21ffe0418d2defb9d1987f3a890a986575e6d12070caebbef9a02624
7
- data.tar.gz: 8c25e53fca0f92d4ba82e2c05d32b1be2d48e869ea21f03beb8759a78aeedd1f9b72553031f4800b4ee8dc38e5c0ba5337875d0b9c602e2c404d70158ba46731
6
+ metadata.gz: 7fb4f2c24ce43bcdbb875dbc303aaa5562a0eb25d91aec437b6b30d254a1e2a3c6a9a22d8de9c00704048317b5aaf18ce19cfe00bb69765fd9735ddfb6801801
7
+ data.tar.gz: 0b026802d29c1c858e92d6de642b823a113b75fffef8a2c5420cd2b449137289c455d74eb87a7d2380ab86123dcf6a48ee7a16de6e9a6bbdd490638c55cd3f57
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0-p353
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ --no-private
2
+ --markup markdown
3
+ --embed-mixins
data/README.md CHANGED
@@ -1,19 +1,28 @@
1
+ [![Gem Version](https://badge.fury.io/rb/wrap_it.png)](http://badge.fury.io/rb/wrap_it)
2
+ [![Code Climate](https://codeclimate.com/github/cybernetlab/wrap_it.png)](https://codeclimate.com/github/cybernetlab/wrap_it)
3
+
1
4
  # WrapIt
2
5
 
3
6
  This library provides set of classes and modules with simple DSL for quick and easy creating html helpers with your own DSL. It's usefull for implementing CSS frameworks, or making your own.
4
7
 
8
+ > Required ruby version is 2.0.0
9
+
10
+ > **Warning** A lot of code refactored. API changed. Review you code if you using previous versions of library.
11
+
5
12
  For example, your designer makes perfect button style for some site. This element will appears in many places of site in some variations. The button have `danger`, `success` and `default` look, and can have `active` state. Button can have some icon. So, you make some CSS styles, and now you should place HTML markup of this element in many places of site. With `wrap_it` library you can do it with following code:
6
13
 
7
14
  ```ruby
15
+ module Helpers; end
16
+
8
17
  WrapIt.register_module Helpers
9
18
 
10
19
  module Helpers
11
20
  class PerfectButton < WrapIt::Container
12
21
  include TextContainer
13
22
  html_class 'button'
14
- enum :look, [:default, :success, :danger], html_class_prefix: 'button-'
23
+ enum :look, %i(default success danger), html_class_prefix: 'button-'
15
24
  switch :active, html_class: 'button-active'
16
- child :icon, [tag: 'img', class: 'button-icon']
25
+ child :icon, tag: 'img', class: 'button-icon'
17
26
  end
18
27
 
19
28
  register :p_button, 'PerfectButton'
@@ -55,11 +64,9 @@ This will produce following code:
55
64
 
56
65
  Note, that lines 2 and 3 produces same html markup.
57
66
 
58
- > **Wraning!** Module registration process changed since 0.2.0. So, if you migrate from 0.1.5, look above examples again and fix your startup code.
59
-
60
67
  # Status
61
68
 
62
- Project in pre-release state. First release version `1.0.0` planned to February of 2014.
69
+ This is a first release version - `1.0.0`.
63
70
 
64
71
  # Installation
65
72
 
@@ -87,25 +94,29 @@ Library have no specific configuration.
87
94
 
88
95
  # Usage
89
96
 
97
+ Now, package is well documented, so make sure to inspect [Reference documentation](http://rubydoc.info/github/cybernetlab/wrap_it/frames)
98
+
90
99
  > This library actively used in [BootstrapIt](https://github.com/cybernetlab/bootstrap_it) package, so explore this project, especially it's [lib/bootstrap_it/view_helpers](https://github.com/cybernetlab/bootstrap_it/tree/master/lib/bootstrap_it/view_helpers) folder for usage examples.
91
100
 
92
- All helpers classes derived from `WrapIt::Base` class, that provides allmost all functionality. For helpers, thats includes other helpers, use `WrapIt::Container` class. Where are some library-specific methods, defined directly in `WrapIt` module.
101
+ All helpers classes derived from `WrapIt::Base` class, that provides allmost all functionality. For helpers, thats includes other helpers, use `WrapIt::Container` class.
93
102
 
94
103
  Simple example explained above. More complex usage is to provide some logic to initalization, capturing and rendering process. To do this, use `after` or `before` `initialize`, `capture` and `reder` callbacks respectively. Usually `after` callbacks used. `initialize` callbacks runs around arguments and optioins parsing, `capture` callbacks runs around capturing element sections and `render` callbacks runs around wrapping content into element tag.
95
104
 
105
+ Also, please inspect arguments [module documentation](http://rubydoc.info/github/cybernetlab/wrap_it/WrapIt/Arguments) for details about creation arguments and options.
106
+
96
107
  Inside callbacks some usefull instance variables available.
97
108
 
98
- `@tag` contains tag name for element.
109
+ `tag` contains tag name for element.
99
110
 
100
- `@options` contains creation options hash. This hash also contains `:class` key with current set of HTML classes. But its recommended to use class-aware methods to manipulate html classes (see below). **Warning!** You **MUST** remove from this hash all your class-specific user options, because this hash will be used as list of HTML attributes of element.
111
+ `html_attr` contains HTML attributes hash.
101
112
 
102
- `@arguments` array available only in `after_initialize` callback and contains creation arguments. Its recommended to extract arguments, related to your class from this array if you plan to subclass your helper in future, so when subclasses `after_initialize` called these arguments will not available there.
113
+ `html_data` contains HTML data hash.
103
114
 
104
- Inside `capture` callback you deals with sections. This mechanism described in [Sections explained](https://github.com/cybernetlab/wrap_it/blob/master/sections_explained.md) article.
115
+ `html_class` contains array of HTML classes and provides array-like acces to its. See [class documentation](http://rubydoc.info/github/cybernetlab/wrap_it/WrapIt/HTMLClass) for details.
105
116
 
106
- `@rendered` string available in `render` callbacks and contains rendered content. You can change it to any value. If you want to include some html markup use `html_safe` method (see below) to prevent HTML escaping.
117
+ Inside `capture` callback you deals with sections. This mechanism explained in [module documentation](http://rubydoc.info/github/cybernetlab/wrap_it/WrapIt/Sections).
107
118
 
108
- `@template` contains rendering template. Use this variable carefully, so if you call `@template.link_to` or something else Rails-related, your library will not be portable to other frameworks. So, if you use this gem in user-end application, or Rails-only library, you are free to use all of `@template` methods.
119
+ `template` contains rendering template. Use this variable carefully, so if you call `template.link_to` or something else Rails-related, your library will not be portable to other frameworks. So, if you use this gem in user-end application, or Rails-only library, you are free to use all of `template` methods.
109
120
 
110
121
  *Examples*
111
122
 
@@ -113,7 +124,7 @@ Prevent user from changing element tag:
113
124
 
114
125
  ```ruby
115
126
  class Helper < WrapIt::Base
116
- after_initialize { @tag = 'table' }
127
+ after_initialize { self.tag = 'table' }
117
128
  end
118
129
  ```
119
130
 
@@ -121,9 +132,8 @@ Including some simple HTML into content
121
132
 
122
133
  ```ruby
123
134
  class IconHelper < WrapIt::Base
124
- after_initialize do
125
- @icon = optioins.delete(:icon)
126
- end
135
+ option :icon
136
+ attr_accessor :icon
127
137
 
128
138
  after_capture do
129
139
  unless @icon.nil?
@@ -214,6 +224,24 @@ Sets html class prefix. It can be `Symbol` or `String` and converted to `String`
214
224
 
215
225
  Once this method called from class, this class will ommit any text content, captured from template. For example, `<%= element do %><p>Any content</p><% end %>` normally will produce `<div><p>Any content</p></div>`. In some cases you whant to drop `<p>Any content</p>`, for exmaple, inside tables.
216
226
 
227
+ #### argument(name, first_only: false, after_options: false, **opts, &block)
228
+
229
+ Desclares argument for capturing on initialization process.
230
+
231
+ Inside initialization process, all arguments (except options hash), passed to constructor will be inspected to satisfy conditions, specified in `:if` and `:and` options. If this happens, and block given, it evaluated in context of component instance. If no block given, setter with `name` will be attempted to set value. In any way if conditions satisfied, argument removed from future processing.
232
+
233
+ If no conditions specified, the `name` of attribute taked as only condition.
234
+
235
+ #### option(name, after: nil, **opts, &block)
236
+
237
+ Desclares option for capturing on initialization process.
238
+
239
+ Provides same manner as `argument` but for hash of options, passed to constructor. Specified conditions are applied to options keys, not to values.
240
+
241
+ > Hint: you can specify argument and options with same name to call
242
+ > same setter.
243
+
244
+
217
245
  #### switch(name, options = {}, &block)
218
246
 
219
247
  Adds `switch`. Switch is a boolean flag. When element created, creation arguments will be scanned for `Symbol`, that equals to `name`. If it founded, switch turned on. Also creation options inspected. If its contains `name: true` key-value pair, this pair removed from options and switch also turned on. `name` can be `Symbol` or `String` and it converted to `Symbol`.
@@ -246,7 +274,7 @@ Adds one ore more sections to element. Refer to [Sections explained](https://git
246
274
 
247
275
  #### place(src, dst)
248
276
 
249
- Places section `src` to destination, specified in `dst` hash. `dst` is a single key-value Hash. Key can be `:before` and `:after`. Value can be `:begin`, `:end` or any section name. Refer to [Sections explained](https://github.com/cybernetlab/wrap_it/blob/master/sections_explained.md) article for description.
277
+ Places section `src` to destination, specified in `dst` hash. `dst` is a single key-value Hash. Key can be `:before` and `:after`. Value can be `:begin`, `:end` or any section name.
250
278
 
251
279
  #### sections
252
280
 
@@ -278,15 +306,11 @@ Returns array of html classes
278
306
 
279
307
  Sets html class(es) for element. Arguments can be `String`, `Symbol` or `Array` of it. All converted to plain array of `Symbols`. Duplicated classes removed.
280
308
 
281
- #### add_html_class(*args)
282
-
283
- Adds html class. For args see `#html_class=`
309
+ #### html_class << *args
284
310
 
285
- #### remove_html_class(*args)
311
+ You can add html classes as into array
286
312
 
287
- Removes html class. For args see `#html_class=`
288
-
289
- #### html_class?(*args, &block)
313
+ #### html_class.include?(*args, &block)
290
314
 
291
315
  Determines whether element contains class, satisfied by conditions, specified in method arguments.
292
316
 
@@ -301,68 +325,45 @@ In second form all arguments are ignored and for each comparison given block cal
301
325
  ```ruby
302
326
  # with `Symbol` or `String` conditions
303
327
  element.html_class = [:a, :b, :c]
304
- element.html_class?(:a) #=> true
305
- element.html_class?(:d) #=> false
306
- element.html_class?(:a, 'b') #=> true
307
- element.html_class?(:a, :d) #=> false
328
+ element.html_class.include?(:a) #=> true
329
+ element.html_class.include?(:d) #=> false
330
+ element.html_class.include?(:a, 'b') #=> true
331
+ element.html_class.include?(:a, :d) #=> false
308
332
 
309
333
  # with `Regexp` conditions
310
334
  element.html_class = [:some, :test]
311
- element.html_class?(/some/) #=> true
312
- element.html_class?(/some/, /bad/) #=> false
313
- element.html_class?(/some/, :test) #=> true
335
+ element.html_class.include?(/some/) #=> true
336
+ element.html_class.include?(/some/, /bad/) #=> false
337
+ element.html_class.include?(/some/, :test) #=> true
314
338
 
315
339
  # with `Array` conditions
316
340
  element.html_class = [:a, :b, :c]
317
- element.html_class?(%w(a d)) #=> true
318
- element.html_class?(%w(e d)) #=> false
341
+ element.html_class.include?(%w(a d)) #=> true
342
+ element.html_class.include?(%w(e d)) #=> false
319
343
 
320
344
  # with block
321
345
  element.html_class = [:a, :b, :c]
322
- element.html_class? { |x| x == 'a' } #=> true
346
+ element.html_class.include? { |x| x == 'a' } #=> true
323
347
  ```
324
348
 
325
- #### no_html_class?(*args, &block)
326
-
327
- Determines whether element doesn't contains class, satisfied by conditions, specified in method arguments. See `html_class?`.
328
-
329
- #### set_html_data(name, value)
330
-
331
- Sets HTML data attribute named `name` to `value`.
332
-
333
- #### remove_html_data(name)
334
-
335
- Removes HTML data attribute named `name`.
336
-
337
- ## WrapIt::Container
338
-
339
- This class used for elements, that can hold other elements.
340
-
341
- At first, note, that children can be created by two ways. First - inside template and second - from code. If child created from template, you have two choises again: first to keep child in place, where it defined in template, second - to cut it from there and place together with other childs. Two variables affects on this process: `ommit_content`, defined in `WrapIt::Base` and `extarct_children`. If `ommit_content` is `true`, all content will be dropped and children placed inside `children` section. If `extract_children` is true, children also placed into `children` section, but `content` is keeped.
342
349
 
343
- At second, you have two choises of moment, when children rendered. If `deffered_render` set to `false`, that is as default, all children will be rendered immideately after creation, so you can't change any of them later. If you plan to render your container after children, you chould set it to `true`, so childrens are collected in buffer and will be rendered with parent.
344
-
345
- All children, added to container injected with `render_to` and `parent` methods, that are gives you rendering section name and container itself.
346
-
347
- ### DSL methods
348
-
349
- #### child(name, *args, &block)
350
-
351
- Creates your own DSL method to create child items. In arguments, you should specify name of method. Then you can specify class name or class itself for child. If ommited, `WrapIt::Base` will be used. All other arguments will be mixed with arguments, specified by user and passed to child constructor. **Warning!** Make shure, that your child arguments don't begins with `String` if you ommit class argument. As workaround, don't ommit class argument and specify it as 'WrapIt::Base'. At last, you can define block, that will be called after creating child, but before its rendering. This child passed as argument to block.
352
-
353
- You can render children to section, other than `:children`. To do this, specify `section` option with section name.
354
-
355
- Look into [lib/bootstrap_it/view_helpers](https://github.com/cybernetlab/bootstrap_it/tree/master/lib/bootstrap_it/view_helpers) folder for usage examples.
350
+ Look to [Reference documentation](http://rubydoc.info/github/cybernetlab/wrap_it/frames) for other classes description.
356
351
 
357
352
  # Todo
358
353
 
359
354
  * Enlarge library functionality
360
- * Strong testing
361
355
  * Finish Sinatra integration
362
- * Rubydoc documentation
363
356
 
364
357
  # Changes
365
358
 
359
+ `1.0.0`
360
+ * first release version
361
+ * a lot of code refactored
362
+ * documentation allmost finished
363
+ * well test coverage
364
+ * API changed
365
+ * added: arguments and options processing
366
+
366
367
  `0.2.0`
367
368
  * added: sections mechanism
368
369
  * many fixes
data/lib/wrap_it.rb CHANGED
@@ -1,23 +1,23 @@
1
- require 'wrap_it/frameworks'
1
+ require File.join %w(wrap_it frameworks)
2
2
 
3
3
  if WrapIt.rails?
4
4
  require 'rails'
5
- require 'wrap_it/rails'
5
+ require File.join %w(wrap_it rails)
6
6
  else
7
- require 'wrap_it/no_rails'
7
+ require File.join %w(wrap_it no_rails)
8
8
  end
9
9
 
10
- require 'wrap_it/helpers'
10
+ require File.join %w(wrap_it helpers)
11
11
 
12
- require 'wrap_it/derived_attributes'
13
- require 'wrap_it/callbacks'
14
- require 'wrap_it/sections'
15
- require 'wrap_it/arguments_array'
16
- require 'wrap_it/html_class'
17
- require 'wrap_it/html_data'
18
- require 'wrap_it/switches'
19
- require 'wrap_it/enums'
20
- require 'wrap_it/base'
21
- require 'wrap_it/container'
22
- require 'wrap_it/text_container'
23
- require 'wrap_it/link'
12
+ require File.join %w(wrap_it derived_attributes)
13
+ require File.join %w(wrap_it callbacks)
14
+ require File.join %w(wrap_it capture_array)
15
+ require File.join %w(wrap_it arguments)
16
+ require File.join %w(wrap_it sections)
17
+ require File.join %w(wrap_it html)
18
+ require File.join %w(wrap_it switches)
19
+ require File.join %w(wrap_it enums)
20
+ require File.join %w(wrap_it base)
21
+ require File.join %w(wrap_it container)
22
+ require File.join %w(wrap_it text_container)
23
+ require File.join %w(wrap_it link)
@@ -0,0 +1,368 @@
1
+ module WrapIt
2
+ #
3
+ # This module responisble to parse creation arguments in component
4
+ # initialization process.
5
+ #
6
+ # Respect to ruby language, any method can take variable number of
7
+ # arguments and a hash of options. Also you can pass a block to it. So,
8
+ # when your component subclassed from WrapIt::Base, user can create its
9
+ # instances via helpers. And when such component initialized you should
10
+ # be able to process all arguments, passed to helper or constructor. Finally
11
+ # all unprocessed options setted as component html attributes.
12
+ #
13
+ # Two API methods provided for this purposes - `argument` and `option`.
14
+ # Each of them declares conditions for capturing some arguments and options.
15
+ # Conditions applies to arguments itself or to options keys. CapturedArray
16
+ # Array extension is used to capture arguments, so refer to its documentation
17
+ # for conditions details.
18
+ #
19
+ # @author Alexey Ovchinnikov <alexiss@cybernetlab.ru>
20
+ #
21
+ module Arguments
22
+ # Documentation includes
23
+ # @!parse extend Arguments::ClassMethods
24
+
25
+ # module implementation
26
+
27
+ extend DerivedAttributes
28
+
29
+ #
30
+ def self.included(base)
31
+ @base = base
32
+ base.extend ClassMethods
33
+ end
34
+
35
+ #
36
+ # {Arguments} Class methods to include
37
+ #
38
+ module ClassMethods
39
+ #
40
+ # Desclares argument for capturing on initialization process.
41
+ #
42
+ # Inside initialization process, all arguments (except options hash),
43
+ # passed to constructor will be inspected to satisfy conditions,
44
+ # specified in `:if` and `:and` options. If this happens, and block
45
+ # given, it evaluated in context of component instance. If no block
46
+ # given, setter with `name` will be attempted to set value. In any way
47
+ # if conditions satisfied, argument removed from future processing.
48
+ #
49
+ # If no conditions specified, the `name` of attribute taked as only
50
+ # condition.
51
+ #
52
+ # @example without conditions - name is a condition
53
+ # class Button < WrapIt::Base
54
+ # argument(:disabled) { |name, value| puts 'DISABLED' }
55
+ # end
56
+ #
57
+ # Button.new(template, :disabled) # => 'DISABLED'
58
+ # Button.new(template, 'disabled') # => nothing
59
+ #
60
+ # @example with conditions and setter
61
+ # class Button < WrapIt::Base
62
+ # argument :disabled, if: /^disable(?:d)?$/
63
+ #
64
+ # def disabled=(value)
65
+ # puts 'DISABLED'
66
+ # end
67
+ # end
68
+ #
69
+ # Button.new(template, :disabled) # => 'DISABLED'
70
+ # Button.new(template, 'disabled') # => 'DISABLED'
71
+ # Button.new(template, :disable) # => 'DISABLED'
72
+ # Button.new(template, 'some_text') # => nothing
73
+ #
74
+ # @overload argument(name, opts = {}, &block)
75
+ # @param name [Symbol] unique name, used to refer to this declaration
76
+ # @param opts [Hash] options
77
+ # @option opts [Object] :if one or array of conditions that should be
78
+ # satisfied to capture argument. See {CaptureArray} for details. If
79
+ # array given, conditions will be or'ed.
80
+ # @option opts [Object] :and additional one or array of conditions,
81
+ # that will be and'ed with :if conditions.
82
+ # @option opts [Boolean] :first_only (false) stop processing on first
83
+ # match
84
+ # @option opts [Boolean] :after_options (false) process this argument
85
+ # after options
86
+ # @yield [name, value] yields every time argument captured. Evaluated
87
+ # in instance context
88
+ # @yieldparam name [Symbol] name of argument, specified in name param
89
+ # above
90
+ # @yieldparam value [Object] real argument value
91
+ #
92
+ # @return [void]
93
+ # @since 1.0.0
94
+ def argument(name, first_only: false, after_options: false,
95
+ **opts, &block)
96
+ name.is_a?(String) && name = name.to_sym
97
+ fail ArgumentError, 'Wrong name' unless name.is_a?(Symbol)
98
+ arguments[name] = {
99
+ name: name,
100
+ conditions: Arguments.make_conditions(name, **opts),
101
+ block: block,
102
+ first_only: first_only == true,
103
+ after_options: after_options == true
104
+ }
105
+ end
106
+
107
+ #
108
+ # Desclares option for capturing on initialization process.
109
+ #
110
+ # Provides same manner as {#argument} but for hash of options, passed
111
+ # to constructor. Specified conditions are applied to options keys, not
112
+ # to values.
113
+ #
114
+ # > Hint: you can specify argument and options with same name to call
115
+ # > same setter.
116
+ #
117
+ # @example shared setter
118
+ # class Button < WrapIt::Base
119
+ # REGEXP = /^disable(?:d)?$/
120
+ #
121
+ # argument :disabled, if: REGEXP
122
+ # option :disabled, if: %i(disable disabled)
123
+ #
124
+ # def disabled=(value)
125
+ # if value == true || REGEXP =~ value.to_s
126
+ # puts 'DISABLED'
127
+ # end
128
+ # end
129
+ # end
130
+ #
131
+ # Button.new(template, :disabled) # => 'DISABLED'
132
+ # Button.new(template, 'disabled') # => 'DISABLED'
133
+ # Button.new(template, :disable) # => 'DISABLED'
134
+ # Button.new(template, disabled: true) # => 'DISABLED'
135
+ # Button.new(template, disable: true) # => 'DISABLED'
136
+ # Button.new(template, disable: false) # => nothing
137
+ # Button.new(template, 'some_text') # => nothing
138
+ #
139
+ # @overload option(name, opts = {}, &block)
140
+ # @param name [Symbol] unique name, used to refer to this declaration
141
+ # @param opts [Hash] options
142
+ # @option opts [Object] :if see
143
+ # {WrapIt::Arguments::ClassMethods#argument}
144
+ # @option opts [Object] :and see
145
+ # {WrapIt::Arguments::ClassMethods#argument}
146
+ # @yield [name, value] yields every time option captured. Evaluated
147
+ # in instance context
148
+ # @yieldparam name [Symbol] name of option, specified in name param
149
+ # above
150
+ # @yieldparam value [Object] real option value
151
+ #
152
+ # @return [void]
153
+ # @since 1.0.0
154
+ def option(name, after: nil, **opts, &block)
155
+ name.is_a?(String) && name = name.to_sym
156
+ fail ArgumentError, 'Wrong name' unless name.is_a?(Symbol)
157
+ @dependencies = !after.nil?
158
+ options[name] = {
159
+ name: name,
160
+ conditions: Arguments.make_conditions(name, **opts),
161
+ block: block
162
+ }
163
+ end
164
+
165
+
166
+ #
167
+ # Capture arguments for class and it's ancestors. All captured arguments
168
+ # and options will be extracted from original `args` argument.
169
+ #
170
+ # Actually you rare needs to call this method directly. For example
171
+ # you can call it in instance
172
+ # {Arguments#capture_arguments! capture_arguments!}
173
+ # override to capture arguments for some child components.
174
+ #
175
+ # @example capturing arguments for child component
176
+ # class Button < WrapIt::Base
177
+ # option(:color) { |name, value| puts "BUTTON COLOR IS: #{value}" }
178
+ # end
179
+ #
180
+ # class Toolbar < WrapIt::Base
181
+ # protected
182
+ # def capture_arguments!(args, &block)
183
+ # @button = Button.new(Button.capture_arguments!(args))
184
+ # super(args, &block) # ! don't forget to call parent method
185
+ # end
186
+ # end
187
+ #
188
+ # Toolbar.new(template, color: :red) # => 'BUTTON COLOR IS red'
189
+ #
190
+ # @overload capture_arguments!(args, opts = {}, &block)
191
+ # @param args [Array<Object>] arguments to process (include options)
192
+ # @param opts [Hash] options
193
+ # @option opts [Boolean] :inherited (true) process ancestors
194
+ # @option opts [Base] :instance (nil) if specified valid instance,
195
+ # all {ClassMethods#argument} and {ClassMethods#option} blocks will
196
+ # and setters will be called.
197
+ # @param &block [Proc] block, passed to constructor if present
198
+ #
199
+ # @return [Array<Object>] captured arguments
200
+ def capture_arguments!(args, inherited = true, instance = nil, &block)
201
+ opts = args.extract_options!
202
+ if inherited
203
+ ancestors.take_while { |a| a != Arguments.base }
204
+ .reverse
205
+ .unshift(Arguments.base)
206
+ .map do |a|
207
+ next unless a.methods.include?(:extract_for_class)
208
+ a.extract_for_class(args, opts, instance, &block)
209
+ end
210
+ result_args = collect_derived(:@provided_arguments, {}, :merge)
211
+ .values
212
+ .flatten
213
+ result_opts = collect_derived(:@provided_options, {}, :merge)
214
+ .values
215
+ .reduce({}) { |a, e| a.merge!(e) }
216
+ else
217
+ extract_for_class(args, opts, instance, &block)
218
+ result_args = @provided_arguments.values.flatten
219
+ result_opts = @provided_options
220
+ .values
221
+ .reduce({}) { |a, e| a.merge!(e) }
222
+ end
223
+ opts.empty? || args << opts
224
+ result_opts.empty? || result_args << result_opts
225
+ result_args
226
+ end
227
+
228
+ protected
229
+
230
+ attr_reader :provided_options, :provided_arguments, :provided_block
231
+
232
+ def option_provided?(*list)
233
+ return false if provided_options.nil?
234
+ if list.empty?
235
+ return provided_options.empty?
236
+ else
237
+ return list.all? do |option|
238
+ provided_options.key?(option)
239
+ end
240
+ end
241
+ end
242
+
243
+ def argument_provided?(*list)
244
+ return false if provided_arguments.nil?
245
+ if list.empty?
246
+ return provided_arguments.empty?
247
+ else
248
+ return list.all? do |arg|
249
+ provided_arguments.key?(arg)
250
+ end
251
+ end
252
+ end
253
+
254
+ def block_provided?
255
+ provided_block.is_a?(Proc)
256
+ end
257
+
258
+ def options
259
+ @options ||= {}
260
+ end
261
+
262
+ def arguments
263
+ @arguments ||= {}
264
+ end
265
+
266
+ def extract_args(args, list, instance = nil)
267
+ args.respond_to?(:extract!) || args.extend(WrapIt::CaptureArray)
268
+ list.each do |arg|
269
+ processed =
270
+ if arg[:first_only]
271
+ [args.capture_first!(*arg[:conditions])].compact
272
+ else
273
+ args.capture!(*arg[:conditions])
274
+ end
275
+ (provided_arguments[arg[:name]] ||= []).concat(processed)
276
+ next if instance.nil?
277
+ processed.each do |v|
278
+ instance.instance_exec(arg[:name], v, arg[:block], &SETTER)
279
+ end
280
+ end
281
+ end
282
+
283
+ def extract_opts(opts, instance = nil)
284
+ keys = opts.keys.extend(WrapIt::CaptureArray)
285
+ options.each do |name, opt|
286
+ (provided_options[name] ||= {}).merge!(Hash[
287
+ keys.capture!(*opt[:conditions])
288
+ .map do |key|
289
+ value = opts.delete(key)
290
+ unless instance.nil?
291
+ instance.instance_exec(key, value, opt[:block], &SETTER)
292
+ end
293
+ [key, value]
294
+ end
295
+ ])
296
+ end
297
+ end
298
+
299
+ def extract_for_class(args, opts, instance = nil, &block)
300
+ @provided_options = {}
301
+ @provided_arguments = {}
302
+ @provided_block = block
303
+
304
+ after, before = arguments.values.partition { |x| x[:after_options] }
305
+
306
+ extract_args(args, before, instance)
307
+ extract_opts(opts, instance)
308
+ extract_args(args, after, instance)
309
+
310
+ @provided_block = nil
311
+ end
312
+ end
313
+
314
+ protected
315
+
316
+ # @!visibility public
317
+ #
318
+ # Captures arguments
319
+ #
320
+ # In rare cases you can override this method to control directly arguments
321
+ # capturing process. Refer to
322
+ # {ClassMethods#capture_arguments! capture_arguments!} for examples.
323
+ #
324
+ # > Note that this method is `protected`, so override should be `protected`
325
+ # > too.
326
+ #
327
+ # @param args [Array<Object>] arguments, passed to constructor
328
+ # @param block [Proc] block, passed to constructor
329
+ #
330
+ # @return [Array<Object>] captured arguments
331
+ def capture_arguments!(args, &block)
332
+ self.class.capture_arguments!(args, true, self, &block)
333
+ end
334
+
335
+ private
336
+
337
+ #
338
+ # Evaluated in instance context by .extract_for_class
339
+ #
340
+ SETTER = ->(name, value, block) do
341
+ if block.nil?
342
+ setter = "#{name}=".to_sym
343
+ respond_to?(setter) && send(setter, value)
344
+ else
345
+ instance_exec(name, value, &block)
346
+ end
347
+ end
348
+
349
+ def self.normalize_conditions(cond)
350
+ if cond.is_a?(Array) &&
351
+ cond.any? { |x| !x.is_a?(Symbol) && !x.is_a?(String) }
352
+ cond
353
+ else
354
+ [cond]
355
+ end
356
+ end
357
+
358
+ def self.make_conditions(name, **opts)
359
+ cond = normalize_conditions(opts.key?(:if) ? opts[:if] : name)
360
+ opts.key?(:and) && cond << {and: normalize_conditions(opts[:and])}
361
+ cond
362
+ end
363
+
364
+ def self.base
365
+ @base
366
+ end
367
+ end
368
+ end