clowne 0.0.1 → 0.1.0.beta1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +49 -0
  5. data/.rufo +3 -0
  6. data/.travis.yml +37 -3
  7. data/CHANGELOG.md +11 -0
  8. data/Gemfile +12 -3
  9. data/LICENSE.txt +1 -1
  10. data/README.md +532 -14
  11. data/Rakefile +6 -8
  12. data/clowne.gemspec +16 -15
  13. data/gemfiles/activerecord42.gemfile +6 -0
  14. data/gemfiles/jruby.gemfile +6 -0
  15. data/gemfiles/railsmaster.gemfile +7 -0
  16. data/lib/clowne.rb +36 -2
  17. data/lib/clowne/adapters/active_record.rb +27 -0
  18. data/lib/clowne/adapters/active_record/association.rb +34 -0
  19. data/lib/clowne/adapters/active_record/associations.rb +30 -0
  20. data/lib/clowne/adapters/active_record/associations/base.rb +63 -0
  21. data/lib/clowne/adapters/active_record/associations/has_and_belongs_to_many.rb +20 -0
  22. data/lib/clowne/adapters/active_record/associations/has_many.rb +21 -0
  23. data/lib/clowne/adapters/active_record/associations/has_one.rb +30 -0
  24. data/lib/clowne/adapters/active_record/associations/noop.rb +19 -0
  25. data/lib/clowne/adapters/active_record/dsl.rb +33 -0
  26. data/lib/clowne/adapters/base.rb +69 -0
  27. data/lib/clowne/adapters/base/finalize.rb +19 -0
  28. data/lib/clowne/adapters/base/nullify.rb +19 -0
  29. data/lib/clowne/adapters/registry.rb +61 -0
  30. data/lib/clowne/cloner.rb +93 -0
  31. data/lib/clowne/declarations.rb +30 -0
  32. data/lib/clowne/declarations/exclude_association.rb +24 -0
  33. data/lib/clowne/declarations/finalize.rb +20 -0
  34. data/lib/clowne/declarations/include_association.rb +45 -0
  35. data/lib/clowne/declarations/nullify.rb +20 -0
  36. data/lib/clowne/declarations/trait.rb +44 -0
  37. data/lib/clowne/dsl.rb +14 -0
  38. data/lib/clowne/ext/string_constantize.rb +23 -0
  39. data/lib/clowne/plan.rb +81 -0
  40. data/lib/clowne/planner.rb +40 -0
  41. data/lib/clowne/version.rb +3 -1
  42. metadata +73 -12
data/Rakefile CHANGED
@@ -1,10 +1,8 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
3
4
 
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
8
- end
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ RuboCop::RakeTask.new
9
7
 
10
- task :default => :test
8
+ task default: [:rubocop, :spec]
data/clowne.gemspec CHANGED
@@ -1,27 +1,28 @@
1
- # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "clowne/version"
3
+ require 'clowne/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "clowne"
6
+ spec.name = 'clowne'
8
7
  spec.version = Clowne::VERSION
9
- spec.authors = ["Vladimir Dementyev"]
10
- spec.email = ["dementiev.vm@gmail.com"]
8
+ spec.authors = ['Vladimir Dementyev', 'Sverchkov Nikolay']
9
+ spec.email = ['palkan@evilmartians.com', 'ssnikolay@gmail.com']
11
10
 
12
- spec.summary = "Declarative models cloning"
13
- spec.description = "Declarative models cloning"
14
- spec.homepage = "https://github.com/palkan/clowne"
15
- spec.license = "MIT"
11
+ spec.summary = 'A flexible gem for cloning your models.'
12
+ spec.description = 'A flexible gem for cloning your models.'
13
+ spec.homepage = 'https://github.com/palkan/clowne'
14
+ spec.license = 'MIT'
16
15
 
17
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
17
  f.match(%r{^(test|spec|features)/})
19
18
  end
