tailmix 0.4.6 → 0.4.8

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +76 -145
  4. data/app/javascript/tailmix/runtime/action_dispatcher.js +132 -0
  5. data/app/javascript/tailmix/runtime/component.js +130 -0
  6. data/app/javascript/tailmix/runtime/index.js +130 -0
  7. data/app/javascript/tailmix/runtime/plugins.js +35 -0
  8. data/app/javascript/tailmix/runtime/updater.js +140 -0
  9. data/docs/01_getting_started.md +39 -0
  10. data/examples/_modal_component.arb +17 -25
  11. data/examples/button.rb +81 -0
  12. data/examples/helpers.rb +25 -0
  13. data/examples/modal_component.rb +32 -164
  14. data/lib/tailmix/definition/builders/action_builder.rb +39 -0
  15. data/lib/tailmix/definition/{contexts → builders}/actions/element_builder.rb +1 -1
  16. data/lib/tailmix/definition/{contexts → builders}/attribute_builder.rb +1 -1
  17. data/lib/tailmix/definition/builders/component_builder.rb +81 -0
  18. data/lib/tailmix/definition/{contexts → builders}/dimension_builder.rb +2 -2
  19. data/lib/tailmix/definition/builders/element_builder.rb +83 -0
  20. data/lib/tailmix/definition/builders/reactor_builder.rb +43 -0
  21. data/lib/tailmix/definition/builders/rule_builder.rb +58 -0
  22. data/lib/tailmix/definition/builders/state_builder.rb +21 -0
  23. data/lib/tailmix/definition/{contexts → builders}/variant_builder.rb +17 -2
  24. data/lib/tailmix/definition/context_builder.rb +19 -13
  25. data/lib/tailmix/definition/merger.rb +2 -1
  26. data/lib/tailmix/definition/payload_proxy.rb +16 -0
  27. data/lib/tailmix/definition/result.rb +17 -18
  28. data/lib/tailmix/dev/docs.rb +32 -7
  29. data/lib/tailmix/dev/tools.rb +0 -5
  30. data/lib/tailmix/dsl.rb +3 -5
  31. data/lib/tailmix/engine.rb +11 -1
  32. data/lib/tailmix/html/attributes.rb +22 -12
  33. data/lib/tailmix/middleware/registry_cleaner.rb +17 -0
  34. data/lib/tailmix/registry.rb +37 -0
  35. data/lib/tailmix/runtime/action_proxy.rb +29 -0
  36. data/lib/tailmix/runtime/attribute_builder.rb +102 -0
  37. data/lib/tailmix/runtime/attribute_cache.rb +23 -0
  38. data/lib/tailmix/runtime/context.rb +51 -47
  39. data/lib/tailmix/runtime/facade_builder.rb +8 -5
  40. data/lib/tailmix/runtime/state.rb +36 -0
  41. data/lib/tailmix/runtime/state_proxy.rb +34 -0
  42. data/lib/tailmix/runtime.rb +0 -1
  43. data/lib/tailmix/version.rb +1 -1
  44. data/lib/tailmix/view_helpers.rb +49 -0
  45. data/lib/tailmix.rb +4 -2
  46. metadata +34 -21
  47. data/app/javascript/tailmix/finder.js +0 -15
  48. data/app/javascript/tailmix/index.js +0 -7
  49. data/app/javascript/tailmix/mutator.js +0 -28
  50. data/app/javascript/tailmix/runner.js +0 -7
  51. data/app/javascript/tailmix/stimulus_adapter.js +0 -37
  52. data/lib/tailmix/definition/contexts/action_builder.rb +0 -31
  53. data/lib/tailmix/definition/contexts/element_builder.rb +0 -41
  54. data/lib/tailmix/definition/contexts/stimulus_builder.rb +0 -101
  55. data/lib/tailmix/dev/stimulus_generator.rb +0 -124
  56. data/lib/tailmix/runtime/stimulus/compiler.rb +0 -59
