halitosis 0.1.0 → 0.3.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.
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module Identifiers
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # Rails-style identifier definition
13
+ #
14
+ # @param name [Symbol, String]
15
+ # @param options [nil, Hash]
16
+ #
17
+ # @return [Halitosis::Identifiers::Field]
18
+ #
19
+ def identifier(name, options = {}, &procedure)
20
+ if fields.for_type(Field).any?
21
+ raise InvalidField, "You can only define one identifier per serializer"
22
+ end
23
+
24
+ fields.add(Field.new(name, options, procedure))
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ # @return [Hash] the rendered hash with identifiers, if any
30
+ #
31
+ def render_with_context(context)
32
+ super.merge(identifiers(context))
33
+ end
34
+
35
+ # @return [Hash] identifiers from fields
36
+ #
37
+ def identifiers(context = build_context)
38
+ render_fields(Field, context) do |field, result|
39
+ result[field.name] = field.value(context)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ require "halitosis/identifiers/field"
@@ -25,7 +25,7 @@ module Halitosis
25
25
 
26
26
  # @return [nil, Hash]
27
27
  #
28
- def value(_instance)
28
+ def value(_context)
29
29
  hrefs = super
30
30
 
31
31
  attrs = options.fetch(:attrs, {})
@@ -19,9 +19,9 @@ module Halitosis
19
19
  module InstanceMethods
20
20
  # @return [Hash] the rendered hash with links, if any
21
21
  #
22
- def render
23
- if options.fetch(:include_links, true)
24
- decorate_render :links, super
22
+ def render_with_context(context)
23
+ if context.fetch(:include_links, true)
24
+ decorate_render :links, context, super
25
25
  else
26
26
  super
27
27
  end
@@ -29,9 +29,9 @@ module Halitosis
29
29
 
30
30
  # @return [Hash] links from fields
31
31
  #
32
- def links
33
- render_fields(Field.name) do |field, result|
34
- value = field.value(self)
32
+ def links(context = build_context)
33
+ render_fields(Field, context) do |field, result|
34
+ value = field.value(context)
35
35
 
36
36
  result[field.name] = value if value
37
37
  end
@@ -19,9 +19,9 @@ module Halitosis
19
19
  module InstanceMethods
20
20
  # @return [Hash] the rendered hash with meta, if any
21
21
  #
22
- def render
23
- if options.fetch(:include_meta, true)
24
- decorate_render :meta, super
22
+ def render_with_context(context)
23
+ if context.fetch(:include_meta, true)
24
+ decorate_render :meta, context, super
25
25
  else
26
26
  super
27
27
  end
@@ -29,11 +29,11 @@ module Halitosis
29
29
 
30
30
  # @return [Hash] meta from fields
31
31
  #
32
- def meta
33
- render_fields(Field.name) do |field, result|
34
- value = field.value(self)
32
+ def meta(context = build_context)
33
+ render_fields(Field, context) do |field, result|
34
+ value = field.value(context)
35
35
 
36
- result[field.name] = value if value
36
+ result[field.name] = value
37
37
  end
38
38
  end
39
39
  end
@@ -19,9 +19,9 @@ module Halitosis
19
19
  module InstanceMethods
20
20
  # @return [Hash] the rendered hash with permissions, if any
21
21
  #
22
- def render
23
- if options.fetch(:include_permissions, true)
24
- decorate_render :permissions, super
22
+ def render_with_context(context)
23
+ if context.fetch(:include_permissions, true)
24
+ decorate_render :permissions, context, super
25
25
  else
26
26
  super
27
27
  end
@@ -29,11 +29,11 @@ module Halitosis
29
29
 
30
30
  # @return [Hash] permissions from fields
31
31
  #
32
- def permissions
33
- render_fields(Field.name) do |field, result|
34
- value = field.value(self)
32
+ def permissions(context = build_context)
33
+ render_fields(Field, context) do |field, result|
34
+ value = field.value(context)
35
35
 
36
- result[field.name] = value if value
36
+ result[field.name] = value || false
37
37
  end
38
38
  end
39
39
  end
@@ -3,8 +3,13 @@ module Halitosis
3
3
  #
4
4
  class Railtie < ::Rails::Railtie
5
5
  module Renderable
6
+ def render_with_params(params)
7
+ render(include: params[:include])
8
+ end
9
+
6
10
  def render_in(view_context)
7
- view_context.render json: self
11
+ rendered = render_with_params(view_context.params)
12
+ view_context.render plain: rendered.to_json
8
13
  end
9
14
 
10
15
  def format
@@ -16,6 +21,10 @@ module Halitosis
16
21
  Halitosis.config.extensions << ::Rails.application.routes.url_helpers
17
22
  end
18
23
 
