clowne 0.0.1 → 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Adapters
|
5
|
+
class Base
|
6
|
+
module Finalize # :nodoc: all
|
7
|
+
def self.call(source, record, declaration, params:)
|
8
|
+
declaration.block.call(source, record, params)
|
9
|
+
record
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Clowne::Adapters::Base.register_resolver(
|
17
|
+
:finalize, Clowne::Adapters::Base::Finalize,
|
18
|
+
after: :nullify
|
19
|
+
)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Adapters
|
5
|
+
class Base
|
6
|
+
module Nullify # :nodoc: all
|
7
|
+
def self.call(_source, record, declaration, **_options)
|
8
|
+
declaration.attributes.each do |attr|
|
9
|
+
record.__send__("#{attr}=", nil)
|
10
|
+
end
|
11
|
+
|
12
|
+
record
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Clowne::Adapters::Base.register_resolver(:nullify, Clowne::Adapters::Base::Nullify)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Adapters
|
5
|
+
class Registry # :nodoc: all
|
6
|
+
attr_reader :actions, :mapping
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@actions = []
|
10
|
+
@mapping = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def insert_after(after, action)
|
14
|
+
validate_uniq!(action)
|
15
|
+
|
16
|
+
after_index = actions.find_index(after)
|
17
|
+
|
18
|
+
raise "Plan action not found: #{after}" if after_index.nil?
|
19
|
+
|
20
|
+
actions.insert(after_index + 1, action)
|
21
|
+
end
|
22
|
+
|
23
|
+
def insert_before(before, action)
|
24
|
+
validate_uniq!(action)
|
25
|
+
|
26
|
+
before_index = actions.find_index(before)
|
27
|
+
|
28
|
+
raise "Plan action not found: #{before}" if before_index.nil?
|
29
|
+
|
30
|
+
actions.insert(before_index, action)
|
31
|
+
end
|
32
|
+
|
33
|
+
def append(action)
|
34
|
+
validate_uniq!(action)
|
35
|
+
actions.push action
|
36
|
+
end
|
37
|
+
|
38
|
+
def prepend(action)
|
39
|
+
validate_uniq!(action)
|
40
|
+
actions.unshift action
|
41
|
+
end
|
42
|
+
|
43
|
+
def dup
|
44
|
+
self.class.new.tap do |duped|
|
45
|
+
actions.each { |act| duped.append(act) }
|
46
|
+
duped.mapping = mapping.dup
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
attr_writer :mapping
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def validate_uniq!(action)
|
57
|
+
raise "Plan action already registered: #{action}" if actions.include?(action)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'clowne/planner'
|
4
|
+
require 'clowne/dsl'
|
5
|
+
|
6
|
+
module Clowne # :nodoc: all
|
7
|
+
class UnprocessableSourceError < StandardError; end
|
8
|
+
class ConfigurationError < StandardError; end
|
9
|
+
|
10
|
+
class Cloner
|
11
|
+
extend Clowne::DSL
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def inherited(subclass)
|
15
|
+
subclass.adapter(adapter) unless self == Clowne::Cloner
|
16
|
+
subclass.declarations = declarations.dup
|
17
|
+
|
18
|
+
return if traits.nil?
|
19
|
+
|
20
|
+
traits.each do |name, trait|
|
21
|
+
subclass.traits[name] = trait.dup
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def declarations
|
26
|
+
return @declarations if instance_variable_defined?(:@declarations)
|
27
|
+
@declarations = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def traits
|
31
|
+
return @traits if instance_variable_defined?(:@traits)
|
32
|
+
@traits = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def register_trait(name, block)
|
36
|
+
@traits ||= {}
|
37
|
+
@traits[name] ||= Declarations::Trait.new
|
38
|
+
@traits[name].extend_with(block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# rubocop: disable Metrics/AbcSize, Metrics/MethodLength
|
42
|
+
# rubocop: disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
43
|
+
def call(object, **options)
|
44
|
+
raise(UnprocessableSourceError, 'Nil is not cloneable object') if object.nil?
|
45
|
+
|
46
|
+
raise(ConfigurationError, 'Adapter is not defined') if adapter.nil?
|
47
|
+
|
48
|
+
traits = options.delete(:traits)
|
49
|
+
|
50
|
+
traits = Array(traits) unless traits.nil?
|
51
|
+
|
52
|
+
plan =
|
53
|
+
if traits.nil? || traits.empty?
|
54
|
+
default_plan
|
55
|
+
else
|
56
|
+
plan_with_traits(traits)
|
57
|
+
end
|
58
|
+
|
59
|
+
plan = Clowne::Planner.enhance(plan, Proc.new) if block_given?
|
60
|
+
|
61
|
+
adapter.clone(object, plan, params: options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# rubocop: enable Metrics/AbcSize, Metrics/MethodLength
|
65
|
+
# rubocop: enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
66
|
+
|
67
|
+
def default_plan
|
68
|
+
return @default_plan if instance_variable_defined?(:@default_plan)
|
69
|
+
@default_plan = Clowne::Planner.compile(self)
|
70
|
+
end
|
71
|
+
|
72
|
+
def plan_with_traits(ids)
|
73
|
+
# Cache plans for combinations of traits
|
74
|
+
traits_id = ids.map(&:to_s).join(':')
|
75
|
+
return traits_plans[traits_id] if traits_plans.key?(traits_id)
|
76
|
+
traits_plans[traits_id] = Clowne::Planner.compile(
|
77
|
+
self, traits: ids
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
attr_writer :declarations
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def traits_plans
|
88
|
+
return @traits_plans if instance_variable_defined?(:@traits_plans)
|
89
|
+
@traits_plans = {}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'clowne/dsl'
|
4
|
+
require 'clowne/plan'
|
5
|
+
|
6
|
+
module Clowne
|
7
|
+
module Declarations # :nodoc:
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def add(id, declaration = nil)
|
11
|
+
declaration = Proc.new if block_given?
|
12
|
+
|
13
|
+
if declaration.is_a?(Class)
|
14
|
+
DSL.send(:define_method, id) do |*args, &block|
|
15
|
+
declarations.push declaration.new(*args, &block)
|
16
|
+
end
|
17
|
+
elsif declaration.is_a?(Proc)
|
18
|
+
DSL.send(:define_method, id, &declaration)
|
19
|
+
else
|
20
|
+
raise ArgumentError, "Unsupported declaration type: #{declaration.class}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'clowne/declarations/exclude_association'
|
27
|
+
require 'clowne/declarations/finalize'
|
28
|
+
require 'clowne/declarations/include_association'
|
29
|
+
require 'clowne/declarations/nullify'
|
30
|
+
require 'clowne/declarations/trait'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Declarations
|
5
|
+
class ExcludeAssociation # :nodoc: all
|
6
|
+
attr_accessor :name
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
@name = name.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
def compile(plan)
|
13
|
+
plan.remove_from(:association, name)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Clowne::Declarations.add :exclude_association, Clowne::Declarations::ExcludeAssociation
|
20
|
+
Clowne::Declarations.add :exclude_associations do |*names|
|
21
|
+
names.each do |name|
|
22
|
+
declarations.push Clowne::Declarations::ExcludeAssociation.new(name)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Declarations
|
5
|
+
class Finalize # :nodoc: all
|
6
|
+
attr_reader :block
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
raise ArgumentError, 'Block is required for finalize' unless block_given?
|
10
|
+
@block = Proc.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def compile(plan)
|
14
|
+
plan.add(:finalize, self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Clowne::Declarations.add :finalize, Clowne::Declarations::Finalize
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'clowne/ext/string_constantize'
|
4
|
+
|
5
|
+
module Clowne
|
6
|
+
module Declarations
|
7
|
+
class IncludeAssociation # :nodoc: all
|
8
|
+
using Clowne::Ext::StringConstantize
|
9
|
+
|
10
|
+
attr_accessor :name, :scope, :options
|
11
|
+
|
12
|
+
def initialize(name, scope = nil, **options)
|
13
|
+
@name = name.to_sym
|
14
|
+
@scope = scope
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def compile(plan)
|
19
|
+
plan.add_to(:association, name, self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def clone_with
|
23
|
+
return @clone_with if instance_variable_defined?(:@clone_with)
|
24
|
+
@clone_with =
|
25
|
+
case options[:clone_with]
|
26
|
+
when String, Symbol
|
27
|
+
options[:clone_with].to_s.constantize
|
28
|
+
else
|
29
|
+
options[:clone_with]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def traits
|
34
|
+
options[:traits]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Clowne::Declarations.add :include_association, Clowne::Declarations::IncludeAssociation
|
41
|
+
Clowne::Declarations.add :include_associations do |*names|
|
42
|
+
names.each do |name|
|
43
|
+
declarations.push Clowne::Declarations::IncludeAssociation.new(name)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Declarations
|
5
|
+
class Nullify # :nodoc: all
|
6
|
+
attr_reader :attributes
|
7
|
+
|
8
|
+
def initialize(*attributes)
|
9
|
+
raise ArgumentError, 'At least one attribute required' if attributes.empty?
|
10
|
+
@attributes = attributes
|
11
|
+
end
|
12
|
+
|
13
|
+
def compile(plan)
|
14
|
+
plan.add(:nullify, self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Clowne::Declarations.add :nullify, Clowne::Declarations::Nullify
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Declarations
|
5
|
+
class Trait # :nodoc: all
|
6
|
+
def initialize
|
7
|
+
@blocks = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def extend_with(block)
|
11
|
+
@blocks << block
|
12
|
+
end
|
13
|
+
|
14
|
+
def compiled
|
15
|
+
return @compiled if instance_variable_defined?(:@compiled)
|
16
|
+
@compiled = compile
|
17
|
+
end
|
18
|
+
|
19
|
+
def dup
|
20
|
+
self.class.new.tap do |duped|
|
21
|
+
blocks.each { |b| duped.extend_with(b) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :blocks
|
28
|
+
|
29
|
+
def compile
|
30
|
+
anonymous_cloner = Class.new(Clowne::Cloner)
|
31
|
+
|
32
|
+
blocks.each do |block|
|
33
|
+
anonymous_cloner.instance_eval(&block)
|
34
|
+
end
|
35
|
+
|
36
|
+
anonymous_cloner.declarations
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Clowne::Declarations.add :trait do |name, &block|
|
43
|
+
register_trait name, block
|
44
|
+
end
|
data/lib/clowne/dsl.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module DSL # :nodoc: all
|
5
|
+
def adapter(adapter = nil)
|
6
|
+
if adapter.nil?
|
7
|
+
return @_adapter if instance_variable_defined?(:@_adapter)
|
8
|
+
@_adapter = Clowne.default_adapter
|
9
|
+
else
|
10
|
+
@_adapter = Clowne.resolve_adapter(adapter)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Ext
|
5
|
+
# Add simple constantize method to String
|
6
|
+
module StringConstantize
|
7
|
+
refine String do
|
8
|
+
def constantize
|
9
|
+
names = split('::')
|
10
|
+
|
11
|
+
Object.const_get(self) if names.empty?
|
12
|
+
|
13
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
14
|
+
names.shift if names.size > 1 && names.first.empty?
|
15
|
+
|
16
|
+
names.inject(Object) do |constant, name|
|
17
|
+
constant.const_get(name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/clowne/plan.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
class Plan # :nodoc: all
|
5
|
+
class TwoPhaseSet
|
6
|
+
def initialize
|
7
|
+
@added = {}
|
8
|
+
@removed = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def []=(k, v)
|
12
|
+
return if @removed.include?(k)
|
13
|
+
@added[k] = v
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete(k)
|
17
|
+
return if @removed.include?(k)
|
18
|
+
@removed << k
|
19
|
+
@added.delete(k)
|
20
|
+
end
|
21
|
+
|
22
|
+
def values
|
23
|
+
@added.values
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(registry)
|
28
|
+
@registry = registry
|
29
|
+
@data = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def add(type, declaration)
|
33
|
+
data[type] = [] unless data.key?(type)
|
34
|
+
data[type] << declaration
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_to(type, id, declaration)
|
38
|
+
data[type] = TwoPhaseSet.new unless data.key?(type)
|
39
|
+
data[type][id] = declaration
|
40
|
+
end
|
41
|
+
|
42
|
+
def set(type, declaration)
|
43
|
+
data[type] = declaration
|
44
|
+
end
|
45
|
+
|
46
|
+
def get(type)
|
47
|
+
data[type]
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove(type)
|
51
|
+
data.delete(type)
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_from(type, id)
|
55
|
+
return unless data[type]
|
56
|
+
data[type].delete(id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def declarations
|
60
|
+
registry.actions.flat_map do |type|
|
61
|
+
value = data[type]
|
62
|
+
next if value.nil?
|
63
|
+
value = value.values if value.is_a?(TwoPhaseSet)
|
64
|
+
value = Array(value)
|
65
|
+
value.map { |v| [type, v] }
|
66
|
+
end.compact
|
67
|
+
end
|
68
|
+
|
69
|
+
def dup
|
70
|
+
self.class.new(registry).tap do |duped|
|
71
|
+
data.each do |k, v|
|
72
|
+
duped.set(k, v.dup)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
attr_reader :data, :registry
|
80
|
+
end
|
81
|
+
end
|