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