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 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('app/models/concerns')).apply_all
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
- @@features = {}
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 = @@features.keys
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(@@features.keys - old_feature_names)
38
+ Container.new(@features.keys - old_feature_names)
26
39
  end
27
40
 
28
41
  def define(feature_name, &feature_body)
29
- feature = Single.new(feature_name, feature_body)
30
- feature_name = feature.name
31
- raise NameError.new("Such feature is already registered: #{feature_name}") if @@features.has_key?(feature_name)
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
- @@features[feature_name] = feature
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 @@features.has_key?(feature_name)
51
+ raise NameError.new("Such feature is not registered: #{feature_name}") unless @features.has_key?(feature_name)
39
52
 
40
- @@features[feature_name].apply
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
@@ -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
 
@@ -2,26 +2,17 @@ module RubyFeatures
2
2
  module Mixins
3
3
  class << self
4
4
 
5
- def build_and_apply!(feature)
6
- feature_module = RubyFeatures::Utils.prepare_module!(
5
+ def new(feature_name, feature_body)
6
+ RubyFeatures::Utils.prepare_module!(
7
7
  self,
8
- RubyFeatures::Utils.modulize(feature.name)
9
- )
10
-
11
- feature.apply_to_blocks.each do |target, blocks|
12
- RubyFeatures::Lazy.apply(target) do
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
@@ -1,8 +1,11 @@
1
1
  module RubyFeatures
2
2
  module Utils
3
3
 
4
- autoload :ConstAccessor19, 'ruby-features/utils/const_accessor_19'
5
- autoload :ConstAccessor20, 'ruby-features/utils/const_accessor_20'
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, modules)
27
- modules = modules.split('::') unless modules.kind_of?(Array)
28
-
29
- first_submodule = modules.shift
30
- new_target = module_defined?(target, first_submodule) ?
31
- target.const_get(first_submodule) :
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
- def modulize(string)
40
- string.gsub(/([a-z\d]+)/i) { $1.capitalize }.gsub(/[-_]/, '').gsub('/', '::')
41
- end
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
- def underscore(string)
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, const_parts)
5
- const_parts = const_parts.split('::') unless const_parts.kind_of?(Array)
6
-
7
- first_const_part = const_parts.shift
8
- first_const_defined = target.const_defined?(first_const_part)
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, const_parts)
16
- const_parts = const_parts.split('::') unless const_parts.kind_of?(Array)
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,15 @@
1
+ module RubyFeatures
2
+ module Utils
3
+ module InflectorActiveSupport
4
+
5
+ def camelize(string)
6
+ ActiveSupport::Inflector.camelize(string)
7
+ end
8
+
9
+ def underscore(string)
10
+ ActiveSupport::Inflector.underscore(string)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module RubyFeatures
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  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::Single do
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('class_methods') do
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('instance_methods') do
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(/tried to define already existing class methods: \[:existing_class_method\]/)
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(/tried to define already existing instance methods: \[:existing_instance_method\]/)
75
+ }.to raise_error(/Tried to include already existing methods: \[:existing_instance_method\]/)
67
76
  end
68
77
 
69
78
  end
@@ -1,4 +1,4 @@
1
- if RubyFeatures::Lazy.active_support_available?
1
+ if RubyFeatures.active_support_available?
2
2
  describe RubyFeatures::Lazy do
3
3
 
4
4
  it 'should use ActiveSupport lazy load' do
@@ -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.0.0
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-17 00:00:00.000000000 Z
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
@@ -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