20
- spec.bindir = "exe"
19
+ spec.bindir = 'exe'
21
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
- spec.require_paths = ["lib"]
21
+ spec.require_paths = ['lib']
23
22
 
24
- spec.add_development_dependency "bundler", "~> 1.15"
25
- spec.add_development_dependency "rake", "~> 10.0"
26
- spec.add_development_dependency "minitest", "~> 5.0"
23
+ spec.add_development_dependency 'bundler', '~> 1.14'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+ spec.add_development_dependency 'factory_bot', '~> 4.8'
27
+ spec.add_development_dependency 'rubocop', '~> 0.51'
27
28
  end
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 4.2'
4
+ gem 'sqlite3'
5
+
6
+ gemspec path: '..'
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord-jdbcsqlite3-adapter', '~> 50.0'
4
+ gem 'activerecord', '~> 5.0.0'
5
+
6
+ gemspec path: '..'
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'arel', github: 'rails/arel'
4
+ gem 'rails', github: 'rails/rails'
5
+ gem 'sqlite3'
6
+
7
+ gemspec path: '..'
data/lib/clowne.rb CHANGED
@@ -1,5 +1,39 @@
1
- require "clowne/version"
1
+ # frozen_string_literal: true
2
2
 
3
+ require 'clowne/version'
4
+ require 'clowne/declarations'
5
+ require 'clowne/cloner'
6
+
7
+ require 'clowne/adapters/base'
8
+
9
+ # Declarative models cloning
3
10
  module Clowne
4
- # Your code goes here...
11
+ # List of built-in adapters
12
+ ADAPTERS = {
13
+ base: 'Base',
14
+ active_record: 'ActiveRecord'
15
+ }.freeze
16
+
17
+ class << self
18
+ attr_reader :default_adapter, :raise_on_override
19
+
20
+ # Set default adapters for all cloners
21
+ def default_adapter=(adapter)
22
+ @default_adapter = resolve_adapter(adapter)
23
+ end
24
+
25
+ def resolve_adapter(adapter)
26
+ if adapter.is_a?(Class)
27
+ adapter.new
28
+ elsif adapter.is_a?(Symbol)
29
+ adapter_class = ADAPTERS[adapter]
30
+ raise "Unknown adapter: #{adapter}" if adapter_class.nil?
31
+ Clowne::Adapters.const_get(adapter_class).new
32
+ else
33
+ adapter
34
+ end
35
+ end
36
+ end
5
37
  end
