praxis-blueprints 3.0 → 3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,28 +1,33 @@
1
+ # frozen_string_literal: true
1
2
  module Praxis
2
-
3
3
  class CollectionView < View
4
- def initialize(name, schema, member_view=nil)
5
- super(name,schema)
4
+ def initialize(name, schema, member_view = nil)
5
+ super(name, schema)
6
+
7
+ @_lazy_view = member_view if member_view
8
+ end
6
9
 
7
- if member_view
8
- @contents = member_view.contents.clone
10
+ def contents
11
+ if @_lazy_view
12
+ @contents = @_lazy_view.contents.clone
13
+ @_lazy_view = nil
9
14
  end
15
+ super
10
16
  end
11
17
 
12
- def example(context=Attributor::DEFAULT_ROOT_CONTEXT)
13
- collection = 3.times.collect do |i|
18
+ def example(context = Attributor::DEFAULT_ROOT_CONTEXT)
19
+ collection = Array.new(3) do |i|
14
20
  subcontext = context + ["at(#{i})"]
15
- self.schema.example(subcontext)
21
+ schema.example(subcontext)
16
22
  end
17
23
  opts = {}
18
24
  opts[:context] = context if context
19
25
 
20
- self.render(collection, **opts)
26
+ render(collection, **opts)
21
27
  end
22
28
 
23
29
  def describe
24
30
  super.merge(type: :collection)
25
31
  end
26
-
27
32
  end
28
33
  end
@@ -1,23 +1,27 @@
1
+ # frozen_string_literal: true
1
2
  module Praxis
2
3
  class ConfigHash < BasicObject
3
-
4
4
  attr_reader :hash
5
5
 
6
- def self.from(hash={},&block)
7
- self.new(hash,&block)
6
+ def self.from(hash = {}, &block)
7
+ new(hash, &block)
8
8
  end
9
9
 
10
- def initialize(hash={},&block)
10
+ def initialize(hash = {}, &block)
11
11
  @hash = hash
12
12
  @block = block
13
13
  end
14
14
 
15
15
  def to_hash
16
- self.instance_eval(&@block)
16
+ instance_eval(&@block)
17
17
  @hash
18
18
  end
19
19
 
20
- def method_missing(name, value, *rest, &block)
20
+ def respond_to_missing?(_method_name, _include_private = false)
21
+ true
22
+ end
23
+
24
+ def method_missing(name, value, *rest, &block) # rubocop:disable Style/MethodMissing
21
25
  if (existing = @hash[name])
22
26
  if block
23
27
  existing << [value, block]
@@ -28,13 +32,12 @@ module Praxis
28
32
  end
29
33
  end
30
34
  else
31
- if rest.any?
32
- @hash[name] = [value] + rest
33
- else
34
- @hash[name] = value
35
- end
35
+ @hash[name] = if rest.any?
36
+ [value] + rest
37
+ else
38
+ value
39
+ end
36
40
  end
37
41
  end
38
-
39
42
  end
40
43
  end
@@ -1,15 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module Praxis
2
3
  class FieldExpander
3
- class CircularExpansionError < StandardError
4
- attr_reader :stack
5
- def initialize(message, stack=[])
6
- super(message)
7
- @stack = stack
8
- end
9
- end
10
-
11
- def self.expand(object, fields=true)
12
- self.new.expand(object,fields)
4
+ def self.expand(object, fields = true)
5
+ new.expand(object, fields)
13
6
  end
14
7
 
15
8
  attr_reader :stack
@@ -19,86 +12,105 @@ module Praxis
19
12
  @stack = Hash.new do |hash, key|
20
13
  hash[key] = Set.new
21
14
  end
22
- @history = Hash.new do |hash,key|
23
- hash[key] = Hash.new
15
+ @history = Hash.new do |hash, key|
16
+ hash[key] = {}
24
17
  end
25
18
  end
26
19
 
27
- def expand(object, fields=true)
20
+ def expand(object, fields = true)
28
21
  if stack[object].include? fields
29
- raise CircularExpansionError, "Circular expansion detected for object #{object.inspect} with fields #{fields.inspect}"
22
+ return history[object][fields] if history[object].include? fields
23
+ # We should probably never get here, since we should have a record
24
+ # of the history of an expansion if we're trying to redo it,
25
+ # but we should also be conservative and raise here just in case.
26
+ raise "Circular expansion detected for object #{object.inspect} with fields #{fields.inspect}"
30
27
  else
31
28
  stack[object] << fields
32
29
  end
33
30
 