@@ -1,39 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "contexts/action_builder"
4
- require_relative "contexts/element_builder"
3
+ require_relative "builders/action_builder"
4
+ require_relative "builders/element_builder"
5
+ require_relative "builders/variant_builder"
6
+ require_relative "payload_proxy"
5
7
 
6
8
  module Tailmix
7
9
  module Definition
8
10
  class ContextBuilder
9
- attr_reader :elements, :actions
11
+ attr_reader :elements, :actions, :component_name
10
12
 
11
- def initialize
13
+ def initialize(component_name:)
12
14
  @elements = {}
13
15
  @actions = {}
16
+ @component_name = component_name
14
17
  end
15
18
 
16
- def element(name, base_classes = "", &block)
17
- builder = Contexts::ElementBuilder.new(name)
18
- builder.attributes.classes(base_classes.split)
19
+ def element(name, classes = "", &block)
20
+ builder = Builders::ElementBuilder.new(name)
21
+ builder.attributes.classes(classes.split)
19
22
 
20
23
  builder.instance_eval(&block) if block
21
-
22
24
  @elements[name.to_sym] = builder
25
+
26
+ @actions.merge!(builder.auto_actions)
23
27
  end
24
28
 
25
- def action(name, method:, &block)
26
- builder = Contexts::ActionBuilder.new(method)
27
- builder.instance_eval(&block) if block
29
+ def action(name, &block)
30
+ builder = Builders::ActionBuilder.new
31
+ proxy = Builders::PayloadProxy.new
32
+ builder.instance_exec(proxy, &block)
28
33
  @actions[name.to_sym] = builder
29
34
  end
30
35
 
31
36
  def build_definition
32
37
  Definition::Result::Context.new(
38
+ name: component_name,
33
39
  elements: @elements.transform_values(&:build_definition).freeze,
34
- actions: @actions.transform_values(&:build_definition).freeze
40
+ actions: @actions.transform_values(&:build_definition).freeze,
35
41
  )
36
42
  end
37
43
  end
38
44
  end
39
- end
45
+ end
@@ -47,7 +47,8 @@ module Tailmix
47
47
  name: parent_el.name,
48
48
  attributes: merge_attributes(parent_el.attributes, child_el.attributes),
49
49
  dimensions: merge_dimensions(parent_el.dimensions, child_el.dimensions),
50
- stimulus: merge_stimulus(parent_el.stimulus, child_el.stimulus)
50
+ stimulus: merge_stimulus(parent_el.stimulus, child_el.stimulus),
51
+ compound_variants: parent_el.compound_variants + child_el.compound_variants
51
52
  )
52
53
  end
53
54
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ # A marker indicating that the value should be taken from the runtime payload.
6
+ PayloadValue = Struct.new(:key)
7
+
8
+ # A proxy object that is passed to the `action do |payload|` block.
9
+ # It creates PayloadValue markers instead of containing the actual data.
10
+ class PayloadProxy
11
+ def [](key)
12
+ PayloadValue.new(key.to_sym)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -3,20 +3,25 @@
3
3
  module Tailmix
4
4
  module Definition
5
5
  module Result
6
- Context = Struct.new(:elements, :actions, keyword_init: true) do
6
+ Context = Struct.new(:name, :states, :actions, :elements, :plugins, :reactions, keyword_init: true) do
7
7
  def to_h
8
8
  {
9
+ name: name,
10
+ states: states,
11
+ actions: actions.transform_values(&:to_h),
9
12
  elements: elements.transform_values(&:to_h),
10
- actions: actions.transform_values(&:to_h)
13
+ plugins: plugins,
14
+ reactions: reactions
11
15
  }
12
16
  end
13
17
  end
14
18
 
15
- Element = Struct.new(:name, :attributes, :dimensions, :stimulus, keyword_init: true) do
19
+ Element = Struct.new(:name, :attributes, :dimensions, :compound_variants, :event_bindings, :attribute_bindings, :model_bindings, :default_attributes, keyword_init: true) do
16
20
  def to_h
