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