24
+ initializer "halitosis.error_response" do |app|
25
+ app.config.action_dispatch.rescue_responses[InvalidQueryParameter.name] ||= :bad_request
26
+ end
27
+
19
28
  initializer "halitosis.renderable" do |_app|
20
29
  Halitosis.config.extensions << Renderable
21
30
  end
@@ -21,10 +21,10 @@ module Halitosis
21
21
  #
22
22
  # @return [true, false]
23
23
  #
24
- def enabled?(instance)
24
+ def enabled?(context)
25
25
  return false unless super
26
26
 
27
- opts = instance.include_options
27
+ opts = context.include_options
28
28
 
29
29
  # Field name must appear in instance included option keys
30
30
  return false unless opts.include?(name.to_s)
@@ -24,34 +24,35 @@ module Halitosis
24
24
  module InstanceMethods
25
25
  # @return [Hash] the rendered hash with relationships resources, if any
26
26
  #
27
- def render
28
- decorate_render :relationships, super
27
+ def render_with_context(context)
28
+ decorate_render :relationships, context, super
29
29
  end
30
30
 
31
31
  # @return [Hash] hash of rendered resources to include
32
32
  #
33
- def relationships
34
- render_fields(Field.name) do |field, result|
35
- value = instance_eval(&field.procedure)
33
+ def relationships(context = build_context)
34
+ # Do not validation non-root collections (as they pass values directly to children)
35
+ validate_relationships!(context) unless collection?
36
36
 
37
- child = relationships_child(field.name.to_s, value)
37
+ render_fields(Field, context) do |field, result|
38
+ value = field.value(context)
38
39
 
39
- result[field.name] = child if child
40
+ result[field.name] = relationships_child(field.name.to_s, context, value)
40
41
  end
41
42
  end
42
43
 
43
44
  # @return [nil, Hash, Array<Hash>] either a single rendered child
44
45
  # serializer or an array of them
45
46
  #
46
- def relationships_child(key, value)
47
+ def relationships_child(key, context, value)
47
48
  return unless value
48
49
 
49
- opts = child_relationship_opts(key)
50
+ opts = child_relationship_opts(key, context)
50
51
 
51
52
  if value.is_a?(Array)
52
- value.map { |item| render_child(item, opts) }.compact
53
+ value.map { |item| render_child(item, context, opts) }.compact
53
54
  else
54
- render_child(value, opts)
55
+ render_child(value, context, opts)
55
56
  end
56
57
  end
57
58
 
@@ -59,8 +60,8 @@ module Halitosis
59
60
  #
60
61
  # @return [Hash]
61
62
  #
62
- def child_relationship_opts(key)
63
- opts = include_options.fetch(key, {})
63
+ def child_relationship_opts(key, context)
64
+ opts = context.include_options.fetch(key, {})
64
65
 
65
66
  # Turn { :report => 1 } into { :report => {} } for child
66
67
  opts = {} unless opts.is_a?(Hash)
@@ -68,10 +69,17 @@ module Halitosis
68
69
  opts
69
70
  end
70
71
 
71
- # @return [Hash] hash of options with top level string keys
72
- #
73
- def include_options
74
- @include_options ||= Halitosis::HashUtil.stringify_params(options.fetch(:include, {}))
72
+ private
73
+
74
+ def validate_relationships!(context)
75
+ opts = context.include_options.keys.map(&:to_s)
76
+
77
+ opts -= self.class.fields.for_type(Field).map { |field| field.name.to_s }
78
+
79
+ return if opts.none?
80
+
81
+ resource_label = [self.class.resource_type, "resource"].compact.join(" ")
82
+ raise Halitosis::InvalidQueryParameter.new("The #{resource_label} does not have a `#{opts.first}` relationship path.", "include")
75
83
  end
76
84
  end
77
85
  end
@@ -14,8 +14,6 @@ module Halitosis
14
14
  base.send :include, InstanceMethods
15
15
 
16
16
  base.send :attr_reader, :resource
17
-
18
- base.class.send :attr_accessor, :resource_name
19
17
  end
20
18
 
21
19
  module ClassMethods
@@ -24,22 +22,33 @@ module Halitosis
24
22
  # @return [Module] self
25
23
  #
26
24
  def define_resource(name)
27
- self.resource_name = name.to_s
25
+ self.resource_type = name.to_s
28
26
 
29
27
  alias_method name, :resource
30
28
  end
31
29
 
32
- # Override standard property field for resource-based serializers
30
+ # Override standard identifier field for resource-based serializers
33
31
  #
34
- # @param name [Symbol, String] name of the property
35
- # @param options [nil, Hash] property options for field
32
+ # @param name [Symbol, String] name of the identifier
33
+ # @param options [nil, Hash] identifier options for field
36
34
  #