34
- if object.kind_of?(Praxis::View)
35
- self.expand_view(object, fields)
36
- elsif object.kind_of? Attributor::Attribute
37
- self.expand_type(object.type, fields)
38
- else
39
- self.expand_type(object,fields)
40
- end
41
- rescue CircularExpansionError => e
42
- e.stack.unshift [object,fields]
43
- raise
31
+ result = if object.is_a?(Praxis::View)
32
+ expand_view(object, fields)
33
+ elsif object.is_a? Attributor::Attribute
34
+ expand_type(object.type, fields)
35
+ else
36
+ expand_type(object, fields)
37
+ end
38
+
39
+ result
44
40
  ensure
45
41
  stack[object].delete fields
46
42
  end
47
43
 
48
44
  def expand_fields(attributes, fields)
49
- raise ArgumentError, "expand_fields must be given a block" unless block_given?
45
+ raise ArgumentError, 'expand_fields must be given a block' unless block_given?
50
46
 
51
47
  unless fields == true
52
- attributes = attributes.select do |k,v|
48
+ attributes = attributes.select do |k, _v|
53
49
  fields.key?(k)
54
50
  end
55
51
  end
56
52
 
57
53
  attributes.each_with_object({}) do |(name, dumpable), hash|
58
54
  sub_fields = case fields
59
- when true
60
- true
61
- when Hash
62
- fields[name] || true
63
- end
64
- hash[name] = yield(dumpable,sub_fields)
55
+ when true
56
+ true
57
+ when Hash
58
+ fields[name] || true
59
+ end
60
+ hash[name] = yield(dumpable, sub_fields)
65
61
  end
66
62
  end
67
63
 
64
+ def expand_view(object, fields = true)
65
+ history[object][fields] = if object.is_a?(Praxis::CollectionView)
66
+ []
67
+ else
68
+ {}
69
+ end
68
70
 
69
- def expand_view(object,fields=true)
70
71
  result = expand_fields(object.contents, fields) do |dumpable, sub_fields|
71
- self.expand(dumpable, sub_fields)
72
+ expand(dumpable, sub_fields)
72
73
  end
73
74
 
74
- return [result] if object.kind_of?(Praxis::CollectionView)
75
- result
75
+ if object.is_a?(Praxis::CollectionView)
76
+ history[object][fields] << result
77
+ else
78
+ history[object][fields].merge!(result)
79
+ end
80
+ history[object][fields]
76
81
  end
77
82
 
78
-
79
- def expand_type(object,fields=true)
83
+ def expand_type(object, fields = true)
80
84
  unless object.respond_to?(:attributes)
81
85
  if object.respond_to?(:member_attribute)
82
- fields = fields[0] if fields.kind_of? Array
83
- return [self.expand(object.member_attribute.type, fields)]
86
+ return expand_with_member_attribute(object, fields)
84
87
  else
85
88
  return true
86
89
  end
87
90
  end
88
91
 
89
92
  # just include the full thing if it has no attributes
90
- if object.attributes.empty?
91
- return true
92
- end
93
+ return true if object.attributes.empty?
93
94
 
94
- if history[object].include? fields
95
- return history[object][fields]
96
- end
95
+ return history[object][fields] if history[object].include? fields
97
96
 
98
- history[object][fields] = expand_fields(object.attributes, fields) do |dumpable, sub_fields|
99
- self.expand(dumpable.type, sub_fields)
97
+ history[object][fields] = {}
98
+ result = expand_fields(object.attributes, fields) do |dumpable, sub_fields|
99
+ expand(dumpable.type, sub_fields)
100
100
  end
101
+ history[object][fields].merge!(result)
101
102
  end
102
103
 
104
+ def expand_with_member_attribute(object, fields = true)
105
+ return history[object][fields] if history[object].include? fields
106
+ history[object][fields] = []
107
+
108
+ new_fields = fields.is_a?(Array) ? fields[0] : fields
109
+
110
+ result = [expand(object.member_attribute.type, new_fields)]
111
+ history[object][fields].concat(result)
112
+
113
+ result
114
+ end
103
115
  end
104
116
  end
@@ -1,7 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module Praxis
2
3
  module Finalizable
3
-
4
-
5
4
  def self.extended(klass)
6
5
  klass.module_eval do
7
6
  @finalizable = Set.new
@@ -26,13 +25,10 @@ module Praxis
26
25
  @finalized = true
27
26
  end
28
27
 
29
- def finalize!
30
- self.finalizable.reject(&:finalized?).each do |klass|
31
- klass._finalize!
32
- end
28
+ def finalize!
29
+ finalizable.reject(&:finalized?).each(&:_finalize!)
33
30
 
34
- self.finalize! unless self.finalizable.all?(&:finalized?)
31
+ finalize! unless finalizable.all?(&:finalized?)
35
32
  end
