ruby-features 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Sergey Tokarenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ Ruby Features
2
+ =============
3
+
4
+ Ruby Features makes extending of Ruby classes and modules to be easy, safe and controlled.
@@ -0,0 +1,13 @@
1
+ module RubyFeatures
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ desc 'Copy RubyFeatures initializer'
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ def copy_initializer
8
+ template 'ruby-features.rb', 'config/initializers/ruby-features.rb'
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ RubyFeatures.find_in_path(Rails.root.join('app/models/concerns')).apply_all
@@ -0,0 +1,45 @@
1
+ require 'ruby-features/version'
2
+
3
+ module RubyFeatures
4
+ autoload :Container, 'ruby-features/container'
5
+ autoload :Single, 'ruby-features/single'
6
+ autoload :Mixins, 'ruby-features/mixins'
7
+ autoload :Concern, 'ruby-features/concern'
8
+ autoload :Utils, 'ruby-features/utils'
9
+ autoload :Lazy, 'ruby-features/lazy'
10
+
11
+ module Generators
12
+ autoload :InstallGenerator, 'generators/ruby-features/install_generator'
13
+ end
14
+
15
+ @@features = {}
16
+
17
+ class << self
18
+ def find_in_path(*folders)
19
+ old_feature_names = @@features.keys
20
+
21
+ Dir[*folders.map{|folder| File.join(folder, '**', '*_feature.rb') }].each do |file|
22
+ require file
23
+ end
24
+
25
+ Container.new(@@features.keys - old_feature_names)
26
+ end
27
+
28
+ 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)
32
+
33
+ @@features[feature_name] = feature
34
+ end
35
+
36
+ def apply(*feature_names)
37
+ feature_names.each do |feature_name|
38
+ raise NameError.new("Such feature is not registered: #{feature_name}") unless @@features.has_key?(feature_name)
39
+
40
+ @@features[feature_name].apply
41
+ end
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,50 @@
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
@@ -0,0 +1,13 @@
1
+ module RubyFeatures
2
+ class Container
3
+
4
+ def initialize(feature_names)
5
+ @feature_names = feature_names
6
+ end
7
+
8
+ def apply_all
9
+ RubyFeatures.apply(*@feature_names)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ module RubyFeatures
2
+ module Lazy
3
+
4
+ ACTIVE_SUPPORT_LAZY_TARGETS = %w(
5
+ action_view action_controller action_mailer
6
+ active_record active_job
7
+ i18n
8
+ ).map(&:to_sym).freeze
9
+
10
+ @active_support_available = begin
11
+ require 'active_support'
12
+ true
13
+ rescue LoadError
14
+ false
15
+ end
16
+
17
+ class << self
18
+ def active_support_available?
19
+ @active_support_available
20
+ end
21
+
22
+ def apply(target, &block)
23
+ if active_support_available?
24
+ target_namespace = RubyFeatures::Utils.underscore(target.split('::').first).to_sym
25
+
26
+ if ACTIVE_SUPPORT_LAZY_TARGETS.include?(target_namespace)
27
+ ActiveSupport.on_load target_namespace, yield: true, &block
28
+
29
+ return
30
+ end
31
+ end
32
+
33
+ yield
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ module RubyFeatures
2
+ module Mixins
3
+ class << self
4
+
5
+ def build_and_apply!(feature)
6
+ feature_module = RubyFeatures::Utils.prepare_module!(
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
15
+ end
16
+ end
17
+
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
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,51 @@
1
+ module RubyFeatures
2
+ module Utils
3
+
4
+ autoload :ConstAccessor19, 'ruby-features/utils/const_accessor_19'
5
+ autoload :ConstAccessor20, 'ruby-features/utils/const_accessor_20'
6
+
7
+ begin
8
+ const_defined?('Some::Const')
9
+ extend ConstAccessor20
10
+ rescue NameError
11
+ extend ConstAccessor19
12
+ end
13
+
14
+ class << self
15
+
16
+ def module_defined?(target, module_name)
17
+ ruby_const_defined?(target, module_name) && ruby_const_get(target, module_name).name.start_with?(target.name)
18
+ end
19
+
20
+ def prepare_module!(target, module_name)
21
+ module_defined?(target, module_name) ?
22
+ raise(NameError.new("Module already initiated: #{target.name}::#{module_name}")) :
23
+ prepare_module(target, module_name)
24
+ end
25
+
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)
37
+ end
38
+
39
+ def modulize(string)
40
+ string.gsub(/([a-z\d]+)/i) { $1.capitalize }.gsub(/[-_]/, '').gsub('/', '::')
41
+ end
42
+
43
+ def underscore(string)
44
+ string.gsub(/([A-Z][a-z\d]*)/){ "_#{$1.downcase}" }.
45
+ gsub(/^_/, '').
46
+ gsub('::', '/')
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,32 @@
1
+ module RubyFeatures
2
+ module Utils
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)
13
+ end
14
+
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
22
+ 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
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ module RubyFeatures
2
+ module Utils
3
+ module ConstAccessor20
4
+ def ruby_const_defined?(target, const_name)
5
+ target.const_defined?(const_name)
6
+ end
7
+
8
+ def ruby_const_get(target, const_name)
9
+ target.const_get(const_name)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module RubyFeatures
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,69 @@
1
+ describe RubyFeatures::Single do
2
+
3
+ prepare_test_class 'DefineTestModule::DefineTestClass'
4
+
5
+ it 'should apply class methods' do
6
+ expect{
7
+ define_test_feature('class_methods') do
8
+ class_methods do
9
+ def test_class_method; end
10
+ end
11
+ end.apply
12
+ }.to change{test_class.respond_to?(:test_class_method)}.from(false).to(true)
13
+ end
14
+
15
+ it 'should apply instance methods' do
16
+ expect{
17
+ define_test_feature('instance_methods') do
18
+ instance_methods do
19
+ def test_instance_method; end
20
+ end
21
+ end.apply
22
+ }.to change{test_class.new.respond_to?(:test_instance_method)}.from(false).to(true)
23
+ end
24
+
25
+ it 'should process applied block' do
26
+ expect{
27
+ define_test_feature('applied_block') do
28
+ applied do
29
+ attr_accessor :applied_block_accessor
30
+ end
31
+ end.apply
32
+ }.to change{test_class.new.respond_to?(:applied_block_accessor)}.from(false).to(true)
33
+ end
34
+
35
+ it 'should check that feature name is correct' do
36
+ expect{
37
+ define_test_feature('wrong feature name')
38
+ }.to raise_error(/Wrong feature name/)
39
+ end
40
+
41
+ it 'should raise error if target already has feature class method' do
42
+ test_class.class_eval do
43
+ def self.existing_class_method; end
44
+ end
45
+
46
+ expect{
47
+ define_test_feature('existing_class_method') do
48
+ class_methods do
49
+ def existing_class_method; end
50
+ end
51
+ end.apply
52
+ }.to raise_error(/tried to define already existing class methods: \[:existing_class_method\]/)
53
+ end
54
+
55
+ it 'should raise error if target already has feature instance method' do
56
+ test_class.class_eval do
57
+ def existing_instance_method; end
58
+ end
59
+
60
+ expect{
61
+ define_test_feature('existing_instance_method') do
62
+ instance_methods do
63
+ def existing_instance_method; end
64
+ end
65
+ end.apply
66
+ }.to raise_error(/tried to define already existing instance methods: \[:existing_instance_method\]/)
67
+ end
68
+
69
+ end
@@ -0,0 +1,44 @@
1
+ describe RubyFeatures do
2
+
3
+ prepare_test_class('FindAndApplyTestClass')
4
+
5
+ it 'should find features in path and apply all on demand' do
6
+ features_container = subject.find_in_path(File.expand_path('../ruby_features', __FILE__))
7
+
8
+ expect(test_class).to_not respond_to(:root_method)
9
+ expect(test_class).to_not respond_to(:nested_method)
10
+
11
+ features_container.apply_all
12
+
13
+ expect(test_class).to respond_to(:root_method)
14
+ expect(test_class).to respond_to(:nested_method)
15
+ end
16
+
17
+ it 'should apply features by name' do
18
+
19
+ define_test_feature('manual') do
20
+ class_methods do
21
+ def manual_method; end
22
+ end
23
+ end
24
+
25
+ expect(test_class).to_not respond_to(:manual_method)
26
+ RubyFeatures.apply('find_and_apply_test_class/manual')
27
+ expect(test_class).to respond_to(:manual_method)
28
+ end
29
+
30
+ it 'should raise error if trying to apply not existing feature' do
31
+ expect{
32
+ RubyFeatures.apply('find_and_apply_test_class/not_existing_feature')
33
+ }.to raise_error(/Such feature is not registered/)
34
+ end
35
+
36
+ it 'should raise error if trying to define already registered feature' do
37
+ RubyFeatures.define('find_and_apply_test_class/duplicate_feature')
38
+
39
+ expect{
40
+ RubyFeatures.define('find_and_apply_test_class/duplicate_feature')
41
+ }.to raise_error(/Such feature is already registered/)
42
+ end
43
+
44
+ end
@@ -0,0 +1,19 @@
1
+ if RubyFeatures::Lazy.active_support_available?
2
+ describe RubyFeatures::Lazy do
3
+
4
+ it 'should use ActiveSupport lazy load' do
5
+ RubyFeatures.define 'lazy_load/active_support' do
6
+ apply_to 'ActiveRecord::Base' do
7
+ class_methods do
8
+ def lazy_active_support; end
9
+ end
10
+ end
11
+ end.apply
12
+ expect(defined?(ActiveRecord)).to_not be true
13
+
14
+ require 'active_record'
15
+ expect(ActiveRecord::Base).to respond_to(:lazy_active_support)
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ RubyFeatures.define 'find_and_apply_test_class/nested' do
2
+ apply_to 'FindAndApplyTestClass' do
3
+
4
+ class_methods do
5
+ def nested_method; end
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ RubyFeatures.define 'find_and_apply_test_class/root' do
2
+ apply_to 'FindAndApplyTestClass' do
3
+
4
+ class_methods do
5
+ def root_method; end
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ require 'codeclimate-test-reporter'
2
+ CodeClimate::TestReporter.start
3
+
4
+ require 'bundler/setup'
5
+ Bundler.require(:default)
6
+
7
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
@@ -0,0 +1,25 @@
1
+ RSpec.configure do
2
+
3
+ def prepare_test_class(class_parts)
4
+ class_parts = class_parts.split('::') unless class_parts.kind_of?(Array)
5
+
6
+ test_class = Object
7
+
8
+ until class_parts.empty?
9
+ test_class = class_parts.size > 1 ?
10
+ test_class.const_set(class_parts.shift, Module.new) :
11
+ test_class.const_set(class_parts.shift, Class.new)
12
+ end
13
+
14
+ let(:test_class){ test_class }
15
+ end
16
+
17
+ def define_test_feature(feature_name_postfix, &block)
18
+ test_class_name = test_class.name
19
+ feature_name = "#{RubyFeatures::Utils.underscore(test_class_name)}/#{feature_name_postfix}"
20
+
21
+ RubyFeatures.define feature_name do
22
+ apply_to test_class_name, &block
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-features
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sergey Tokarenko
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-05-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Makes extending of Ruby classes and modules to be easy, safe and controlled.
31
+ email: private.tokarenko.sergey@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/generators/ruby_features/install_generator.rb
37
+ - lib/generators/ruby_features/templates/ruby-features.rb
38
+ - lib/ruby-features/concern.rb
39
+ - lib/ruby-features/container.rb
40
+ - lib/ruby-features/lazy.rb
41
+ - lib/ruby-features/mixins.rb
42
+ - lib/ruby-features/single.rb
43
+ - lib/ruby-features/utils/const_accessor_19.rb
44
+ - lib/ruby-features/utils/const_accessor_20.rb
45
+ - lib/ruby-features/utils.rb
46
+ - lib/ruby-features/version.rb
47
+ - lib/ruby-features.rb
48
+ - LICENSE
49
+ - README.md
50
+ - spec/define_spec.rb
51
+ - spec/find_and_apply_spec.rb
52
+ - spec/lazy_apply_spec.rb
53
+ - spec/ruby_features/nested/nested_feature.rb
54
+ - spec/ruby_features/root_feature.rb
55
+ - spec/spec_helper.rb
56
+ - spec/support/helpers.rb
57
+ homepage: https://github.com/stokarenko/ruby-features
58
+ licenses:
59
+ - MIT
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: 1.9.3
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 1.8.23.2
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Makes extending of Ruby classes and modules to be easy, safe and controlled.
82
+ test_files:
83
+ - spec/define_spec.rb
84
+ - spec/find_and_apply_spec.rb
85
+ - spec/lazy_apply_spec.rb
86
+ - spec/ruby_features/nested/nested_feature.rb
87
+ - spec/ruby_features/root_feature.rb
88
+ - spec/spec_helper.rb
89
+ - spec/support/helpers.rb