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.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +49 -0
- data/.rufo +3 -0
- data/.travis.yml +37 -3
- data/CHANGELOG.md +11 -0
- data/Gemfile +12 -3
- data/LICENSE.txt +1 -1
- data/README.md +532 -14
- data/Rakefile +6 -8
- data/clowne.gemspec +16 -15
- data/gemfiles/activerecord42.gemfile +6 -0
- data/gemfiles/jruby.gemfile +6 -0
- data/gemfiles/railsmaster.gemfile +7 -0
- data/lib/clowne.rb +36 -2
- data/lib/clowne/adapters/active_record.rb +27 -0
- data/lib/clowne/adapters/active_record/association.rb +34 -0
- data/lib/clowne/adapters/active_record/associations.rb +30 -0
- data/lib/clowne/adapters/active_record/associations/base.rb +63 -0
- data/lib/clowne/adapters/active_record/associations/has_and_belongs_to_many.rb +20 -0
- data/lib/clowne/adapters/active_record/associations/has_many.rb +21 -0
- data/lib/clowne/adapters/active_record/associations/has_one.rb +30 -0
- data/lib/clowne/adapters/active_record/associations/noop.rb +19 -0
- data/lib/clowne/adapters/active_record/dsl.rb +33 -0
- data/lib/clowne/adapters/base.rb +69 -0
- data/lib/clowne/adapters/base/finalize.rb +19 -0
- data/lib/clowne/adapters/base/nullify.rb +19 -0
- data/lib/clowne/adapters/registry.rb +61 -0
- data/lib/clowne/cloner.rb +93 -0
- data/lib/clowne/declarations.rb +30 -0
- data/lib/clowne/declarations/exclude_association.rb +24 -0
- data/lib/clowne/declarations/finalize.rb +20 -0
- data/lib/clowne/declarations/include_association.rb +45 -0
- data/lib/clowne/declarations/nullify.rb +20 -0
- data/lib/clowne/declarations/trait.rb +44 -0
- data/lib/clowne/dsl.rb +14 -0
- data/lib/clowne/ext/string_constantize.rb +23 -0
- data/lib/clowne/plan.rb +81 -0
- data/lib/clowne/planner.rb +40 -0
- data/lib/clowne/version.rb +3 -1
- metadata +73 -12
data/Rakefile
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rubocop/rake_task'
|
3
4
|
|
4
|
-
|
5
|
-
|
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 :
|
8
|
+
task default: [:rubocop, :spec]
|
data/clowne.gemspec
CHANGED
@@ -1,27 +1,28 @@
|
|
1
|
-
|
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
|
3
|
+
require 'clowne/version'
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
6
|
+
spec.name = 'clowne'
|
8
7
|
spec.version = Clowne::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
8
|
+
spec.authors = ['Vladimir Dementyev', 'Sverchkov Nikolay']
|
9
|
+
spec.email = ['palkan@evilmartians.com', 'ssnikolay@gmail.com']
|
11
10
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
14
|
-
spec.homepage =
|
15
|
-
spec.license =
|
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 =
|
19
|
+
spec.bindir = 'exe'
|
21
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
-
spec.require_paths = [
|
21
|
+
spec.require_paths = ['lib']
|
23
22
|
|
24
|
-
spec.add_development_dependency
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency
|
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
|
data/lib/clowne.rb
CHANGED
@@ -1,5 +1,39 @@
|
|
1
|
-
|
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
|
-
#
|
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'
|