37
- def property(name, options = {}, &procedure)
38
- super.tap do |field|
39
- unless field.procedure || field.options.key?(:value)
40
- field.procedure = proc { resource.send(name) }
41
- end
35
+ def identifier(name, options = {}, &procedure)
36
+ unless procedure || options.key?(:value)
37
+ procedure = proc { resource.public_send(name) }
42
38
  end
39
+ super(name, options, &procedure)
40
+ end
41
+
42
+ # Override standard attribute field for resource-based serializers
43
+ #
44
+ # @param name [Symbol, String] name of the attribute
45
+ # @param options [nil, Hash] attribute options for field
46
+ #
47
+ def attribute(name, options = {}, &procedure)
48
+ unless procedure || options.key?(:value)
49
+ procedure = proc { resource.public_send(name) }
50
+ end
51
+ super(name, options, &procedure)
43
52
  end
44
53
  end
45
54
 
@@ -53,6 +62,23 @@ module Halitosis
53
62
 
54
63
  super(**)
55
64
  end
65
+
66
+ # @return [Hash] the rendered hash with resource, as a hash
67
+ #
68
+ def render_with_context(context)
69
+ if (include_root = context.fetch(:include_root) { context.depth.zero? })
70
+ {root_name(include_root, self.class.resource_type) => super}
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def root_name(include_root, default)
79
+ return include_root.to_sym if include_root.is_a?(String) || include_root.is_a?(Symbol)
80
+ default.to_sym
81
+ end
56
82
  end
57
83
  end
58
84
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module RootLinks
5
+ class Field < Halitosis::Links::Field
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module RootLinks
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # @return [Halitosis::RootLinks::Field]
13
+ #
14
+ def root_link(name, **options, &procedure)
15
+ fields.add(Field.new(name, options, procedure))
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ # @return [Hash] the rendered hash with link, if any
21
+ #
22
+ def render(**)
23
+ super.tap do |result|
24
+ next unless options.fetch(:include_root) { true }
25
+
26
+ value = root_links
27
+ result[:_links] = result.fetch(:_links, {}).merge(value) if value.any?
28
+ end
29
+ end
30
+
31
+ # @return [Hash] link from fields
32
+ #
33
+ # @return [Hash] root_links from fields
34
+ #
35
+ def root_links(context = build_context)
36
+ render_fields(Field, context) do |field, result|
37
+ value = field.value(context)
38
+
39
+ result[field.name] = value if value
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ require "halitosis/root_links/field"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module RootMeta
5
+ class Field < Halitosis::Meta::Field
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module RootMeta
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # @return [Halitosis::RootMeta::Field]
13
+ #
14
+ def root_meta(name, **options, &procedure)
15
+ fields.add(Field.new(name, options, procedure))
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ # @return [Hash] the rendered hash with meta, if any
21
+ #
22
+ def render(**)
23
+ super.tap do |result|
24
+ next unless options.fetch(:include_root) { true }
25
+
26
+ value = root_meta
27
+ result[:_meta] = result.fetch(:_meta, {}).merge(value) if value.any?
28
+ end
29
+ end
30
+
31
+ # @return [Hash] meta from fields
32
+ #
33
+ def root_meta(context = build_context)
34
+ render_fields(Field, context) do |field, result|
35
+ value = field.value(context)
36
+
37
+ result[field.name] = value
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ require "halitosis/root_meta/field"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module RootPermissions
5
+ class Field < Halitosis::Permissions::Field
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module RootPermissions
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # @return [Halitosis::RootPermission::Field]
13
+ #
14
+ def root_permission(name, **options, &procedure)
15
+ fields.add(Field.new(name, options, procedure))
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ # @return [Hash] the rendered hash with permissions, if any
21
+ #
22
+ def render(**)
23
+ super.tap do |result|
24
+ value = root_permissions
25
+ result[:_permissions] = result.fetch(:_permissions, {}).merge(value) if value.any?
26
+ end
27
+ end
28
+
29
+ # @return [Hash] permissions from fields
30
+ #
31
+ def root_permissions(context = build_context)
32
+ render_fields(Field, context) do |field, result|
33
+ next unless options.fetch(:include_root) { true }
34
+
35
+ value = field.value(context)
36
+ result[field.name] = value || false
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ require "halitosis/root_permissions/field"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Halitosis
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/halitosis.rb CHANGED
@@ -15,9 +15,9 @@ require "json"
15
15
  #
16
16
  # resource :article
17
17
  #
18
- # property :id, required: true
18
+ # attribute :id, required: true
19
19
  #
20
- # property :title
20
+ # attribute :title
21
21
  #
22
22
  # link :self, -> { article_path(article) }
23
23
  #
@@ -33,19 +33,31 @@ module Halitosis
33
33
  base.extend ClassMethods
34
34
 
35
35
  base.include Base
36
+ base.include Attributes
36
37
 