17
21
  {
18
22
  name: name,
19
23
  attributes: attributes.to_h,
24
+ default_attributes: default_attributes,
20
25
  dimensions: dimensions.transform_values do |dimension|
21
26
  dimension.transform_values do |value|
22
27
  case value
@@ -29,12 +34,14 @@ module Tailmix
29
34
  end
30
35
  end
31
36
  end,
32
- stimulus: stimulus.to_h
37
+ compound_variants: compound_variants,
38
+ attribute_bindings: attribute_bindings,
39
+ model_bindings: model_bindings,
33
40
  }
34
41
  end
35
42
  end
36
43
 
37
- Variant = Struct.new(:class_groups, :data, :aria, keyword_init: true) do
44
+ Variant = Struct.new(:class_groups, :data, :aria, :attributes, keyword_init: true) do
38
45
  def classes
39
46
  class_groups.flat_map { |group| group[:classes] }
40
47
  end
@@ -44,7 +51,8 @@ module Tailmix
44
51
  classes: classes,
45
52
  class_groups: class_groups,
46
53
  data: data,
47
- aria: aria
54
+ aria: aria,
55
+ attributes: attributes
48
56
  }
49
57
  end
50
58
  end
@@ -57,22 +65,13 @@ module Tailmix
57
65
  end
58
66
  end
59
67
 
60
- Stimulus = Struct.new(:definitions, keyword_init: true) do
61
- def to_h
62
- {
63
- definitions: definitions
64
- }
65
- end
66
- end
67
-
68
- Action = Struct.new(:action, :mutations, keyword_init: true) do
68
+ Action = Struct.new(:transitions, keyword_init: true) do
69
69
  def to_h
70
70
  {
71
- action: action,
72
- mutations: mutations
71
+ transitions: transitions,
73
72
  }
74
73
  end
75
74
  end
76
75
  end
77
76
  end
78
- end
77
+ end
@@ -18,10 +18,8 @@ module Tailmix
18
18
  output << ""
19
19
 
20
20
  output << generate_dimensions_docs
21
- output << ""
21
+ output << generate_compound_variants_docs
22
22
  output << generate_actions_docs
23
- output << ""
24
- output << generate_stimulus_docs
25
23
 
26
24
  output.join("\n")
27
25
  end
@@ -60,6 +58,37 @@ module Tailmix
60
58
  output.join("\n")
61
59
  end
62
60
 
61
+ def generate_compound_variants_docs
62
+ output = []
63
+
64
+ compound_variants_by_element = @definition.elements.values.select do |el|
65
+ el.compound_variants.any?
66
+ end
67
+
68
+ if compound_variants_by_element.any?
69
+ output << ""
70
+ output << "Compound Variants:"
71
+ compound_variants_by_element.each do |element|
72
+ output << " - on element `:#{element.name}`:"
73
+ element.compound_variants.each do |cv|
74
+ conditions = cv[:on].map { |k, v| "#{k}: :#{v}" }.join(", ")
75
+ output << " - on: { #{conditions} }"
76
+
77
+ modifications = cv[:modifications]
78
+ modifications.class_groups.each do |group|
79
+ label = group[:options][:group] ? "(group: :#{group[:options][:group]})" : ""
80
+ output << " - classes #{label}: \"#{group[:classes].join(' ')}\""
81
+ end
82
+ output << " - data: #{modifications.data.inspect}" if modifications.data.any?
83
+ output << " - aria: #{modifications.aria.inspect}" if modifications.aria.any?
84
+ end
85
+ end
86
+ output << ""
87
+ end
88
+
89
+ output.join("\n")
90
+ end
91
+
63
92
  def generate_actions_docs
64
93
  output = []
65
94
  actions = @definition.actions
@@ -76,10 +105,6 @@ module Tailmix
76
105
  output.join("\n")
77
106
  end
78
107
 
79
- def generate_stimulus_docs
80
- @tools.stimulus.scaffold(show_docs: true)
81
- end
82
-
83
108
  def all_dimensions
84
109
  @_all_dimensions ||= @definition.elements.values.flat_map(&:dimensions).reduce({}, :merge)