36
-
37
33
  end
38
34
  end
@@ -1,11 +1,27 @@
1
+ # frozen_string_literal: true
1
2
  module Praxis
2
3
  class Renderer
3
4
  attr_reader :include_nil
4
5
  attr_reader :cache
5
6
 
7
+ class CircularRenderingError < StandardError
8
+ attr_reader :object
9
+ attr_reader :context
10
+
11
+ def initialize(object, context)
12
+ @object = object
13
+ @context = context
14
+
15
+ first = Attributor.humanize_context(context[0..10])
16
+ last = Attributor.humanize_context(context[-5..-1])
17
+ pretty_context = "#{first}...#{last}"
18
+ super("SystemStackError in rendering #{object.class} with context: #{pretty_context}")
19
+ end
20
+ end
21
+
6
22
  def initialize(include_nil: false)
7
- @cache = Hash.new do |hash,key|
8
- hash[key] = Hash.new
23
+ @cache = Hash.new do |hash, key|
24
+ hash[key] = {}
9
25
  end
10
26
 
11
27
  @include_nil = include_nil
@@ -15,30 +31,39 @@ module Praxis
15
31
  #
16
32
  # @param [Object] object the object to render
17
33
  # @param [Hash] fields the set of fields, as from FieldExpander, to apply to each member of the collection.
