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
@@ -7,6 +7,7 @@ module Clowne
7
7
 
8
8
  def initialize
9
9
  raise ArgumentError, 'Block is required for finalize' unless block_given?
10
+
10
11
  @block = Proc.new
11
12
  end
12
13
 
@@ -25,7 +25,7 @@ module Clowne
25
25
  end
26
26
 
27
27
  def params_proxy
28
- @_params_proxy ||= Clowne::Params.proxy(options[:params])
28
+ @_params_proxy ||= Clowne::Utils::Params.proxy(options[:params])
29
29
  end
30
30
 
31
31
  def params
@@ -34,6 +34,7 @@ module Clowne
34
34
 
35
35
  def clone_with
36
36
  return @clone_with if instance_variable_defined?(:@clone_with)
37
+
37
38
  @clone_with =
38
39
  case options[:clone_with]
39
40
  when String, Symbol
@@ -7,6 +7,7 @@ module Clowne
7
7
 
8
8
  def initialize
9
9
  raise ArgumentError, 'Block is required for init_as' unless block_given?
10
+
10
11
  @block = Proc.new
11
12
  end
12
13
 
@@ -7,6 +7,7 @@ module Clowne
7
7
 
8
8
  def initialize(*attributes)
9
9
  raise ArgumentError, 'At least one attribute required' if attributes.empty?
10
+
10
11
  @attributes = attributes
11
12
  end
12
13
 
@@ -13,6 +13,7 @@ module Clowne
13
13
 
14
14
  def compiled
15
15
  return @compiled if instance_variable_defined?(:@compiled)
16
+
16
17
  @compiled = compile
17
18
  end
18
19
 
@@ -5,10 +5,19 @@ module Clowne
5
5
  def adapter(adapter = nil)
6
6
  if adapter.nil?
7
7
  return @_adapter if instance_variable_defined?(:@_adapter)
8
+
8
9
  @_adapter = Clowne.default_adapter
9
10
  else
10
11
  @_adapter = Clowne.resolve_adapter(adapter)
11
12
  end
12
13
  end
14
+
15
+ private
16
+
17
+ def current_adapter(user_adapter)
18
+ return adapter if user_adapter.nil?
19
+
20
+ Clowne.resolve_adapter(user_adapter)
21
+ end
13
22
  end
14
23
  end
@@ -7,6 +7,7 @@ module Clowne
7
7
  refine Proc do
8
8
  def to_proc
9
9
  return self unless lambda?
10
+
10
11
  this = self
11
12
  proc { |*args| this.call(*args.take(this.arity)) }
12
13
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Ext
5
+ module RecordKey # :nodoc: all
6
+ def key(record)
7
+ id = record.respond_to?(:id) && record.id ? record.id : record.__id__
8
+ [record.class.name, id].join('#')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Ext # :nodoc: all
5
+ # Add yield_self and then if missing
6
+ module YieldSelfThen
7
+ module Ext
8
+ unless nil.respond_to?(:yield_self)
9
+ def yield_self
10
+ yield self
11
+ end
12
+ end
13
+
14
+ alias then yield_self
15
+ end
16
+
17
+ # See https://github.com/jruby/jruby/issues/5220
18
+ ::Object.include(Ext) if RUBY_PLATFORM =~ /java/i
19
+
20
+ refine Object do
21
+ include Ext
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'clowne/plan'
3
+ require 'clowne/utils/plan'
4
4
 
5
5
  module Clowne
6
6
  class Planner # :nodoc: all
7
7
  class << self
8
8
  # Compile plan for cloner with traits
9
- def compile(cloner, traits: nil)
9
+ def compile(adapter, cloner, traits: nil)
10
10
  declarations = cloner.declarations.dup
11
11
 
12
12
  declarations += compile_traits(cloner, traits) unless traits.nil?
13
13
 
14
- declarations.each_with_object(Plan.new(cloner.adapter.registry)) do |declaration, plan|
14
+ declarations.each_with_object(
15
+ Utils::Plan.new(adapter.registry)
16
+ ) do |declaration, plan|
15
17
  declaration.compile(plan)
16
18
  end
17
19
  end
@@ -42,6 +44,7 @@ module Clowne
42
44
  traits.map do |id|
43
45
  trait = cloner.traits[id]
