clowne 0.2.0 → 1.0.0

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.travis.yml +6 -3
  4. data/CHANGELOG.md +14 -0
  5. data/Gemfile +1 -1
  6. data/README.md +30 -9
  7. data/clowne.gemspec +4 -3
  8. data/docs/active_record.md +2 -2
  9. data/docs/after_persist.md +80 -0
  10. data/docs/basic_example.md +22 -5
  11. data/docs/clone_mapper.md +62 -0
  12. data/docs/customization.md +2 -1
  13. data/docs/exclude_association.md +5 -4
  14. data/docs/finalize.md +2 -2
  15. data/docs/from_v02_to_v1.md +91 -0
  16. data/docs/implicit_cloner.md +1 -1
  17. data/docs/include_association.md +4 -4
  18. data/docs/init_as.md +10 -2
  19. data/docs/inline_configuration.md +4 -2
  20. data/docs/installation.md +31 -1
  21. data/docs/nullify.md +2 -2
  22. data/docs/operation.md +58 -0
  23. data/docs/overview.md +24 -0
  24. data/docs/parameters.md +5 -4
  25. data/docs/sequel.md +15 -18
  26. data/docs/supported_adapters.md +2 -2
  27. data/docs/testing.md +7 -5
  28. data/docs/web/README.md +6 -0
  29. data/docs/web/core/Footer.js +5 -9
  30. data/docs/web/i18n/en.json +8 -4
  31. data/docs/web/pages/en/index.js +1 -1
  32. data/docs/web/sidebars.json +10 -4
  33. data/docs/web/siteConfig.js +6 -4
  34. data/docs/web/static/css/custom.css +16 -10
  35. data/gemfiles/activerecord42.gemfile +3 -1
  36. data/gemfiles/jruby.gemfile +2 -0
  37. data/gemfiles/railsmaster.gemfile +2 -0
  38. data/lib/clowne.rb +3 -0
  39. data/lib/clowne/adapters/active_record.rb +2 -3
  40. data/lib/clowne/adapters/active_record/associations/base.rb +0 -4
  41. data/lib/clowne/adapters/active_record/associations/has_one.rb +2 -1
  42. data/lib/clowne/adapters/active_record/resolvers/association.rb +38 -0
  43. data/lib/clowne/adapters/base.rb +42 -43
  44. data/lib/clowne/adapters/base/association.rb +24 -15
  45. data/lib/clowne/adapters/registry.rb +49 -0
  46. data/lib/clowne/adapters/sequel.rb +10 -6
  47. data/lib/clowne/adapters/sequel/associations/base.rb +8 -4
  48. data/lib/clowne/adapters/sequel/associations/many_to_many.rb +6 -2
  49. data/lib/clowne/adapters/sequel/associations/one_to_many.rb +7 -2
  50. data/lib/clowne/adapters/sequel/associations/one_to_one.rb +7 -2
  51. data/lib/clowne/adapters/sequel/operation.rb +32 -0
  52. data/lib/clowne/adapters/sequel/record_wrapper.rb +0 -16
  53. data/lib/clowne/adapters/sequel/resolvers/after_persist.rb +22 -0
  54. data/lib/clowne/adapters/sequel/resolvers/association.rb +51 -0
  55. data/lib/clowne/adapters/sequel/specifications/after_persist_does_not_support.rb +15 -0
  56. data/lib/clowne/cloner.rb +27 -20
  57. data/lib/clowne/declarations.rb +2 -1
  58. data/lib/clowne/declarations/after_persist.rb +21 -0
  59. data/lib/clowne/declarations/finalize.rb +1 -0
  60. data/lib/clowne/declarations/include_association.rb +2 -1
  61. data/lib/clowne/declarations/init_as.rb +1 -0
  62. data/lib/clowne/declarations/nullify.rb +1 -0
  63. data/lib/clowne/declarations/trait.rb +1 -0
  64. data/lib/clowne/dsl.rb +9 -0
  65. data/lib/clowne/ext/lambda_as_proc.rb +1 -0
  66. data/lib/clowne/ext/record_key.rb +12 -0
  67. data/lib/clowne/ext/yield_self_then.rb +25 -0
  68. data/lib/clowne/planner.rb +6 -3
  69. data/lib/clowne/resolvers/after_persist.rb +18 -0
  70. data/lib/clowne/resolvers/finalize.rb +12 -0
  71. data/lib/clowne/resolvers/init_as.rb +13 -0
  72. data/lib/clowne/resolvers/nullify.rb +15 -0
  73. data/lib/clowne/rspec/helpers.rb +1 -0
  74. data/lib/clowne/utils/clone_mapper.rb +26 -0
  75. data/lib/clowne/utils/operation.rb +83 -0
  76. data/lib/clowne/utils/options.rb +39 -0
  77. data/lib/clowne/utils/params.rb +64 -0
  78. data/lib/clowne/utils/plan.rb +90 -0
  79. data/lib/clowne/version.rb +1 -1
  80. metadata +44 -18
  81. data/docs/configuration.md +0 -29
  82. data/docs/execution_order.md +0 -14
  83. data/docs/web/static/fonts/StemText.woff +0 -0
  84. data/docs/web/static/fonts/StemTextBold.woff +0 -0
  85. data/lib/clowne/adapters/active_record/association.rb +0 -34
  86. data/lib/clowne/adapters/base/finalize.rb +0 -19
  87. data/lib/clowne/adapters/base/init_as.rb +0 -21
  88. data/lib/clowne/adapters/base/nullify.rb +0 -19
  89. data/lib/clowne/adapters/sequel/association.rb +0 -47
  90. data/lib/clowne/params.rb +0 -62
  91. data/lib/clowne/plan.rb +0 -83