18
- def render_collection(collection, member_fields, view=nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
19
- render(collection,[member_fields], view, context: context)
34
+ def render_collection(collection, member_fields, view = nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
35
+ render(collection, [member_fields], view, context: context)
20
36
  end
21
37
 
22
38
  # Renders an object using a given list of fields.
23
39
  #
24
40
  # @param [Object] object the object to render
25
41
  # @param [Hash] fields the correct set of fields, as from FieldExpander
26
- def render(object, fields, view=nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
27
- if fields.kind_of? Array
42
+ def render(object, fields, view = nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
43
+ if fields.is_a? Array
28
44
  sub_fields = fields[0]
29
45
  object.each_with_index.collect do |sub_object, i|
30
46
  sub_context = context + ["at(#{i})"]
31
47
  render(sub_object, sub_fields, view, context: sub_context)
32
48
  end
33
- elsif object.kind_of? Praxis::Blueprint
34
- @cache[object.object_id][fields.object_id] ||= _render(object,fields, view, context: context)
49
+ elsif object.is_a? Praxis::Blueprint
50
+ @cache[object._cache_key][fields] ||= _render(object, fields, view, context: context)
35
51
  else
36
- _render(object,fields, view, context: context)
52
+ _render(object, fields, view, context: context)
37
53
  end
54
+ rescue SystemStackError
55
+ raise CircularRenderingError.new(object, context)
38
56
  end
39
57
 
40
- def _render(object, fields, view=nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
41
- return object if fields == true
58
+ def _render(object, fields, view = nil, context: Attributor::DEFAULT_ROOT_CONTEXT)
59
+ if fields == true
60
+ return case object
61
+ when Attributor::Dumpable
62
+ object.dump
63
+ else
64
+ object
65
+ end
66
+ end
42
67
 
43
68
  notification_payload = {
44
69
  blueprint: object,
@@ -46,26 +71,32 @@ module Praxis
46
71
  view: view
47
72
  }
48
73
 
49
- ActiveSupport::Notifications.instrument 'praxis.blueprint.render'.freeze, notification_payload do
50
- fields.each_with_object(Hash.new) do |(key, subfields), hash|
74
+ ActiveSupport::Notifications.instrument 'praxis.blueprint.render', notification_payload do
75
+ fields.each_with_object({}) do |(key, subfields), hash|
51
76
  begin
52
77
  value = object._get_attr(key)
53
78
  rescue => e
54
- raise Attributor::DumpError, context: context, name: key, type: object.class, original_exception: e
79
+ raise Attributor::DumpError.new(context: context, name: key, type: object.class, original_exception: e)
55
80
  end
56
81
 
57
- next if value.nil? && !self.include_nil
82
+ if value.nil?
83
+ hash[key] = nil if self.include_nil
84
+ next
85
+ end
58
86
 
59
87
  if subfields == true
60
- hash[key] = value
88
+ hash[key] = case value
89
+ when Attributor::Dumpable
90
+ value.dump
91
+ else
92
+ value
93
+ end
61
94
  else
62
95
  new_context = context + [key]
63
- hash[key] = self.render(value, subfields, context: new_context)
96
+ hash[key] = render(value, subfields, context: new_context)
64
97
  end
65
-
66
98
  end
67
99
  end
68
100
  end
69
-
70
101
  end
71
102
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Praxis
2
- BLUEPRINTS_VERSION = "3.0"
3
+ BLUEPRINTS_VERSION = '3.5'
3
4
  end
@@ -1,5 +1,5 @@
1
+ # frozen_string_literal: true
1
2
  module Praxis
2
-
3
3
  class View
4
4
  attr_reader :schema
5
5
  attr_reader :contents
@@ -17,7 +17,7 @@ module Praxis
17
17
 
18
18
  def contents
19
19
  if @block
20
- self.instance_eval(&@block)
20
+ instance_eval(&@block)
21
21
  @block = nil
22
22
  end
23
23
 
@@ -26,32 +26,29 @@ module Praxis
26
26
 
27
27
  def expanded_fields
28
28
  @expanded_fields ||= begin
29
- self.contents # force evaluation of the contents
29
+ contents # force evaluation of the contents
30
30
  FieldExpander.expand(self)
31
31
  end
32
32
  end
33
33
 
34
34
  def render(object, context: Attributor::DEFAULT_ROOT_CONTEXT, renderer: Renderer.new)
35
- renderer.render(object, self.expanded_fields, context: context)
35
+ renderer.render(object, expanded_fields, context: context)
36
36
  end
37
37
 
38
- alias_method :to_hash, :render # Why did we need this again?
39
-
40
-
41
38
  def attribute(name, **opts, &block)
42
- raise AttributorException, "Attribute names must be symbols, got: #{name.inspect}" unless name.kind_of? ::Symbol
39
+ raise AttributorException, "Attribute names must be symbols, got: #{name.inspect}" unless name.is_a? ::Symbol
43
40
 
44
- attribute = self.schema.attributes.fetch(name) do
45
- raise "Displaying :#{name} is not allowed in view :#{self.name} of #{self.schema}. This attribute does not exist in the mediatype"
41
+ attribute = schema.attributes.fetch(name) do
42
+ raise "Displaying :#{name} is not allowed in view :#{self.name} of #{schema}. This attribute does not exist in the mediatype"
46
43
  end
47
44
 
48
45
  if block_given?
49
46
  type = attribute.type
50
47
  @contents[name] = if type < Attributor::Collection
51
- CollectionView.new(name, type.member_attribute.type, &block)
52
- else
53
- View.new(name, attribute, &block)
54
- end
48
+ CollectionView.new(name, type.member_attribute.type, &block)
49
+ else
50
+ View.new(name, attribute, &block)
51
+ end
55
52
  else
56
53
  type = attribute.type
57
54
  if type < Attributor::Collection
@@ -59,38 +56,36 @@ module Praxis
59
56
  type = type.member_attribute.type
60
57
  end
61
58
 
62
-
63
59
  if type < Praxis::Blueprint
64
60
  view_name = opts[:view] || :default
65
61
  view = type.views.fetch(view_name) do
66
62
  raise "view with name '#{view_name.inspect}' is not defined in #{type}"
67
63
  end
68
- if is_collection
69
- @contents[name] = Praxis::CollectionView.new(view_name, type, view)
70
- else
71
- @contents[name] = view
72
- end
64
+ @contents[name] = if is_collection
65
+ Praxis::CollectionView.new(view_name, type, view)
66
+ else
67
+ view
68
+ end
73
69
  else
74
- @contents[name] = attribute #, opts]
70
+ @contents[name] = attribute # , opts]
75
71
  end
76
72
  end
77
-
78
73
  end
79
74
 
80
- def example(context=Attributor::DEFAULT_ROOT_CONTEXT)
81
- object = self.schema.example(context)
75
+ def example(context = Attributor::DEFAULT_ROOT_CONTEXT)
76
+ object = schema.example(context)
82
77
  opts = {}
83
78
  opts[:context] = context if context
84
- self.render(object, opts)
79
+ render(object, **opts)
85
80
  end
86
81
 
87
82
  def describe
88
83
  # TODO: for now we are just return the first level keys
89
84
  view_attributes = {}
90
85
 
91
- self.contents.each do |k,dumpable|
86
+ contents.each do |k, dumpable|
92
87
  inner_desc = {}
93
- if dumpable.kind_of?(Praxis::View)
88
+ if dumpable.is_a?(Praxis::View)
94
89
  inner_desc[:view] = dumpable.name if dumpable.name
95
90
  end
96
91
  view_attributes[k] = inner_desc
@@ -98,7 +93,5 @@ module Praxis
98
93
 
99
94
  { attributes: view_attributes, type: :standard }
100
95
  end
101
-
102
-
103
96
  end
104
97
  end