44
46
  raise ConfigurationError, "Trait not found: #{id}" if trait.nil?
47
+
45
48
  trait.compiled
46
49
  end.flatten
47
50
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ class Resolvers
5
+ module AfterPersist # :nodoc: all
6
+ def self.call(source, record, declaration, params:, **_options)
7
+ operation = Clowne::Utils::Operation.current
8
+ params ||= {}
9
+ operation.add_after_persist(
10
+ proc do
11
+ declaration.block.call(source, record, params.merge(mapper: operation.mapper))
12
+ end
13
+ )
14
+ record
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ class Resolvers
5
+ module Finalize # :nodoc: all
6
+ def self.call(source, record, declaration, params:, **_options)
7
+ declaration.block.call(source, record, params)
8
+ record
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ class Resolvers
5
+ module InitAs # :nodoc: all
6
+ # rubocop: disable Metrics/ParameterLists
7
+ def self.call(source, _record, declaration, params:, adapter:, **_options)
8
+ adapter.init_record(declaration.block.call(source, **params))
9
+ end
10
+ # rubocop: enable Metrics/ParameterLists
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ class Resolvers
5
+ module Nullify # :nodoc: all
6
+ def self.call(_source, record, declaration, **_options)
7
+ declaration.attributes.each do |attr|
8
+ record.__send__("#{attr}=", nil)
9
+ end
10
+
11
+ record
12
+ end
13
+ end
14
+ end
15
+ end
@@ -12,6 +12,7 @@ module Clowne
12
12
 
13
13
  def matches?(actual)
14
14
  raise ArgumentError, non_cloner_message unless actual <= ::Clowne::Cloner
15
+
15
16
  @cloner = actual
16
17
  super
