ruby-features 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +243 -1
- data/lib/generators/ruby_features/templates/ruby-features.rb +1 -1
- data/lib/ruby-features.rb +28 -11
- data/lib/ruby-features/concern/apply_to.rb +86 -0
- data/lib/ruby-features/concern/feature.rb +65 -0
- data/lib/ruby-features/conditions.rb +72 -0
- data/lib/ruby-features/lazy.rb +2 -15
- data/lib/ruby-features/mixins.rb +7 -16
- data/lib/ruby-features/utils.rb +24 -20
- data/lib/ruby-features/utils/const_accessor_19.rb +10 -22
- data/lib/ruby-features/utils/inflector.rb +17 -0
- data/lib/ruby-features/utils/inflector_active_support.rb +15 -0
- data/lib/ruby-features/version.rb +1 -1
- data/spec/conditions_spec.rb +85 -0
- data/spec/define_spec.rb +14 -5
- data/spec/lazy_apply_spec.rb +1 -1
- data/spec/require_features_spec.rb +38 -0
- metadata +11 -4
- data/lib/ruby-features/concern.rb +0 -50
- data/lib/ruby-features/single.rb +0 -36
data/README.md
CHANGED
@@ -1,4 +1,246 @@
|
|
1
1
|
Ruby Features
|
2
2
|
=============
|
3
|
+
[](http://badge.fury.io/rb/ruby-features)
|
4
|
+
[](https://travis-ci.org/stokarenko/ruby-features)
|
5
|
+
[](https://codeclimate.com/github/stokarenko/ruby-features)
|
6
|
+
[](https://codeclimate.com/github/stokarenko/ruby-features/coverage)
|
3
7
|
|
4
|
-
Ruby Features makes extending of Ruby classes and modules to be easy, safe and controlled.
|
8
|
+
Ruby Features makes the extending of Ruby classes and modules to be easy, safe and controlled.
|
9
|
+
|
10
|
+
## Why?
|
11
|
+
Lets ask, is good to write the code like this:
|
12
|
+
```ruby
|
13
|
+
String.send :include, MyStringExtension
|
14
|
+
```
|
15
|
+
|
16
|
+
Or even like this:
|
17
|
+
```ruby
|
18
|
+
Object.class_eval do
|
19
|
+
def my_object_method
|
20
|
+
# some super code
|
21
|
+
end
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
The question is about motivation to write such things. Lets skip well-known reason like
|
26
|
+
> Because I can! That is Ruby baby, lets make some anarchy!
|
27
|
+
|
28
|
+
but say:
|
29
|
+
> I want to implement the functionality, which I expected to find right in the box.
|
30
|
+
|
31
|
+
In fact, the cool things can be injected right in core programming entities in this way.
|
32
|
+
They are able to improve the development speed, to make the sources to be more readable and light.
|
33
|
+
|
34
|
+
From the other side, the project's behavior loses the predictability once it requires the third-party
|
35
|
+
library, infected by massive patches to core entities.
|
36
|
+
|
37
|
+
Ruby Features goal is to take that under control.
|
38
|
+
|
39
|
+
The main features are:
|
40
|
+
* No any dependencies;
|
41
|
+
* Built-in lazy load;
|
42
|
+
* Supports ActiveSupport lazy load as well;
|
43
|
+
* Stimulates the clear extending, but prevents monkey patching;
|
44
|
+
* Gives the control what core extensions to apply;
|
45
|
+
* Any moment gives the knowledge who and how exactly affected to programming entities.
|
46
|
+
|
47
|
+
## requirements
|
48
|
+
* Ruby >= 1.9.3
|
49
|
+
|
50
|
+
## Getting started
|
51
|
+
|
52
|
+
Add to your Gemfile:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
gem 'ruby-features'
|
56
|
+
```
|
57
|
+
|
58
|
+
Run the bundle command to install it.
|
59
|
+
|
60
|
+
For Rails projects, gun generator:
|
61
|
+
```console
|
62
|
+
rails generate ruby_features:install
|
63
|
+
```
|
64
|
+
|
65
|
+
Generator will add `ruby-features.rb` initializer, which loads the ruby features
|
66
|
+
from `{Rails.root}/lib/features` folder. Also such initializer is a good place
|
67
|
+
to apply third-party features.
|
68
|
+
|
69
|
+
## Usage
|
70
|
+
### Feature definition
|
71
|
+
Feature file name should ends with `_feature.rb`.
|
72
|
+
|
73
|
+
Lets define the feature in `lib/features/something_useful_feature.rb`:
|
74
|
+
```ruby
|
75
|
+
RubyFeatures.define 'some_namespace/something_useful' do
|
76
|
+
apply_to 'ActiveRecord::Base' do
|
77
|
+
|
78
|
+
applied do
|
79
|
+
# will be evaluated on target class
|
80
|
+
attr_accessor :useful_variable
|
81
|
+
end
|
82
|
+
|
83
|
+
instance_methods do
|
84
|
+
# instance methods
|
85
|
+
def useful_instance_method
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class_methods do
|
90
|
+
# class methods
|
91
|
+
def useful_class_method
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
apply_to 'ActiveRecord::Relation' do
|
96
|
+
# feature can contain several apply_to definition
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
### Dependencies
|
104
|
+
The dependencies from other Ruby Features can be defined like:
|
105
|
+
```ruby
|
106
|
+
RubyFeatures.define 'main_feature' do
|
107
|
+
dependency 'dependent_feature1'
|
108
|
+
dependencies 'dependent_feature2', 'dependent_feature3'
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
### Conditions
|
113
|
+
Sometimes is required to apply different things, dependent on some criteria:
|
114
|
+
```ruby
|
115
|
+
RubyFeatures.define 'some_namespace/something_useful' do
|
116
|
+
apply_to 'ActiveRecord::Base' do
|
117
|
+
|
118
|
+
class_methods do
|
119
|
+
if ActiveRecord::VERSION::MAJOR > 3
|
120
|
+
def useful_method
|
121
|
+
# Implementation for newest ActiveRecord
|
122
|
+
end
|
123
|
+
else
|
124
|
+
def useful_method
|
125
|
+
# Implementation for ActiveRecord 3
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
It's bad to do like that, because the mixin applied by Ruby Features became to be not static.
|
134
|
+
That cause not predictable behavior.
|
135
|
+
|
136
|
+
Ruby Features provides the `conditions` mechanism to avoid such problem:
|
137
|
+
```ruby
|
138
|
+
RubyFeatures.define 'some_namespace/something_useful' do
|
139
|
+
condition(:newest_activerecord){ ActiveRecord::VERSION::MAJOR > 3 }
|
140
|
+
|
141
|
+
apply_to 'ActiveRecord::Base' do
|
142
|
+
|
143
|
+
class_methods if: :newest_activerecord do
|
144
|
+
def useful_method
|
145
|
+
# Implementation for newest ActiveRecord
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class_methods unless: :newest_activerecord do
|
150
|
+
def useful_method
|
151
|
+
# Implementation for ActiveRecord 3
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
All feature definition helpers supports the conditions:
|
160
|
+
```ruby
|
161
|
+
apply_to 'ActiveRecord::Base', if: :first_criteria do; end
|
162
|
+
|
163
|
+
applied if: :second_criteria do; end
|
164
|
+
|
165
|
+
class_methods if: :third_criteria do; end
|
166
|
+
|
167
|
+
instance_methods if: :fourth_criteria do; end
|
168
|
+
```
|
169
|
+
|
170
|
+
It's possible to define not boolean condition:
|
171
|
+
```ruby
|
172
|
+
RubyFeatures.define 'some_namespace/something_useful' do
|
173
|
+
condition(:activerecord_version){ ActiveRecord::VERSION::MAJOR }
|
174
|
+
|
175
|
+
apply_to 'ActiveRecord::Base' do
|
176
|
+
|
177
|
+
class_methods unless: {activerecord_version: 3} do
|
178
|
+
def useful_method
|
179
|
+
# Implementation for newest ActiveRecord
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class_methods if: {activerecord_version: 3} do
|
184
|
+
def useful_method
|
185
|
+
# Implementation for ActiveRecord 3
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
It's possible to combine the conditions:
|
194
|
+
```ruby
|
195
|
+
class_methods {
|
196
|
+
if: [
|
197
|
+
:boolean_condition,
|
198
|
+
:other_boolean_condition,
|
199
|
+
{
|
200
|
+
string_condition: 'some_string',
|
201
|
+
symbol_condition: :some_symbol
|
202
|
+
}
|
203
|
+
],
|
204
|
+
unless: :unless_boolean_condition
|
205
|
+
} do; end
|
206
|
+
```
|
207
|
+
|
208
|
+
### Feature loading
|
209
|
+
Feature can be loaded by normal `require` call:
|
210
|
+
```ruby
|
211
|
+
require `lib/features/something_useful_feature`
|
212
|
+
```
|
213
|
+
|
214
|
+
All features within path can be loaded as follows:
|
215
|
+
```ruby
|
216
|
+
# require all "*_feature.rb" files within path, recursively:
|
217
|
+
RubyFeatures.find_in_path(File.expand_path('../lib/features', __FILE__))
|
218
|
+
```
|
219
|
+
|
220
|
+
### Feature applying
|
221
|
+
Feature can be applied immediately after it's definition:
|
222
|
+
```ruby
|
223
|
+
RubyFeatures.define 'some_namespace/something_useful' do
|
224
|
+
# definition
|
225
|
+
end.apply
|
226
|
+
```
|
227
|
+
|
228
|
+
Features can be applied right after loading from path:
|
229
|
+
```ruby
|
230
|
+
RubyFeatures.find_in_path(File.expand_path('../lib/features', __FILE__)).apply_all
|
231
|
+
```
|
232
|
+
|
233
|
+
Feature can be applied by name, if such feature is already loaded:
|
234
|
+
```ruby
|
235
|
+
require `lib/features/something_useful_feature`
|
236
|
+
|
237
|
+
RubyFeatures.apply 'some_namespace/something_useful'
|
238
|
+
```
|
239
|
+
|
240
|
+
## Changes
|
241
|
+
### v1.1.0
|
242
|
+
* Added conditions.
|
243
|
+
* Added dependencies.
|
244
|
+
|
245
|
+
## License
|
246
|
+
MIT License. Copyright (c) 2015 Sergey Tokarenko
|
@@ -1 +1 @@
|
|
1
|
-
RubyFeatures.find_in_path(Rails.root.join('
|
1
|
+
RubyFeatures.find_in_path(Rails.root.join('lib/features')).apply_all
|
data/lib/ruby-features.rb
CHANGED
@@ -1,45 +1,62 @@
|
|
1
1
|
require 'ruby-features/version'
|
2
2
|
|
3
3
|
module RubyFeatures
|
4
|
+
autoload :Conditions, 'ruby-features/conditions'
|
4
5
|
autoload :Container, 'ruby-features/container'
|
5
|
-
autoload :Single, 'ruby-features/single'
|
6
6
|
autoload :Mixins, 'ruby-features/mixins'
|
7
|
-
autoload :Concern, 'ruby-features/concern'
|
8
7
|
autoload :Utils, 'ruby-features/utils'
|
9
8
|
autoload :Lazy, 'ruby-features/lazy'
|
10
9
|
|
10
|
+
module Concern
|
11
|
+
autoload :ApplyTo, 'ruby-features/concern/apply_to'
|
12
|
+
autoload :Feature, 'ruby-features/concern/feature'
|
13
|
+
end
|
14
|
+
|
11
15
|
module Generators
|
12
16
|
autoload :InstallGenerator, 'generators/ruby-features/install_generator'
|
13
17
|
end
|
14
18
|
|
15
|
-
|
19
|
+
class ApplyError < StandardError; end
|
20
|
+
|
21
|
+
@features = {}
|
22
|
+
|
23
|
+
@active_support_available = begin
|
24
|
+
require 'active_support'
|
25
|
+
true
|
26
|
+
rescue LoadError
|
27
|
+
false
|
28
|
+
end
|
16
29
|
|
17
30
|
class << self
|
18
31
|
def find_in_path(*folders)
|
19
|
-
old_feature_names =
|
32
|
+
old_feature_names = @features.keys
|
20
33
|
|
21
34
|
Dir[*folders.map{|folder| File.join(folder, '**', '*_feature.rb') }].each do |file|
|
22
35
|
require file
|
23
36
|
end
|
24
37
|
|
25
|
-
Container.new(
|
38
|
+
Container.new(@features.keys - old_feature_names)
|
26
39
|
end
|
27
40
|
|
28
41
|
def define(feature_name, &feature_body)
|
29
|
-
|
30
|
-
|
31
|
-
raise NameError.new("Such feature is already registered: #{feature_name}") if
|
42
|
+
feature_name = feature_name.to_s
|
43
|
+
raise NameError.new("Wrong feature name: #{name}") unless feature_name.match(/^[\/_a-z\d]+$/)
|
44
|
+
raise NameError.new("Such feature is already registered: #{feature_name}") if @features.has_key?(feature_name)
|
32
45
|
|
33
|
-
|
46
|
+
@features[feature_name] = Mixins.new(feature_name, feature_body)
|
34
47
|
end
|
35
48
|
|
36
49
|
def apply(*feature_names)
|
37
50
|
feature_names.each do |feature_name|
|
38
|
-
raise NameError.new("Such feature is not registered: #{feature_name}") unless
|
51
|
+
raise NameError.new("Such feature is not registered: #{feature_name}") unless @features.has_key?(feature_name)
|
39
52
|
|
40
|
-
|
53
|
+
@features[feature_name].apply
|
41
54
|
end
|
42
55
|
end
|
43
56
|
|
57
|
+
def active_support_available?
|
58
|
+
@active_support_available
|
59
|
+
end
|
60
|
+
|
44
61
|
end
|
45
62
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module RubyFeatures
|
2
|
+
module Concern
|
3
|
+
module ApplyTo
|
4
|
+
|
5
|
+
def _apply(target, apply_to_definitions, conditions)
|
6
|
+
_with_instance_variable(:@_conditions, conditions) do
|
7
|
+
_build_mixins(apply_to_definitions)
|
8
|
+
end
|
9
|
+
|
10
|
+
target_class = RubyFeatures::Utils.ruby_const_get(self, "::#{target}")
|
11
|
+
|
12
|
+
_apply_methods(target_class)
|
13
|
+
_apply_applied_blocks(target_class)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def self.extended(base)
|
19
|
+
base.instance_variable_set(:@_applied_blocks, [])
|
20
|
+
end
|
21
|
+
|
22
|
+
def applied(asserts = {}, &block)
|
23
|
+
if @_conditions.match?(asserts, false)
|
24
|
+
@_applied_blocks << block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def class_methods(asserts = {}, &block)
|
29
|
+
_methods('Extend', asserts, block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def instance_methods(asserts = {}, &block)
|
33
|
+
_methods('Include', asserts, block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def _apply_methods(target_class)
|
37
|
+
constants.each do |constant|
|
38
|
+
mixin_method, existing_methods_method = case(constant)
|
39
|
+
when /^Extend/ then [:extend, :methods]
|
40
|
+
when /^Include/ then [:include, :instance_methods]
|
41
|
+
else raise ArgumentError.new("Wrong mixin constant: #{constant}")
|
42
|
+
end
|
43
|
+
|
44
|
+
mixin = const_get(constant)
|
45
|
+
|
46
|
+
common_methods = target_class.public_send(existing_methods_method) & mixin.instance_methods
|
47
|
+
raise NameError.new("Tried to #{mixin_method} already existing methods: #{common_methods.inspect}") unless common_methods.empty?
|
48
|
+
|
49
|
+
target_class.send(mixin_method, mixin)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def _apply_applied_blocks(target_class)
|
54
|
+
@_applied_blocks.each do |applied_block|
|
55
|
+
target_class.class_eval(&applied_block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def _methods(mixin_prefix, asserts, block)
|
60
|
+
asserts = @_conditions.normalize_asserts(asserts)
|
61
|
+
|
62
|
+
if @_conditions.match?(asserts)
|
63
|
+
RubyFeatures::Utils.prepare_module(self, "#{mixin_prefix}#{@_conditions.build_constant_postfix(@_global_asserts, asserts)}").class_eval(&block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def _with_instance_variable(variable_name, value)
|
68
|
+
instance_variable_set(variable_name, value)
|
69
|
+
yield
|
70
|
+
remove_instance_variable(variable_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def _build_mixins(apply_to_definitions)
|
74
|
+
apply_to_definitions.each do |apply_to_definition|
|
75
|
+
global_asserts = @_conditions.normalize_asserts(apply_to_definition[:asserts])
|
76
|
+
if @_conditions.match?(global_asserts)
|
77
|
+
_with_instance_variable(:@_global_asserts, global_asserts) do
|
78
|
+
class_eval(&apply_to_definition[:block])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module RubyFeatures
|
2
|
+
module Concern
|
3
|
+
module Feature
|
4
|
+
|
5
|
+
def apply
|
6
|
+
unless applied?
|
7
|
+
RubyFeatures.apply(*@_dependencies)
|
8
|
+
|
9
|
+
@_apply_to_definitions.keys.each do |target|
|
10
|
+
RubyFeatures::Lazy.apply(target) do
|
11
|
+
_lazy_apply(target)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
@_applied = true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def applied?
|
20
|
+
@_applied
|
21
|
+
end
|
22
|
+
|
23
|
+
def _set_name(name)
|
24
|
+
@_name = name
|
25
|
+
end
|
26
|
+
|
27
|
+
def name
|
28
|
+
@_name
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def self.extended(base)
|
34
|
+
base.instance_variable_set(:@_dependencies, [])
|
35
|
+
base.instance_variable_set(:@_conditions, RubyFeatures::Conditions.new)
|
36
|
+
base.instance_variable_set(:@_apply_to_definitions, {})
|
37
|
+
base.instance_variable_set(:@_applied, false)
|
38
|
+
end
|
39
|
+
|
40
|
+
def dependencies(*feature_names)
|
41
|
+
@_dependencies.push(*feature_names).uniq!
|
42
|
+
end
|
43
|
+
alias dependency dependencies
|
44
|
+
|
45
|
+
def condition(condition_name, &block)
|
46
|
+
@_conditions.push(condition_name, block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def apply_to(target, asserts = {}, &block)
|
50
|
+
target = target.to_s
|
51
|
+
|
52
|
+
(@_apply_to_definitions[target] ||= []) << { asserts: asserts, block: block }
|
53
|
+
end
|
54
|
+
|
55
|
+
def _lazy_apply(target)
|
56
|
+
apply_to_module = RubyFeatures::Utils.prepare_module!(self, target)
|
57
|
+
apply_to_module.extend RubyFeatures::Concern::ApplyTo
|
58
|
+
apply_to_module._apply(target, @_apply_to_definitions.delete(target), @_conditions)
|
59
|
+
rescue NameError => e
|
60
|
+
raise ApplyError.new("[#{name}] #{e.message}")
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module RubyFeatures
|
2
|
+
class Conditions
|
3
|
+
def initialize
|
4
|
+
@conditions = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def push(condition_name, block)
|
8
|
+
condition_name = condition_name.to_sym
|
9
|
+
raise NameError.new("Such condition is already defined: #{condition_name}") if @conditions.has_key?(condition_name)
|
10
|
+
|
11
|
+
@conditions[condition_name] = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def normalize_asserts(asserts)
|
15
|
+
asserts.inject({if: {}, unless: {}}) do |mem, (assert_type, asserts_per_type)|
|
16
|
+
asserts_per_type = [asserts_per_type] unless asserts_per_type.kind_of?(Array)
|
17
|
+
asserts_per_type.each do |assert_per_type|
|
18
|
+
assert_per_type = {assert_per_type => true} unless assert_per_type.kind_of?(Hash)
|
19
|
+
mem[assert_type].merge!(assert_per_type)
|
20
|
+
end
|
21
|
+
|
22
|
+
mem
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def match?(asserts, normalized = true)
|
27
|
+
asserts = normalize_asserts(asserts) unless normalized
|
28
|
+
|
29
|
+
asserts[:if].each do |condition_name, condition_value|
|
30
|
+
return false unless value(condition_name) == condition_value
|
31
|
+
end
|
32
|
+
|
33
|
+
asserts[:unless].each do |condition_name, condition_value|
|
34
|
+
return false if value(condition_name) == condition_value
|
35
|
+
end
|
36
|
+
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_constant_postfix(*asserts)
|
41
|
+
asserts = merge_asserts(asserts)
|
42
|
+
|
43
|
+
RubyFeatures::Utils.camelize(
|
44
|
+
asserts.sort.map { |assert_type, asserts_per_type|
|
45
|
+
if asserts_per_type.empty?
|
46
|
+
nil
|
47
|
+
else
|
48
|
+
"#{assert_type}_" + asserts_per_type.sort.map { |condition_name, condition_value|
|
49
|
+
"#{condition_name}_is_#{condition_value}"
|
50
|
+
}.join('_and_')
|
51
|
+
end
|
52
|
+
}.compact.join('_and_')
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def value(condition_name)
|
59
|
+
@conditions[condition_name] = @conditions[condition_name].call if @conditions[condition_name].respond_to?(:call)
|
60
|
+
@conditions[condition_name]
|
61
|
+
end
|
62
|
+
|
63
|
+
def merge_asserts(asserts)
|
64
|
+
asserts.inject({if: {}, unless: {}}) do |mem, assert|
|
65
|
+
mem[:if].merge!(assert[:if])
|
66
|
+
mem[:unless].merge!(assert[:unless])
|
67
|
+
mem
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
data/lib/ruby-features/lazy.rb
CHANGED
@@ -7,26 +7,13 @@ module RubyFeatures
|
|
7
7
|
i18n
|
8
8
|
).map(&:to_sym).freeze
|
9
9
|
|
10
|
-
@active_support_available = begin
|
11
|
-
require 'active_support'
|
12
|
-
true
|
13
|
-
rescue LoadError
|
14
|
-
false
|
15
|
-
end
|
16
|
-
|
17
10
|
class << self
|
18
|
-
def active_support_available?
|
19
|
-
@active_support_available
|
20
|
-
end
|
21
|
-
|
22
11
|
def apply(target, &block)
|
23
|
-
if active_support_available?
|
12
|
+
if RubyFeatures.active_support_available?
|
24
13
|
target_namespace = RubyFeatures::Utils.underscore(target.split('::').first).to_sym
|
25
14
|
|
26
15
|
if ACTIVE_SUPPORT_LAZY_TARGETS.include?(target_namespace)
|
27
|
-
ActiveSupport.on_load target_namespace, yield: true, &block
|
28
|
-
|
29
|
-
return
|
16
|
+
return ActiveSupport.on_load target_namespace, yield: true, &block
|
30
17
|
end
|
31
18
|
end
|
32
19
|
|
data/lib/ruby-features/mixins.rb
CHANGED
@@ -2,26 +2,17 @@ module RubyFeatures
|
|
2
2
|
module Mixins
|
3
3
|
class << self
|
4
4
|
|
5
|
-
def
|
6
|
-
|
5
|
+
def new(feature_name, feature_body)
|
6
|
+
RubyFeatures::Utils.prepare_module!(
|
7
7
|
self,
|
8
|
-
RubyFeatures::Utils.
|
9
|
-
)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
_lazy_build_and_apply!(feature, feature_module, target, blocks)
|
14
|
-
end
|
8
|
+
RubyFeatures::Utils.camelize(feature_name)
|
9
|
+
).tap do |feature_module|
|
10
|
+
feature_module.extend RubyFeatures::Concern::Feature
|
11
|
+
feature_module._set_name(feature_name)
|
12
|
+
feature_module.class_eval(&feature_body) if feature_body
|
15
13
|
end
|
16
14
|
end
|
17
15
|
|
18
|
-
def _lazy_build_and_apply!(feature, feature_module, target, blocks)
|
19
|
-
lazy_module = RubyFeatures::Utils.prepare_module!(feature_module, target)
|
20
|
-
lazy_module.extend RubyFeatures::Concern
|
21
|
-
blocks.each { |block| lazy_module.class_eval(&block) }
|
22
|
-
lazy_module._apply(target, feature.name)
|
23
|
-
end
|
24
|
-
|
25
16
|
end
|
26
17
|
end
|
27
18
|
end
|
data/lib/ruby-features/utils.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
module RubyFeatures
|
2
2
|
module Utils
|
3
3
|
|
4
|
-
autoload :ConstAccessor19,
|
5
|
-
autoload :ConstAccessor20,
|
4
|
+
autoload :ConstAccessor19, 'ruby-features/utils/const_accessor_19'
|
5
|
+
autoload :ConstAccessor20, 'ruby-features/utils/const_accessor_20'
|
6
|
+
|
7
|
+
autoload :Inflector, 'ruby-features/utils/inflector'
|
8
|
+
autoload :InflectorActiveSupport, 'ruby-features/utils/inflector_active_support'
|
6
9
|
|
7
10
|
begin
|
8
11
|
const_defined?('Some::Const')
|
@@ -11,6 +14,10 @@ module RubyFeatures
|
|
11
14
|
extend ConstAccessor19
|
12
15
|
end
|
13
16
|
|
17
|
+
extend RubyFeatures.active_support_available? ?
|
18
|
+
InflectorActiveSupport :
|
19
|
+
Inflector
|
20
|
+
|
14
21
|
class << self
|
15
22
|
|
16
23
|
def module_defined?(target, module_name)
|
@@ -23,27 +30,24 @@ module RubyFeatures
|
|
23
30
|
prepare_module(target, module_name)
|
24
31
|
end
|
25
32
|
|
26
|
-
def prepare_module(target,
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
target.const_set(first_submodule, Module.new)
|
33
|
-
|
34
|
-
modules.empty? ?
|
35
|
-
new_target :
|
36
|
-
prepare_module(new_target, modules)
|
33
|
+
def prepare_module(target, module_name)
|
34
|
+
inject_const_parts_with_target(target, module_name) do |parent, submodule|
|
35
|
+
module_defined?(parent, submodule) ?
|
36
|
+
parent.const_get(submodule) :
|
37
|
+
parent.const_set(submodule, Module.new)
|
38
|
+
end
|
37
39
|
end
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
|
41
|
+
private
|
42
|
+
|
43
|
+
def inject_const_parts_with_target(target, const_name, &block)
|
44
|
+
const_parts = const_name.split('::')
|
45
|
+
if const_parts.first == ''
|
46
|
+
target = ::Object
|
47
|
+
const_parts.shift
|
48
|
+
end
|
42
49
|
|
43
|
-
|
44
|
-
string.gsub(/([A-Z][a-z\d]*)/){ "_#{$1.downcase}" }.
|
45
|
-
gsub(/^_/, '').
|
46
|
-
gsub('::', '/')
|
50
|
+
const_parts.inject(target, &block)
|
47
51
|
end
|
48
52
|
|
49
53
|
end
|
@@ -1,32 +1,20 @@
|
|
1
1
|
module RubyFeatures
|
2
2
|
module Utils
|
3
3
|
module ConstAccessor19
|
4
|
-
def ruby_const_defined?(target,
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
!first_const_defined || const_parts.empty? ?
|
11
|
-
first_const_defined :
|
12
|
-
ruby_const_defined?(target.const_get(first_const_part), const_parts)
|
4
|
+
def ruby_const_defined?(target, const_name)
|
5
|
+
!!inject_const_parts_with_target(target, const_name){ |parent, submodule|
|
6
|
+
parent && parent.const_defined?(submodule) ?
|
7
|
+
parent.const_get(submodule) :
|
8
|
+
false
|
9
|
+
}
|
13
10
|
end
|
14
11
|
|
15
|
-
def ruby_const_get(target,
|
16
|
-
|
17
|
-
|
18
|
-
first_const_part = const_parts.shift
|
19
|
-
if first_const_part == ''
|
20
|
-
target = ::Object
|
21
|
-
first_const_part = const_parts.shift
|
12
|
+
def ruby_const_get(target, const_name)
|
13
|
+
inject_const_parts_with_target(target, const_name) do |parent, submodule|
|
14
|
+
parent.const_get(submodule)
|
22
15
|
end
|
23
|
-
|
24
|
-
first_const = target.const_get(first_const_part)
|
25
|
-
|
26
|
-
const_parts.empty? ?
|
27
|
-
first_const :
|
28
|
-
ruby_const_get(first_const, const_parts)
|
29
16
|
end
|
17
|
+
|
30
18
|
end
|
31
19
|
end
|
32
20
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RubyFeatures
|
2
|
+
module Utils
|
3
|
+
module Inflector
|
4
|
+
|
5
|
+
def camelize(string)
|
6
|
+
string.gsub(/([a-z\d]+)/i) { $1.capitalize }.gsub(/[-_]/, '').gsub('/', '::')
|
7
|
+
end
|
8
|
+
|
9
|
+
def underscore(string)
|
10
|
+
string.gsub(/([A-Z][a-z\d]*)/){ "_#{$1.downcase}" }.
|
11
|
+
gsub(/^_/, '').
|
12
|
+
gsub('::', '/')
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
describe RubyFeatures do
|
2
|
+
|
3
|
+
class ConditionsTestClass; end
|
4
|
+
|
5
|
+
RubyFeatures.define 'conditions_test_feature' do
|
6
|
+
condition(:boolean) { true }
|
7
|
+
condition(:string) { 'string' }
|
8
|
+
|
9
|
+
apply_to 'ConditionsTestClass', if: :boolean do
|
10
|
+
class_methods if: {string: 'string'} do
|
11
|
+
def boolean_true; end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
apply_to 'ConditionsTestClass', unless: :boolean do
|
16
|
+
class_methods do
|
17
|
+
def boolean_false; end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
apply_to 'ConditionsTestClass' do
|
22
|
+
class_methods if: :boolean do
|
23
|
+
def class_boolean_true; end
|
24
|
+
end
|
25
|
+
|
26
|
+
class_methods unless: :boolean do
|
27
|
+
def class_boolean_false; end
|
28
|
+
end
|
29
|
+
|
30
|
+
instance_methods if: :boolean do
|
31
|
+
def instance_boolean_true; end
|
32
|
+
end
|
33
|
+
|
34
|
+
instance_methods unless: :boolean do
|
35
|
+
def instance_boolean_false; end
|
36
|
+
end
|
37
|
+
|
38
|
+
applied if: :boolean do
|
39
|
+
attr_accessor :boolean_true_accessor
|
40
|
+
end
|
41
|
+
|
42
|
+
applied unless: :boolean do
|
43
|
+
attr_accessor :boolean_false_accessor
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end.apply
|
49
|
+
|
50
|
+
subject { ConditionsTestClass }
|
51
|
+
|
52
|
+
it 'should respect conditions when appling to target' do
|
53
|
+
expect(subject).to respond_to(:boolean_true)
|
54
|
+
expect(subject).to_not respond_to(:boolean_false)
|
55
|
+
|
56
|
+
expect(subject.singleton_class.included_modules).to include(
|
57
|
+
RubyFeatures::Mixins::ConditionsTestFeature::ConditionsTestClass::ExtendIfBooleanIsTrueAndStringIsString
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should respect conditions for class methods' do
|
62
|
+
expect(subject).to respond_to(:class_boolean_true)
|
63
|
+
expect(subject).to_not respond_to(:class_boolean_false)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should respect conditions for instance methods' do
|
67
|
+
expect(subject.new).to respond_to(:instance_boolean_true)
|
68
|
+
expect(subject.new).to_not respond_to(:instance_boolean_false)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should respect conditions for applied blocks' do
|
72
|
+
expect(subject.new).to respond_to(:boolean_true_accessor)
|
73
|
+
expect(subject.new).to_not respond_to(:boolean_false_accessor)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should raise error if such condition is aready defined' do
|
77
|
+
expect{
|
78
|
+
RubyFeatures.define 'conditions_test_class/duplicate_conditions' do
|
79
|
+
condition(:duplicate_condition){ :duplicate_condition }
|
80
|
+
condition('duplicate_condition'){ 'duplicate_condition' }
|
81
|
+
end
|
82
|
+
}.to raise_error(/Such condition is already defined/)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
data/spec/define_spec.rb
CHANGED
@@ -1,25 +1,34 @@
|
|
1
|
-
describe RubyFeatures::
|
1
|
+
describe RubyFeatures::Concern::Feature do
|
2
2
|
|
3
3
|
prepare_test_class 'DefineTestModule::DefineTestClass'
|
4
4
|
|
5
5
|
it 'should apply class methods' do
|
6
6
|
expect{
|
7
|
-
define_test_feature('
|
7
|
+
define_test_feature('class_methods_feature') do
|
8
8
|
class_methods do
|
9
9
|
def test_class_method; end
|
10
10
|
end
|
11
11
|
end.apply
|
12
12
|
}.to change{test_class.respond_to?(:test_class_method)}.from(false).to(true)
|
13
|
+
|
14
|
+
|
15
|
+
expect(test_class.singleton_class.included_modules).to include(
|
16
|
+
RubyFeatures::Mixins::DefineTestModule::DefineTestClass::ClassMethodsFeature::DefineTestModule::DefineTestClass::Extend
|
17
|
+
)
|
13
18
|
end
|
14
19
|
|
15
20
|
it 'should apply instance methods' do
|
16
21
|
expect{
|
17
|
-
define_test_feature('
|
22
|
+
define_test_feature('instance_methods_feature') do
|
18
23
|
instance_methods do
|
19
24
|
def test_instance_method; end
|
20
25
|
end
|
21
26
|
end.apply
|
22
27
|
}.to change{test_class.new.respond_to?(:test_instance_method)}.from(false).to(true)
|
28
|
+
|
29
|
+
expect(test_class.included_modules).to include(
|
30
|
+
RubyFeatures::Mixins::DefineTestModule::DefineTestClass::InstanceMethodsFeature::DefineTestModule::DefineTestClass::Include
|
31
|
+
)
|
23
32
|
end
|
24
33
|
|
25
34
|
it 'should process applied block' do
|
@@ -49,7 +58,7 @@ describe RubyFeatures::Single do
|
|
49
58
|
def existing_class_method; end
|
50
59
|
end
|
51
60
|
end.apply
|
52
|
-
}.to raise_error(/
|
61
|
+
}.to raise_error(/Tried to extend already existing methods: \[:existing_class_method\]/)
|
53
62
|
end
|
54
63
|
|
55
64
|
it 'should raise error if target already has feature instance method' do
|
@@ -63,7 +72,7 @@ describe RubyFeatures::Single do
|
|
63
72
|
def existing_instance_method; end
|
64
73
|
end
|
65
74
|
end.apply
|
66
|
-
}.to raise_error(/
|
75
|
+
}.to raise_error(/Tried to include already existing methods: \[:existing_instance_method\]/)
|
67
76
|
end
|
68
77
|
|
69
78
|
end
|
data/spec/lazy_apply_spec.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
describe RubyFeatures::Concern::Feature do
|
2
|
+
|
3
|
+
prepare_test_class 'RequreFeaturesTestClass'
|
4
|
+
|
5
|
+
before do
|
6
|
+
define_test_feature('required_feature1') do
|
7
|
+
class_methods do
|
8
|
+
def required_method1; end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
define_test_feature('required_feature2') do
|
13
|
+
class_methods do
|
14
|
+
def required_method2; end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
define_test_feature('required_feature3') do
|
19
|
+
class_methods do
|
20
|
+
def required_method3; end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:main_feature) do
|
26
|
+
RubyFeatures.define 'requre_features_test_class/main_feature' do
|
27
|
+
dependency 'requre_features_test_class/required_feature1'
|
28
|
+
dependencies 'requre_features_test_class/required_feature2', 'requre_features_test_class/required_feature3'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should apply required features' do
|
33
|
+
expect{main_feature.apply}.to change{
|
34
|
+
(1..3).map{|i| test_class.respond_to?("required_method#{i}")}
|
35
|
+
}.from(Array.new(3, false)).to(Array.new(3, true))
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-features
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-05-
|
12
|
+
date: 2015-05-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -35,21 +35,26 @@ extra_rdoc_files: []
|
|
35
35
|
files:
|
36
36
|
- lib/generators/ruby_features/install_generator.rb
|
37
37
|
- lib/generators/ruby_features/templates/ruby-features.rb
|
38
|
-
- lib/ruby-features/concern.rb
|
38
|
+
- lib/ruby-features/concern/apply_to.rb
|
39
|
+
- lib/ruby-features/concern/feature.rb
|
40
|
+
- lib/ruby-features/conditions.rb
|
39
41
|
- lib/ruby-features/container.rb
|
40
42
|
- lib/ruby-features/lazy.rb
|
41
43
|
- lib/ruby-features/mixins.rb
|
42
|
-
- lib/ruby-features/single.rb
|
43
44
|
- lib/ruby-features/utils/const_accessor_19.rb
|
44
45
|
- lib/ruby-features/utils/const_accessor_20.rb
|
46
|
+
- lib/ruby-features/utils/inflector.rb
|
47
|
+
- lib/ruby-features/utils/inflector_active_support.rb
|
45
48
|
- lib/ruby-features/utils.rb
|
46
49
|
- lib/ruby-features/version.rb
|
47
50
|
- lib/ruby-features.rb
|
48
51
|
- LICENSE
|
49
52
|
- README.md
|
53
|
+
- spec/conditions_spec.rb
|
50
54
|
- spec/define_spec.rb
|
51
55
|
- spec/find_and_apply_spec.rb
|
52
56
|
- spec/lazy_apply_spec.rb
|
57
|
+
- spec/require_features_spec.rb
|
53
58
|
- spec/ruby_features/nested/nested_feature.rb
|
54
59
|
- spec/ruby_features/root_feature.rb
|
55
60
|
- spec/spec_helper.rb
|
@@ -80,9 +85,11 @@ signing_key:
|
|
80
85
|
specification_version: 3
|
81
86
|
summary: Makes extending of Ruby classes and modules to be easy, safe and controlled.
|
82
87
|
test_files:
|
88
|
+
- spec/conditions_spec.rb
|
83
89
|
- spec/define_spec.rb
|
84
90
|
- spec/find_and_apply_spec.rb
|
85
91
|
- spec/lazy_apply_spec.rb
|
92
|
+
- spec/require_features_spec.rb
|
86
93
|
- spec/ruby_features/nested/nested_feature.rb
|
87
94
|
- spec/ruby_features/root_feature.rb
|
88
95
|
- spec/spec_helper.rb
|
@@ -1,50 +0,0 @@
|
|
1
|
-
module RubyFeatures
|
2
|
-
module Concern
|
3
|
-
|
4
|
-
def _apply(target, feature_name)
|
5
|
-
target_class = RubyFeatures::Utils.ruby_const_get(self, "::#{target}")
|
6
|
-
|
7
|
-
_apply_methods(target_class, feature_name, :class)
|
8
|
-
_apply_methods(target_class, feature_name, :instance)
|
9
|
-
_apply_applied_blocks(target_class)
|
10
|
-
end
|
11
|
-
|
12
|
-
private
|
13
|
-
|
14
|
-
def self.extended(base)
|
15
|
-
base.instance_variable_set(:@_applied_blocks, [])
|
16
|
-
end
|
17
|
-
|
18
|
-
def applied(&block)
|
19
|
-
instance_variable_get(:@_applied_blocks) << block
|
20
|
-
end
|
21
|
-
|
22
|
-
def class_methods(&block)
|
23
|
-
RubyFeatures::Utils.prepare_module(self, 'ClassMethods').class_eval(&block)
|
24
|
-
end
|
25
|
-
|
26
|
-
def instance_methods(&block)
|
27
|
-
RubyFeatures::Utils.prepare_module(self, 'InstanceMethods').class_eval(&block)
|
28
|
-
end
|
29
|
-
|
30
|
-
def _apply_methods(target_class, feature_name, methods_type)
|
31
|
-
methods_module_name = "#{methods_type.capitalize}Methods"
|
32
|
-
|
33
|
-
if RubyFeatures::Utils.module_defined?(self, methods_module_name)
|
34
|
-
methods_module = const_get(methods_module_name)
|
35
|
-
common_methods = target_class.public_send(:"#{'instance_' if methods_type == :instance}methods") & methods_module.instance_methods
|
36
|
-
raise NameError.new("Feature #{feature_name} tried to define already existing #{methods_type} methods: #{common_methods.inspect}") unless common_methods.empty?
|
37
|
-
|
38
|
-
target_class.send((methods_type == :instance ? :include : :extend), methods_module)
|
39
|
-
end
|
40
|
-
|
41
|
-
end
|
42
|
-
|
43
|
-
def _apply_applied_blocks(target_class)
|
44
|
-
instance_variable_get(:@_applied_blocks).each do |applied_block|
|
45
|
-
target_class.class_eval(&applied_block)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
end
|
50
|
-
end
|
data/lib/ruby-features/single.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
module RubyFeatures
|
2
|
-
class Single
|
3
|
-
|
4
|
-
attr_reader :name, :applied, :apply_to_blocks
|
5
|
-
alias applied? applied
|
6
|
-
|
7
|
-
def initialize(name, feature_body)
|
8
|
-
@name = name = name.to_s
|
9
|
-
raise NameError.new("Wrong feature name: #{name}") unless name.match(/^[\/_a-z\d]+$/)
|
10
|
-
|
11
|
-
@apply_to_blocks = {}
|
12
|
-
@applied = false
|
13
|
-
|
14
|
-
instance_eval(&feature_body) if feature_body
|
15
|
-
end
|
16
|
-
|
17
|
-
def apply
|
18
|
-
unless applied?
|
19
|
-
Mixins.build_and_apply!(self)
|
20
|
-
|
21
|
-
@apply_to_blocks = nil
|
22
|
-
@applied = true
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def apply_to(target, &block)
|
29
|
-
target = target.to_s
|
30
|
-
|
31
|
-
@apply_to_blocks[target] ||= []
|
32
|
-
@apply_to_blocks[target] << block
|
33
|
-
end
|
34
|
-
|
35
|
-
end
|
36
|
-
end
|