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
@@ -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
|