@@ -3,6 +3,55 @@
3
3
  module Clowne
4
4
  module Adapters
5
5
  class Registry # :nodoc: all
6
+ module Container
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+
10
+ base.class_eval do
11
+ self.registry = Registry.new
12
+
13
+ def registry
14
+ self.class.registry
15
+ end
16
+
17
+ def resolver_for(type)
18
+ self.class.resolver_for(type)
19
+ end
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ attr_reader :registry
25
+
26
+ def inherited(subclass)
27
+ # Duplicate registry
28
+ subclass.registry = registry.dup
29
+ end
30
+
31
+ def resolver_for(type)
32
+ registry.mapping[type] || raise("Uknown resolver #{type} for #{self}")
33
+ end
34
+
35
+ def register_resolver(type, resolver, after: nil, before: nil, prepend: nil)
36
+ registry.mapping[type] = resolver
37
+
38
+ if prepend
39
+ registry.unshift type
40
+ elsif after
41
+ registry.insert_after after, type
42
+ elsif before
43
+ registry.insert_before before, type
44
+ else
45
+ registry.append type
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ attr_writer :registry
52
+ end
53
+ end
54
+
6
55
  attr_reader :actions, :mapping
7
56
 
8
57
  def initialize
@@ -6,12 +6,14 @@ module Clowne
6
6
  module Adapters
7
7
  # Cloning adapter for Sequel
8
8
  class Sequel < Base
9
- def init_record(source)
10
- RecordWrapper.new(source)
11
- end
9
+ class << self
10
+ def dup_record(record)
11
+ Clowne::Adapters::Sequel::Copier.call(record)
12
+ end
12
13
 
13
- def dup_source(source)
14
- Clowne::Adapters::Sequel::Copier.call(source)
14
+ def operation_class
15
+ Clowne::Adapters::Sequel::Operation
16
+ end
15
17
  end
16
18
  end
17
19
  end
@@ -19,7 +21,9 @@ end
19
21
 
20
22
  ::Sequel::Model.extend Clowne::Ext::ORMExt
21
23
 
24
+ require 'clowne/adapters/sequel/operation'
22
25
  require 'clowne/adapters/sequel/associations'
