ruby-features 1.0.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/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