38
+
39
+ require 'clowne/adapters/active_record' if defined?(::ActiveRecord)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters
5
+ # Cloning adapter for ActiveRecord
6
+ class ActiveRecord < Base
7
+ # Adds #cloner_class method to ActiveRecord::Base
8
+ module ActiveRecordExt
9
+ def cloner_class
10
+ return @_clowne_cloner if instance_variable_defined?(:@_clowne_cloner)
11
+
12
+ cloner = "#{name}Cloner".safe_constantize
13
+ return @_clowne_cloner = cloner if cloner && cloner <= Clowne::Cloner
14
+
15
+ @_clowne_cloner = superclass.cloner_class if superclass.respond_to?(:cloner_class)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ ActiveSupport.on_load(:active_record) do
23
+ ::ActiveRecord::Base.extend Clowne::Adapters::ActiveRecord::ActiveRecordExt
24
+ end
25
+
26
+ require 'clowne/adapters/active_record/associations'
27
+ require 'clowne/adapters/active_record/association'
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters # :nodoc: all
5
+ class ActiveRecord
6
+ class UnknownAssociation < StandardError; end
7
+
8
+ class Association
9
+ class << self
10
+ def call(source, record, declaration, params:)
11
+ reflection = source.class.reflections[declaration.name.to_s]
12
+
13
+ if reflection.nil?
14
+ raise UnknownAssociation,
15
+ "Association #{declaration.name} couldn't be found for #{source.class}"
16
+ end
17
+
18
+ cloner_class = Associations.cloner_for(reflection)
19
+
20
+ cloner_class.new(reflection, source, declaration, params).call(record)
21
+
22
+ record
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ Clowne::Adapters::ActiveRecord.register_resolver(
31
+ :association,
32
+ Clowne::Adapters::ActiveRecord::Association,
33
+ before: :nullify
34
+ )
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clowne/adapters/active_record/associations/base'
4
+ require 'clowne/adapters/active_record/associations/noop'
5
+ require 'clowne/adapters/active_record/associations/has_one'
6
+ require 'clowne/adapters/active_record/associations/has_many'
7
+ require 'clowne/adapters/active_record/associations/has_and_belongs_to_many'
8
+
9
+ module Clowne
10
+ module Adapters # :nodoc: all
11
+ class ActiveRecord
12
+ module Associations
13
+ AR_2_CLONER = {
14
+ has_one: HasOne,
15
+ has_many: HasMany,
16
+ has_and_belongs_to_many: HABTM
17
+ }.freeze
18
+
19
+ # Returns an association cloner class for reflection
20
+ def self.cloner_for(reflection)
21
+ if reflection.is_a?(::ActiveRecord::Reflection::ThroughReflection)
22
+ Noop
23
+ else
24
+ AR_2_CLONER.fetch(reflection.macro, Noop)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters # :nodoc: all
5
+ class ActiveRecord
6
+ module Associations
7
+ class Base
8
+ # Params:
9
+ # +reflection+:: Association eflection object
10
+ # +source+:: Instance of cloned object (ex: User.new(posts: posts))
11
+ # +declaration+:: = Relation description
12
+ # (ex: Clowne::Declarations::IncludeAssociation.new(:posts))
13
+ # +params+:: = Instance of Clowne::Params
14
+ def initialize(reflection, source, declaration, params)
15
+ @source = source
16
+ @scope = declaration.scope
17
+ @clone_with = declaration.clone_with
18
+ @params = params
19
+ @association_name = declaration.name.to_s
20
+ @reflection = reflection
21
+ @cloner_options = params
22
+ @cloner_options.merge!(traits: declaration.traits) if declaration.traits
23
+ end
24
+
25
+ def call(_record)
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def association
30
+ @_association ||= source.__send__(association_name)
31
+ end
32
+
33
+ def clone_one(child)
34
+ cloner = cloner_for(child)
35
+ cloner ? cloner.call(child, cloner_options) : child.dup
36
+ end
37
+
38
+ def with_scope
39
+ base_scope = association
40
+ if scope.is_a?(Symbol)
41
+ base_scope.__send__(scope)
42
+ elsif scope.is_a?(Proc)
43
+ base_scope.instance_exec(params, &scope) || base_scope
44
+ else
45
+ base_scope
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def cloner_for(child)
52
+ return clone_with if clone_with
53
+
54
+ return child.class.cloner_class if child.class.respond_to?(:cloner_class)
55
+ end
56
+
57
+ attr_reader :source, :scope, :clone_with, :params, :association_name,
58
+ :reflection, :cloner_options
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters # :nodoc: all
5
+ class ActiveRecord
6
+ module Associations
7
+ class HABTM < Base
8
+ def call(record)
9
+ with_scope.each do |child|
10
+ child_clone = clone_one(child)
11
+ record.__send__(association_name) << child_clone
12
+ end
13
+
14
+ record
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters # :nodoc: all
5
+ class ActiveRecord
6
+ module Associations
7
+ class HasMany < Base
8
+ def call(record)
9
+ with_scope.each do |child|
10
+ child_clone = clone_one(child)
11
+ child_clone[:"#{reflection.foreign_key}"] = nil
12
+ record.__send__(association_name) << child_clone
13
+ end
14
+
15
+ record
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters # :nodoc: all
5
+ class ActiveRecord
6
+ module Associations
7
+ class HasOne < Base
8
+ # rubocop: disable Metrics/MethodLength
9
+ def call(record)
10
+ child = association
11
+ return record unless child
12
+ unless scope.nil?
13
+ warn(
14
+ '[Clowne] Has one association does not support scopes ' \
15
+ "(#{@association_name} for #{@source.class})"
16
+ )
17
+ end
18
+
19
+ child_clone = clone_one(child)
20
+ child_clone[:"#{reflection.foreign_key}"] = nil
21
+ record.__send__(:"#{association_name}=", child_clone)
22
+
23
+ record
24
+ end
25
+ # rubocop: enable Metrics/MethodLength
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters # :nodoc: all
5
+ class ActiveRecord
6
+ module Associations
7
+ class Noop < Base
8
+ def call(record)
9
+ warn(
10
+ "[Clowne] Reflection #{reflection.class.name} is not supported "\
11
+ "(#{@association_name} for #{@source.class})"
12
+ )
13
+ record
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters
5
+ # Extend ActiveRecord with Clowne DSL and methods
6
+ module ActiveRecordDSL
7
+ module InstanceMethods # :nodoc:
8
+ # Shortcut to call class's cloner call with self
9
+ def clowne(*args)
10
+ self.class.cloner_class.call(self, *args)
11
+ end
12
+ end
13
+
14
+ module ClassMethods # :nodoc:
15
+ def clowne_config(options = {}, &block)
16
+ if options.delete(:inherit) != false && superclass.respond_to?(:cloner_class)
17
+ parent_cloner = superclass.cloner_class
18
+ end
19
+
20
+ parent_cloner ||= Clowne::Cloner
21
+ cloner = instance_variable_set(:@_clowne_cloner, Class.new(parent_cloner))
22
+ cloner.adapter :active_record
23
+ cloner.instance_exec(&block)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ ActiveSupport.on_load(:active_record) do
31
+ ::ActiveRecord::Base.extend Clowne::Adapters::ActiveRecordDSL::ClassMethods
32
+ ::ActiveRecord::Base.include Clowne::Adapters::ActiveRecordDSL::InstanceMethods
33
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clowne/adapters/registry'
4
+
5
+ module Clowne
6
+ module Adapters
7
+ # ORM-independant adapter (just calls #dup).
8
+ # Works with nullify/finalize.
9
+ class Base
10
+ class << self
11
+ attr_reader :registry
12
+
13
+ def inherited(subclass)
14
+ # Duplicate registry
15
+ subclass.registry = registry.dup
16
+ end
17
+
18
+ def resolver_for(type)
19
+ registry.mapping[type] || raise("Uknown resolver #{type} for #{self}")
20
+ end
21
+
22
+ def register_resolver(type, resolver, after: nil, before: nil)
23
+ registry.mapping[type] = resolver
24
+
25
+ if after
26
+ registry.insert_after after, type
27
+ elsif before
28
+ registry.insert_before before, type
29
+ else
30
+ registry.append type
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ attr_writer :registry
37
+ end
38
+
39
+ self.registry = Registry.new
40
+
41
+ def registry
42
+ self.class.registry
43
+ end
44
+
45
+ # Using a plan make full duplicate of record
46
+ # +source+:: Instance of cloned object (ex: User.new(posts: posts))
47
+ # +plan+:: Array of Declarations
48
+ # +params+:: Custom params hash
49
+ def clone(source, plan, params: {})
50
+ declarations = plan.declarations
51
+ declarations.inject(clone_record(source)) do |record, (type, declaration)|
52
+ resolver_for(type).call(source, record, declaration, params: params)
53
+ end
54
+ end
55
+
56
+ def resolver_for(type)
57
+ self.class.resolver_for(type)
58
+ end
59
+
60
+ # Return #dup if any
61
+ def clone_record(source)
62
+ source.dup
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ require 'clowne/adapters/base/nullify'
69
+ require 'clowne/adapters/base/finalize'