23
- require 'clowne/adapters/sequel/association'
24
26
  require 'clowne/adapters/sequel/copier'
25
27
  require 'clowne/adapters/sequel/record_wrapper'
28
+ require 'clowne/adapters/sequel/resolvers/association'
29
+ require 'clowne/adapters/sequel/resolvers/after_persist'
@@ -9,13 +9,17 @@ module Clowne
9
9
  class Base < Base::Association
10
10
  private
11
11
 
12
- def clone_record(record)
13
- Clowne::Adapters::Sequel::Copier.call(record)
14
- end
15
-
16
12
  def init_scope
17
13
  @_init_scope ||= source.__send__([association_name, 'dataset'].join('_'))
18
14
  end
15
+
16
+ def record_wrapper(record)
17
+ operation.record_wrapper(record)
18
+ end
19
+
20
+ def operation
21
+ @_operation ||= adapter.class.operation_class.current
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -6,9 +6,13 @@ module Clowne
6
6
  module Associations
7
7
  class ManyToMany < Base
8
8
  def call(record)
9
- clones = with_scope.map { |child| clone_one(child) }
9
+ clones = with_scope
10
+ .lazy
11
+ .map(&method(:clone_one))
12
+ .map(&method(:record_wrapper))
13
+ .to_a
10
14
 
11
- record.remember_assoc(:"#{association_name}_attributes", clones)
15
+ record_wrapper(record).remember_assoc(:"#{association_name}_attributes", clones)
12
16
 
13
17
  record
14
18
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'clowne/ext/yield_self_then'
4
+
5
+ using Clowne::Ext::YieldSelfThen
6
+
3
7
  module Clowne
4
8
  module Adapters # :nodoc: all
5
9
  class Sequel
@@ -10,9 +14,10 @@ module Clowne
10
14
  with_scope.map do |child|
11
15
  clone_one(child).tap do |child_clone|
12
16
  child_clone[:"#{reflection[:key]}"] = nil
13
- end
17
+ end.then(&method(:record_wrapper))
14
18
  end
15
- record.remember_assoc(:"#{association_name}_attributes", clones)
19
+
20
+ record_wrapper(record).remember_assoc(:"#{association_name}_attributes", clones)
16
21
 
17
22
  record
18
23
  end
@@ -8,11 +8,16 @@ module Clowne
8
8
  def call(record)
9
9
  child = association
10
10
  return record unless child
11
- warn '[Clowne] Has one association does not support scopes' unless scope.nil?
11
+
12
+ warn '[Clowne] Has one association does not support scope' unless declaration.scope.nil?
12
13
 
13
14
  child_clone = clone_one(child)
14
15
  child_clone[:"#{reflection[:key]}"] = nil
15
- record.remember_assoc(:"#{association_name}_attributes", child_clone)
16
+
17
+ record_wrapper(record).remember_assoc(
18
+ :"#{association_name}_attributes",
19
+ record_wrapper(child_clone)
20
+ )
16
21
 
17
22
  record
18
23
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clowne/ext/record_key'
4
+
5
+ module Clowne
6
+ module Adapters
7
+ class Sequel # :nodoc: all
8
+ class Operation < Clowne::Utils::Operation
9
+ include Clowne::Ext::RecordKey
10
+
11
+ def initialize(mapper)
12
+ super
13
+ @records = {}
14
+ end
15
+
16
+ def record_wrapper(record)
17
+ @records[key(record)] ||= RecordWrapper.new(record)
18
+ end
19
+
20
+ def hash
21
+ @records[key(@clone)]
22
+ end
23
+
24
+ def to_record
25
+ return @_record if defined?(@_record)
26
+
27
+ @_record = @records[key(@clone)].to_model
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -15,10 +15,6 @@ module Clowne
15
15
  @association_store[association] = value
16
16
  end
17
17
 
18
- def save
19
- to_model.save
20
- end
21
-
22
18
  def to_model
23
19
  association_store.each_with_object(record) do |(name, value), acc|
24
20
  acc.send("#{name}=", association_to_model(value))