85
110
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "stimulus_generator"
4
3
  require_relative "docs"
5
4
 
6
5
  module Tailmix
@@ -18,10 +17,6 @@ module Tailmix
18
17
  end
19
18
  alias_method :help, :docs
20
19
 
21
- def stimulus
22
- StimulusGenerator.new(@definition, @component_class.name)
23
- end
24
-
25
20
  def elements
26
21
  @definition.elements.values.map(&:name)
27
22
  end
data/lib/tailmix/dsl.rb CHANGED
@@ -1,15 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "definition/context_builder"
3
+ require_relative "definition/builders/component_builder"
4
4
  require_relative "definition/merger"
5
5
  require_relative "dev/tools"
6
6
 
7
7
  module Tailmix
8
- # The main DSL for defining component styles and behaviors.
9
- # This module is extended into any class that includes Tailmix.
10
8
  module DSL
11
9
  def tailmix(&block)
12
- child_context = Definition::ContextBuilder.new
10
+ child_context = Definition::Builders::ComponentBuilder.new(component_name: self)
13
11
  child_context.instance_eval(&block)
14
12
  child_definition = child_context.build_definition
15
13
 
@@ -32,4 +30,4 @@ module Tailmix
32
30
  Dev::Tools.new(self)
33
31
  end
34
32
  end
35
- end
33
+ end
@@ -6,12 +6,22 @@ module Tailmix
6
6
  Rails.application.config.assets.paths << Engine.root.join("app/javascript")
7
7
  end
8
8
 
9
- PRECOMPILE_ASSETS = %w[ index.js runner.js finder.js mutator.js stimulus_adapter.js ]
9
+ PRECOMPILE_ASSETS = %w[ runtime/action_dispatcher.js runtime/component.js runtime/updater.js runtime/index.js runtime/plugins.js ]
10
10
 
11
11
  initializer "tailmix.assets" do
12
12
  if Rails.application.config.respond_to?(:assets)
13
13
  Rails.application.config.assets.precompile += PRECOMPILE_ASSETS
14
14
  end
15
15
  end
16
+
17
+ initializer "tailmix.add_middleware" do |app|
18
+ app.middleware.use Tailmix::Middleware::RegistryCleaner
19
+ end
20
+
21
+ initializer "tailmix.helpers" do
22
+ ActiveSupport.on_load(:action_controller_base) do
23
+ helper Tailmix::ViewHelpers
24
+ end
25
+ end
16
26
  end
17
27
  end
@@ -3,16 +3,15 @@
3
3
  require "erb"
4
4
  require_relative "class_list"
5
5
  require_relative "data_map"
6
- require_relative "selector"
7
6
 
8
7
  module Tailmix
9
8
  module HTML
10
9
  class Attributes < Hash
11
- attr_reader :element_name, :variant_string
10
+ attr_reader :element_name
12
11
 
13
- def initialize(initial_hash = {}, element_name: nil, variant_string: "")
12
+ def initialize(initial_hash = {}, element_name: nil, context: nil)
14
13
  @element_name = element_name
15
- @variant_string = variant_string
14
+ @context = context
16
15
  super()
17
16
 
18
17
  attrs_to_merge = initial_hash.dup
@@ -24,24 +23,27 @@ module Tailmix
24
23
  self[:class] = ClassList.new(initial_classes)
25
24
  self[:data] = DataMap.new("data", initial_data || {})
26
25
  self[:aria] = DataMap.new("aria", initial_aria || {})
27
- self[:tailmix] = Selector.new(element_name, variant_string)
28
26
 
29
27
  merge!(attrs_to_merge)
30
28
  end
31
29
 
30
+
32
31
  def each(&block)
33
32
  to_h.each(&block)
34
33
  end
34
+ alias_method :each_pair, :each
35
35
 
36
36
  def to_h
37
- final_attrs = select { |k, _| !%i[class data aria tailmix].include?(k.to_sym) }
37
+ final_attrs = select { |k, _| !%i[class data aria].include?(k.to_sym) }
38
38
 
39
39
  class_string = self[:class].to_s