17
18
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clowne/ext/record_key'
4
+
5
+ module Clowne
6
+ module Utils
7
+ class CloneMapper # :nodoc: all
8
+ def initialize
9
+ @store = {}
10
+ end
11
+
12
+ def add(origin, clone)
13
+ @store[origin] ||= clone
14
+ end
15
+
16
+ def clone_of(origin)
17
+ @store[origin]
18
+ end
19
+
20
+ def origin_of(clone)
21
+ origin, _clone = @store.rassoc(clone)
22
+ origin
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clowne/utils/clone_mapper'
4
+
5
+ module Clowne
6
+ module Utils
7
+ class Operation # :nodoc: all
8
+ THREAD_KEY = :"#{name}.clowne_operation"
9
+ DEFAULT_MAPPER = Utils::CloneMapper
10
+
11
+ private_constant :THREAD_KEY
12
+
13
+ class << self
14
+ def current
15
+ Thread.current[THREAD_KEY]
16
+ end
17
+
18
+ def wrap(mapper: nil)
19
+ return yield if current
20
+
21
+ Thread.current[THREAD_KEY] = new(mapper || DEFAULT_MAPPER.new)
22
+
23
+ current.tap do |operation|
24
+ operation.clone = yield
25
+ clear!
26
+ end
27
+ end
28
+
29
+ def clear!
30
+ Thread.current[THREAD_KEY] = nil
31
+ end
32
+ end
33
+
34
+ attr_writer :clone
35
+ attr_reader :mapper
36
+
37
+ def initialize(mapper)
38
+ @blocks = []
39
+ @mapper = mapper
40
+ end
41
+
42
+ def add_after_persist(block)
43
+ @blocks.unshift(block)
44
+ end
45
+
46
+ def add_mapping(origin, clone)
47
+ @mapper.add(origin, clone)
48
+ end
49
+
50
+ def to_record
51
+ @clone
52
+ end
53
+
54
+ def persist!
55
+ to_record.save!.tap do
56
+ run_after_persist
57
+ end
58
+ end
59
+
60
+ def persist
61
+ to_record.save.tap do |result|
62
+ next unless result
63
+
64
+ run_after_persist
65
+ end
66
+ end
67
+
68
+ def save
69
+ warn '[DEPRECATION] `save` is deprecated. Please use `persist` instead.'
70
+ @clone.save
71
+ end
72
+
73
+ def save!
74
+ warn '[DEPRECATION] `save!` is deprecated. Please use `persist!` instead.'
75
+ @clone.save!
76
+ end
77
+
78
+ def run_after_persist
79
+ @blocks.each(&:call)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Utils # :nodoc: all
5
+ class Options
6
+ INTERNAL_KEYS = %i[adapter traits clowne_only_actions mapping only].freeze
7
+
8
+ def initialize(options)
9
+ @options = options
10
+ end
11
+
12
+ def traits
13
+ @_traits ||= Array(options[:traits])
14
+ end
15
+
16
+ def only
17
+ options[:clowne_only_actions]
18
+ end
19
+
20
+ def mapper
21
+ options[:mapper]
22
+ end
23
+
24
+ def adapter
25
+ options[:adapter]
26
+ end
27
+
28
+ def params
29
+ options.dup.tap do |o|
30
+ INTERNAL_KEYS.each { |key| o.delete(key) }
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :options
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clowne/ext/lambda_as_proc'
4
+
5
+ module Clowne
6
+ module Utils
7
+ class Params # :nodoc: all
8
+ class BaseProxy
9
+ attr_reader :value
10
+
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ def permit(_params)
16
+ raise NotImplementedError
17
+ end
18
+ end
19
+
20
+ class PassProxy < BaseProxy
21
+ def permit(params:, **)
22
+ params
23
+ end
24
+ end
25
+
26
+ class NullProxy < BaseProxy
27
+ def permit(_params)
28
+ {}
29
+ end
30
+ end
31
+
32
+ class BlockProxy < BaseProxy
33
+ using Clowne::Ext::LambdaAsProc
34
+
35
+ def permit(params:, parent:)
36
+ value.to_proc.call(params, parent)
37
+ end
38
+ end
39
+
40
+ class KeyProxy < BaseProxy
41
+ def permit(params:, **)
42
+ nested_params = params.fetch(value)
43
+ return nested_params if nested_params.is_a?(Hash)
44
+
45
+ raise KeyError, "value by key '#{value}' must be a Hash"
46
+ end
47
+ end
48
+
49
+ class << self
50
+ def proxy(value)
51
+ if value == true
52
+ PassProxy
53
+ elsif value.nil? || value == false
54
+ NullProxy
55
+ elsif value.is_a?(Proc)
56
+ BlockProxy
57
+ else
58
+ KeyProxy
59
+ end.new(value)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Utils
5
+ class Plan # :nodoc: all
6
+ class TwoPhaseSet
7
+ def initialize
8
+ @added = {}
9
+ @removed = []
10
+ end
11
+
12
+ def []=(k, v)
13
+ return if @removed.include?(k)
14
+
15
+ @added[k] = v
16
+ end
17
+
18
+ def delete(k)
19
+ return if @removed.include?(k)
20
+
21
+ @removed << k
22
+ @added.delete(k)
23
+ end
24
+
25
+ def values
26
+ @added.values
27
+ end
28
+ end
29
+
30
+ def initialize(registry)
31
+ @registry = registry
32
+ @data = {}
33
+ end
34
+
35
+ def add(type, declaration)
36
+ data[type] = [] unless data.key?(type)
37
+ data[type] << declaration
38
+ end
39
+
40
+ def add_to(type, id, declaration)
41
+ data[type] = TwoPhaseSet.new unless data.key?(type)
42
+ data[type][id] = declaration
43
+ end
44
+
45
+ def set(type, declaration)
46
+ data[type] = declaration
47
+ end
48
+
49
+ def get(type)
50
+ data[type]
51
+ end
52
+
53
+ def remove(type)
54
+ data.delete(type)
55
+ end
56
+
57
+ def remove_from(type, id)
58
+ return unless data[type]
59
+
60
+ data[type].delete(id)
61
+ end
62
+
63
+ def declarations(reload = false)
64
+ return @declarations if !reload && instance_variable_defined?(:@declarations)
65
+
66
+ @declarations =
67
+ registry.actions.flat_map do |type|
68
+ value = data[type]
69
+ next if value.nil?
70
+
71
+ value = value.values if value.is_a?(TwoPhaseSet)
72
+ value = Array(value)
73
+ value.map { |v| [type, v] }
74
+ end.compact
75
+ end
76
+
77
+ def dup
78
+ self.class.new(registry).tap do |duped|
79
+ data.each do |k, v|
80
+ duped.set(k, v.dup)
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ attr_reader :data, :registry
88
+ end
89
+ end
90
+ end