@@ -32,18 +28,6 @@ module Clowne
32
28
  end
33
29
  end
34
30
 
35
- def respond_to_missing?(method_name, include_private = false)
36
- record.respond_to?(method_name) || super
37
- end
38
-
39
- def method_missing(method_name, *args, &block)
40
- if record.respond_to?(method_name)
41
- record.public_send(method_name, *args, &block)
42
- else
43
- super
44
- end
45
- end
46
-
47
31
  private
48
32
 
49
33
  def association_to_model(assoc)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clowne/adapters/sequel/specifications/after_persist_does_not_support'
4
+
5
+ module Clowne
6
+ module Adapters # :nodoc: all
7
+ class Sequel
8
+ module Resolvers
9
+ class AfterPersist
10
+ def self.call(_source, _record, _declaration, **)
11
+ raise Specifications::AfterPersistDoesNotSupportException
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ Clowne::Adapters::Sequel.register_resolver(
20
+ :after_persist,
21
+ Clowne::Adapters::Sequel::Resolvers::AfterPersist
22
+ )
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters # :nodoc: all
5
+ class Sequel
6
+ module Resolvers
7
+ class Association
8
+ class << self
9
+ # rubocop: disable Metrics/ParameterLists
10
+ def call(source, record, declaration, adapter:, params:, **_options)
11
+ with_clonable(source, record, declaration) do
12
+ reflection = source.class.association_reflections[declaration.name.to_sym]
13
+
14
+ cloner_class = Associations.cloner_for(reflection)
15
+
16
+ cloner_class.new(reflection, source, declaration, adapter, params).call(record)
17
+
18
+ record
19
+ end
20
+ end
21
+ # rubocop: enable Metrics/ParameterLists
22
+
23
+ private
24
+
25
+ def with_clonable(source, record, declaration)
26
+ if clonable_assoc?(source, declaration)
27
+ yield(record)
28
+ else
29
+ warn <<-WARN
30
+ Relation #{declaration.name} of #{source.class.name} does not configure for Sequel::Plugins::NestedAttributes
31
+ WARN
32
+ end
33
+
34
+ record
35
+ end
36
+
37
+ def clonable_assoc?(source, declaration)
38
+ source.class.plugins.include?(::Sequel::Plugins::NestedAttributes) &&
39
+ source.respond_to?(:"#{declaration.name.to_s}_attributes=")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ Clowne::Adapters::Sequel.register_resolver(
49
+ :association,
50
+ Clowne::Adapters::Sequel::Resolvers::Association
51
+ )
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Adapters
5
+ class Sequel
6
+ module Specifications # :nodoc: all
7
+ class AfterPersistDoesNotSupportException < StandardError
8
+ def message
9
+ 'Sequel adapter does not support after_persist callback'
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,7 +2,9 @@
2
2
 
3
3
  require 'clowne/planner'
4
4
  require 'clowne/dsl'
5
- require 'clowne/params'
5
+ require 'clowne/utils/options'
6
+ require 'clowne/utils/params'
7
+ require 'clowne/utils/operation'
6
8
 
7
9
  module Clowne # :nodoc: all
8
10
  class UnprocessableSourceError < StandardError; end
@@ -25,11 +27,13 @@ module Clowne # :nodoc: all
25
27
 
26
28
  def declarations
27
29
  return @declarations if instance_variable_defined?(:@declarations)
30
+
28
31
  @declarations = []
29
32
  end
30
33
 
31
34
  def traits
32
35
  return @traits if instance_variable_defined?(:@traits)
36
+
33
37
  @traits = {}
34
38
  end
35
39
 
@@ -40,50 +44,46 @@ module Clowne # :nodoc: all
40
44
  end
41
45
 
42
46
  # rubocop: disable Metrics/AbcSize, Metrics/MethodLength
43
- # rubocop: disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
44
47
  def call(object, **options)
45
48
  raise(UnprocessableSourceError, 'Nil is not cloneable object') if object.nil?
