ruby-features 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Version](https://badge.fury.io/rb/ruby-features.svg)](http://badge.fury.io/rb/ruby-features)
|
4
|
+
[![Build](https://travis-ci.org/stokarenko/ruby-features.svg?branch=master)](https://travis-ci.org/stokarenko/ruby-features)
|
5
|
+
[![Climate](https://codeclimate.com/github/stokarenko/ruby-features/badges/gpa.svg)](https://codeclimate.com/github/stokarenko/ruby-features)
|
6
|
+
[![Coverage](https://codeclimate.com/github/stokarenko/ruby-features/badges/coverage.svg)](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
|