40
40
  final_attrs[:class] = class_string unless class_string.empty?
41
41
 
42
42
  final_attrs.merge!(self[:data].to_h)
43
43
  final_attrs.merge!(self[:aria].to_h)
44
- final_attrs.merge!(self[:tailmix].to_h)
44
+
45
+ final_attrs["data-tailmix-element"] = @element_name if @element_name
46
+ final_attrs["data-tailmix-id"] = @context.id if @context.id
45
47
 
46
48
  final_attrs
47
49
  end
@@ -67,10 +69,6 @@ module Tailmix
67
69
  data.stimulus
68
70
  end
69
71
 
70
- def tailmix
71
- self[:tailmix]
72
- end
73
-
74
72
  def toggle(class_names)
75
73
  classes.toggle(class_names)
76
74
  self
@@ -87,8 +85,20 @@ module Tailmix
87
85
  end
88
86
 
89
87
  def each_attribute(&block)
90
- [ classes: classes, data: data.to_h, aria: aria.to_h, tailmix: tailmix.to_h ].each(&block)
88
+ [ classes: classes, data: data.to_h, aria: aria.to_h ].each(&block)
89
+ end
90
+
91
+ def component
92
+ raise "No context available to build component root" unless @context
93
+
94
+ root_attrs = {
95
+ "data-tailmix-component" => @context.component_name,
96
+ "data-tailmix-state" => @context.state_payload,
97
+ }
98
+
99
+ self.class.new(self.to_h.merge(root_attrs), element_name: @element_name, context: @context)
91
100
  end
101
+ alias_method :root, :component
92
102
  end
93
103
  end