46
49
 
47
- raise(ConfigurationError, 'Adapter is not defined') if adapter.nil?
48
-
49
- traits = options.delete(:traits)
50
-
51
- only = options.delete(:clowne_only_actions)
50
+ options = Clowne::Utils::Options.new(options)
51
+ current_adapter = current_adapter(options.adapter)
52
52
 
53
- traits = Array(traits) unless traits.nil?
53
+ raise(ConfigurationError, 'Adapter is not defined') if current_adapter.nil?
54
54
 
55
55
  plan =
56
- if traits.nil? || traits.empty?
57
- default_plan
56
+ if options.traits.empty?
57
+ default_plan(current_adapter: current_adapter)
58
58
  else
59
- plan_with_traits(traits)
59
+ plan_with_traits(options.traits, current_adapter: current_adapter)
60
60
  end
61
61
 
62
62
  plan = Clowne::Planner.enhance(plan, Proc.new) if block_given?
63
63
 
64
- plan = Clowne::Planner.filter_declarations(plan, only)
64
+ plan = Clowne::Planner.filter_declarations(plan, options.only)
65
65
 
66
- adapter.clone(object, plan, params: options)
66
+ call_operation(current_adapter, object, plan, options)
67
67
  end
68
+ # rubocop: enable Metrics/AbcSize, Metrics/MethodLength
68
69
 
69
70
  def partial_apply(only, *args, **hargs)
70
71
  call(*args, **hargs, clowne_only_actions: prepare_only(only))
71
72
  end
72
73
 
73
- # rubocop: enable Metrics/AbcSize, Metrics/MethodLength
74
- # rubocop: enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
75
-
76
- def default_plan
74
+ def default_plan(current_adapter: adapter)
77
75
  return @default_plan if instance_variable_defined?(:@default_plan)
78
- @default_plan = Clowne::Planner.compile(self)
76
+
77
+ @default_plan = Clowne::Planner.compile(current_adapter, self)
79
78
  end
80
79
 
81
- def plan_with_traits(ids)
80
+ def plan_with_traits(ids, current_adapter: adapter)
82
81
  # Cache plans for combinations of traits
83
82
  traits_id = ids.map(&:to_s).join(':')
84
83
  return traits_plans[traits_id] if traits_plans.key?(traits_id)
84
+
85
85
  traits_plans[traits_id] = Clowne::Planner.compile(
86
- self, traits: ids
86
+ current_adapter, self, traits: ids
87
87
  )
88
88
  end
89
89
 
@@ -93,8 +93,15 @@ module Clowne # :nodoc: all
93
93
 
94
94
  private
95
95
 
96
+ def call_operation(adapter, object, plan, options)
97
+ adapter.class.operation_class.wrap(mapper: options.mapper) do
98
+ adapter.clone(object, plan, params: options.params)
99
+ end
100
+ end
101
+
96
102
  def traits_plans
97
103
  return @traits_plans if instance_variable_defined?(:@traits_plans)
104
+
98
105
  @traits_plans = {}
99
106
  end
100
107
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'clowne/dsl'
4
- require 'clowne/plan'
4
+ require 'clowne/utils/plan'
5
5
 
6
6
  module Clowne
7
7
  module Declarations # :nodoc:
@@ -30,3 +30,4 @@ require 'clowne/declarations/finalize'
30
30
  require 'clowne/declarations/include_association'
31
31
  require 'clowne/declarations/nullify'
32
32
  require 'clowne/declarations/trait'
33
+ require 'clowne/declarations/after_persist'
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Declarations
5
+ class AfterPersist < Base # :nodoc: all
6
+ attr_reader :block
7
+
8
+ def initialize
9
+ raise ArgumentError, 'Block is required for after_persist' unless block_given?
10
+
11
+ @block = Proc.new
12
+ end
13
+
14
+ def compile(plan)
15
+ plan.add(:after_persist, self)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ Clowne::Declarations.add :after_persist, Clowne::Declarations::AfterPersist