37
38
  config.extensions.each { |extension| base.send :include, extension }
38
39
  end
39
40
 
40
41
  module ClassMethods
41
42
  def resource(name)
43
+ include Identifiers
44
+ include Links
45
+ include Meta
46
+ include Permissions
47
+ include Relationships
42
48
  include Halitosis::Resource
49
+ include RootLinks
50
+ include RootMeta
51
+ include RootPermissions
43
52
 
44
53
  define_resource(name)
45
54
  end
46
55
 
47
56
  def collection(name, ...)
48
57
  include Halitosis::Collection
58
+ include RootLinks
59
+ include RootMeta
60
+ include RootPermissions
49
61
 
50
62
  define_collection(name, ...)
51
63
  end
@@ -68,15 +80,20 @@ module Halitosis
68
80
  end
69
81
  end
70
82
 
83
+ require_relative "halitosis/context"
71
84
  require_relative "halitosis/base"
72
85
  require_relative "halitosis/errors"
73
86
  require_relative "halitosis/field"
74
87
  require_relative "halitosis/fields"
75
- require_relative "halitosis/properties"
88
+ require_relative "halitosis/attributes"
89
+ require_relative "halitosis/identifiers"
76
90
  require_relative "halitosis/links"
77
91
  require_relative "halitosis/meta"
78
92
  require_relative "halitosis/permissions"
79
93
  require_relative "halitosis/relationships"
94
+ require_relative "halitosis/root_links"
95
+ require_relative "halitosis/root_meta"
96
+ require_relative "halitosis/root_permissions"
80
97
  require_relative "halitosis/resource"
81
98
  require_relative "halitosis/collection"
82
99
  require_relative "halitosis/hash_util"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: halitosis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Morrall
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-30 00:00:00.000000000 Z
11
+ date: 2024-12-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Provides an interface for serializing resources as JSON with HAL-like
14
14
  links and relationships.
@@ -26,26 +26,35 @@ files:
26
26
  - README.md
27
27
  - Rakefile
28
28
  - lib/halitosis.rb
29
+ - lib/halitosis/attributes.rb
30
+ - lib/halitosis/attributes/field.rb
29
31
  - lib/halitosis/base.rb
30
32
  - lib/halitosis/collection.rb
31
33
  - lib/halitosis/collection/field.rb
32
34
  - lib/halitosis/configuration.rb
35
+ - lib/halitosis/context.rb
33
36
  - lib/halitosis/errors.rb
34
37
  - lib/halitosis/field.rb
35
38
  - lib/halitosis/fields.rb
36
39
  - lib/halitosis/hash_util.rb
40
+ - lib/halitosis/identifiers.rb
41
+ - lib/halitosis/identifiers/field.rb
37
42
  - lib/halitosis/links.rb
38
43
  - lib/halitosis/links/field.rb
39
44
  - lib/halitosis/meta.rb
40
45
  - lib/halitosis/meta/field.rb
41
46
  - lib/halitosis/permissions.rb
42
47
  - lib/halitosis/permissions/field.rb
43
- - lib/halitosis/properties.rb
44
- - lib/halitosis/properties/field.rb
45
48
  - lib/halitosis/railtie.rb
46
49
  - lib/halitosis/relationships.rb
47
50
  - lib/halitosis/relationships/field.rb
48
51
  - lib/halitosis/resource.rb
52
+ - lib/halitosis/root_links.rb
53
+ - lib/halitosis/root_links/field.rb
54
+ - lib/halitosis/root_meta.rb
55
+ - lib/halitosis/root_meta/field.rb
56
+ - lib/halitosis/root_permissions.rb
57
+ - lib/halitosis/root_permissions/field.rb
49
58
  - lib/halitosis/version.rb
50
59
  - sig/halitosis.rbs
51
60
  homepage: https://github.com/bmorrall/halitosis
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Halitosis
4
- module Properties
5
- def self.included(base)
6
- base.extend ClassMethods
7
-
8
- base.send :include, InstanceMethods
9
- end
10
-
11
- module ClassMethods
12
- # @param name [Symbol, String]
13
- # @param options [nil, Hash]
14
- #
15
- # @return [Halitosis::Properties::Field]
16
- #
17
- def property(name, options = {}, &procedure)
18
- fields.add(Field.new(name, options, procedure))
19
- end
20
- end
21
-
22
- module InstanceMethods
23
- # @return [Hash] the rendered hash with properties, if any
24
- #
25
- def render
26
- super.merge(properties)
27
- end
28
-
29
- # @return [Hash] properties from fields
30
- #
31
- def properties
32
- render_fields(Field.name) do |field, result|
33
- result[field.name] = field.value(self)
34
- end
35
- end
36
- end
37
- end
38
- end
39
-
40
- require "halitosis/properties/field"