94
104
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Middleware
5
+ class RegistryCleaner
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ @app.call(env)
12
+ ensure
13
+ Tailmix::Registry.instance.clear!
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ require "singleton"
3
+ require "set"
4
+
5
+ module Tailmix
6
+ # A per-request registry to store unique component classes rendered
7
+ # during a request-response cycle.
8
+ class Registry
9
+ include Singleton
10
+
11
+ def initialize
12
+ @component_classes = Set.new
13
+ end
14
+
15
+ # Registers a component class.
16
+ # @param component_class [Class] The component class to register.
17
+ def register(component_class)
18
+ @component_classes.add(component_class)
19
+ end
20
+
21
+ # Gathers definitions from all registered classes.
22
+ # @return [Hash] A hash mapping component names to their definitions.
23
+ def definitions
24
+ @component_classes.each_with_object({}) do |klass, hash|
25
+ component_name = klass.name
26
+ if component_name && klass.respond_to?(:tailmix_definition)
27
+ hash[component_name] = klass.tailmix_definition.to_h
28
+ end
29
+ end
30
+ end
31
+
32
+ # Clears the registry. Must be called after each request.
33
+ def clear!
34
+ @component_classes.clear
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Runtime
5
+ # Proxy for convenient calling of actions (ui.action).
6
+ class ActionProxy
7
+ def initialize(context)
8
+ @context = context
9
+ end
10
+
11
+ def method_missing(method_name, *args, &block)
12
+ action_name = method_name.to_sym
13
+ action_def = @context.definition.actions[action_name]
14
+
15
+ unless action_def
16
+ raise NoMethodError, "undefined action `#{action_name}` for #{@context.component_name}"
17
+ end
18
+
19
+ # We return an object that can be called using .call.
20
+ # This allows you to write ui.action.save.call(payload)
21
+ ->(payload = {}) { @context.run_action(action_def, payload) }
22
+ end
23
+
24
+ def respond_to_missing?(method_name, include_private = false)
25
+ @context.definition.actions.key?(method_name.to_sym) || super
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Runtime
5
+ class AttributeBuilder
6
+ def initialize(element_def, state, context)
7
+ @element_def = element_def
8
+ @state = state
9
+ @context = context
10
+ end
11
+
12
+ def build
13
+ attributes = create_base_attributes
14
+
15
+ apply_dimensions(attributes)
16
+ apply_compound_variants(attributes)
17
+ apply_attribute_bindings(attributes)
18
+ apply_model_bindings(attributes)
19
+ apply_event_bindings(attributes)
20
+
21
+ attributes
22
+ end
23
+
24
+ private
25
+
26
+ def create_base_attributes
27
+ base_attrs = @element_def.default_attributes.merge(
28
+ class: @element_def.attributes.classes
29
+ )
30
+ HTML::Attributes.new(base_attrs, element_name: @element_def.name, context: @context)
31
+ end
32
+
33
+ # Applies classes and data/aria attributes from `dimension`.
34
+ def apply_dimensions(attributes)
35
+ @element_def.dimensions.each do |name, dim_def|
36
+ value = @state[name] || dim_def[:default]
37
+ next if value.nil?
38
+
39
+ variant_def = dim_def.fetch(:variants, {}).fetch(value, nil)
40
+
41
+ next unless variant_def
42
+ attributes.classes.add(variant_def.classes)
43
+ attributes.data.merge!(variant_def.data)
44
+ attributes.aria.merge!(variant_def.aria)
45
+ attributes.merge!(variant_def.attributes)
46
+ end
47
+ end
48
+
49
+ # Applies classes and data/aria attributes from `compound_variant`.
50
+ def apply_compound_variants(attributes)
51
+ @element_def.compound_variants.each do |cv|
52
+ next unless cv[:on].all? { |key, value| @state[key] == value }
53
+
54
+ modifications = cv[:modifications]
55
+ attributes.classes.add(modifications.classes)
56
+ attributes.data.merge!(modifications.data) if modifications.data
57
+ attributes.aria.merge!(modifications.aria) if modifications.aria
58
+ end
59
+ end
60
+
61
+ # Applies one-way attribute bindings (`bind :src, to: :url`).
62
+ def apply_attribute_bindings(attributes)
63
+ @element_def.attribute_bindings&.each do |attr_name, state_key_or_proc|
64
+ next if %i[text html].include?(attr_name)
65
+
66
+ value = if state_key_or_proc.is_a?(Proc)
67
+ state_key_or_proc.call(@state)
68
+ else
69
+ @state[state_key_or_proc]
70
+ end
71
+ attributes[attr_name] = value if value
72
+ end
73
+ end
74
+
75
+ # Applies two-way bindings (`model :value, to: :query`).
76
+ def apply_model_bindings(attributes)
77
+ @element_def.model_bindings&.each do |attr_name, binding_def|
78
+ state_key = binding_def[:state]
79
+ value = @state[state_key]
80
+ attributes[attr_name] = value if value
81
+
82
+ # We are adding data attributes that will "bring to life" the client-side JS.
83
+ attributes.data.add("tailmix-model-attr": attr_name)
84
+ attributes.data.add("tailmix-model-state": state_key)
85
+ attributes.data.add("tailmix-model-event": binding_def[:event])
86
+ attributes.data.add("tailmix-model-action": binding_def[:action]) if binding_def[:action]
87
+ end
88
+ end
89
+
90
+ # Applies event handlers (`on :click, :save`).
91
+ def apply_event_bindings(attributes)
92
+ return unless @element_def.event_bindings&.any?
93
+
94
+ action_string = @element_def.event_bindings.map { |b| "#{b[:event]}->#{b[:action]}" }.join(" ")
95
+ with_map = @element_def.event_bindings.map { |b| b[:with] }.compact.reduce({}, :merge)
96
+
97
+ attributes.data.add(tailmix_action: action_string)
98
+ attributes.data.add(tailmix_action_with: with_map.to_json) unless with_map.empty?
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Runtime
5
+ class AttributeCache
6
+ def initialize
7
+ @cache = {}
8
+ end
9
+
10
+ def get(element_name)
11
+ @cache[element_name.to_sym]
12
+ end
13
+
14
+ def set(element_name, attributes)
15
+ @cache[element_name.to_sym] = attributes
16
+ end
17
+
18
+ def clear!
19
+ @cache.clear
20
+ end
21
+ end
22
+ end
23
+ end