hal_presenter 0.4.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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +1 -0
- data.tar.gz.sig +0 -0
- data/lib/hal_presenter.rb +33 -0
- data/lib/hal_presenter/attributes.rb +27 -0
- data/lib/hal_presenter/collection.rb +42 -0
- data/lib/hal_presenter/curies.rb +30 -0
- data/lib/hal_presenter/deserializer.rb +91 -0
- data/lib/hal_presenter/embedded.rb +36 -0
- data/lib/hal_presenter/links.rb +54 -0
- data/lib/hal_presenter/model.rb +35 -0
- data/lib/hal_presenter/pagination.rb +126 -0
- data/lib/hal_presenter/policy.rb +22 -0
- data/lib/hal_presenter/policy/dsl.rb +112 -0
- data/lib/hal_presenter/property.rb +57 -0
- data/lib/hal_presenter/serialize_hooks.rb +41 -0
- data/lib/hal_presenter/serializer.rb +162 -0
- metadata +202 -0
- metadata.gz.sig +3 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9322632b515fd679f3bde7bcd8f0c3725330a66f3e4d8439f3223c3bc70f2e4e
|
4
|
+
data.tar.gz: 56e87ad4268c7bf247d9bf593fb0c6c91358a2d0d61a39824085075c4cbd9944
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7c1663fc6ba1427c4a30162892dd3ccc4ae551b299990657d3cbac06566d32483112a77527baab2c05316c57bc2ac65e3b5cc4ee91ddfb8317fd8b0afaf4da51
|
7
|
+
data.tar.gz: f99e735e37406ec5c7e370e9cc08fa74babbd135c6ceb60160c980e5a132c8c70d1412c7e0b585c868f6eece599bd2e88220ef2be26e8671e08b5236472c3200
|
checksums.yaml.gz.sig
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
��?t�=����rF5���=�<��cя�H�s1U��ϯ���Kcb��qy����ⵃ�Y����п��Me�G'U��;W�I������yBi�:&��y��ݠc�v� p��5�Ɗ���'scL��/ś��]���Y�ᓵ/ҭ��S�Z�L�����&�)����1��d �2N��`y{�<)u��n�E*��n�C��lP�!��`��n�EԲ�;��8=����qΡ$�OB�fW��e �x�
|
data.tar.gz.sig
ADDED
Binary file
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'hal_presenter/model'
|
2
|
+
require 'hal_presenter/policy'
|
3
|
+
require 'hal_presenter/policy/dsl'
|
4
|
+
require 'hal_presenter/attributes'
|
5
|
+
require 'hal_presenter/links'
|
6
|
+
require 'hal_presenter/embedded'
|
7
|
+
require 'hal_presenter/curies'
|
8
|
+
require 'hal_presenter/serializer'
|
9
|
+
require 'hal_presenter/deserializer'
|
10
|
+
require 'hal_presenter/collection'
|
11
|
+
require 'hal_presenter/serialize_hooks'
|
12
|
+
|
13
|
+
module HALPresenter
|
14
|
+
include HALPresenter::Attributes
|
15
|
+
include HALPresenter::Links
|
16
|
+
include HALPresenter::Curies
|
17
|
+
include HALPresenter::Embedded
|
18
|
+
include HALPresenter::Collection
|
19
|
+
include HALPresenter::SerializeHooks
|
20
|
+
include HALPresenter::Model
|
21
|
+
include HALPresenter::Serializer
|
22
|
+
include HALPresenter::Deserializer
|
23
|
+
include HALPresenter::Policy
|
24
|
+
end
|
25
|
+
|
26
|
+
# Keeping this module for backward compatibility!
|
27
|
+
module HALDecorator
|
28
|
+
include HALPresenter
|
29
|
+
|
30
|
+
def self.method_missing(m, *args, &block)
|
31
|
+
HALPresenter.send(m, *args, &block)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'hal_presenter/property'
|
2
|
+
|
3
|
+
module HALPresenter
|
4
|
+
module Attributes
|
5
|
+
def attribute(*args, &block)
|
6
|
+
@_attributes ||= init_attributes
|
7
|
+
@_attributes = @_attributes.reject { |attr| attr.name == args.first }
|
8
|
+
@_attributes << Property.new(*args, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def attributes
|
14
|
+
@_attributes ||= init_attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def init_attributes
|
20
|
+
return [] unless is_a? Class
|
21
|
+
return [] unless superclass.respond_to?(:attributes, true)
|
22
|
+
superclass.attributes.each do |attr|
|
23
|
+
attr.change_scope(self)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'hal_presenter/property'
|
2
|
+
|
3
|
+
module HALPresenter
|
4
|
+
module Collection
|
5
|
+
|
6
|
+
class CollectionParameters
|
7
|
+
include Attributes
|
8
|
+
include Links
|
9
|
+
include Curies
|
10
|
+
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
def initialize(name, &block)
|
14
|
+
@name = name
|
15
|
+
instance_exec(&block) if block_given?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def collection(of:, &block)
|
20
|
+
@_parameters = CollectionParameters.new(of, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def collection_parameters
|
26
|
+
@_parameters ||= init_collection_params
|
27
|
+
end
|
28
|
+
|
29
|
+
def can_serialize_collection?
|
30
|
+
!collection_parameters.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def init_collection_params
|
36
|
+
return unless is_a? Class
|
37
|
+
if superclass.respond_to?(:collection_parameters, true)
|
38
|
+
superclass.collection_parameters
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'hal_presenter/property'
|
2
|
+
|
3
|
+
module HALPresenter
|
4
|
+
module Curies
|
5
|
+
def curie(rel, value = nil, &block)
|
6
|
+
if value.nil? && !block_given?
|
7
|
+
raise 'curie must be called with non nil value or be given a block'
|
8
|
+
end
|
9
|
+
@_curies ||= init_curies
|
10
|
+
@_curies = @_curies.reject { |curie| curie.name == rel }
|
11
|
+
@_curies << Property.new(rel, value, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def curies
|
17
|
+
@_curies ||= init_curies
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def init_curies
|
23
|
+
return [] unless is_a? Class
|
24
|
+
return [] unless superclass.respond_to?(:curies, true)
|
25
|
+
superclass.curies.each do |curie|
|
26
|
+
curie.change_scope(self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module HALPresenter
|
4
|
+
|
5
|
+
def self.from_hal(presenter, payload, resource = nil)
|
6
|
+
presenter.from_hal(payload, resource)
|
7
|
+
end
|
8
|
+
|
9
|
+
module Deserializer
|
10
|
+
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
def from_hal(payload, resource = nil)
|
14
|
+
return if payload.nil? || payload.empty?
|
15
|
+
hash = JSON.parse(payload)
|
16
|
+
from_hash(hash, resource)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def from_hash(hash, resource)
|
22
|
+
as_collection = deserialize_as_collection?(hash)
|
23
|
+
|
24
|
+
if resource.nil?
|
25
|
+
model = HALPresenter.lookup_model self
|
26
|
+
raise Error, "No model for #{self.class}" unless model
|
27
|
+
resource = as_collection ? [] : model.new
|
28
|
+
elsif as_collection
|
29
|
+
resource.clear
|
30
|
+
end
|
31
|
+
|
32
|
+
if as_collection
|
33
|
+
deserialize_collection(hash, resource)
|
34
|
+
else
|
35
|
+
deserialize_attributes(hash, resource)
|
36
|
+
deserialize_embedded(hash, resource)
|
37
|
+
end
|
38
|
+
resource
|
39
|
+
end
|
40
|
+
|
41
|
+
def deserialize_attributes(hash, resource)
|
42
|
+
attributes.each do |attribute|
|
43
|
+
setter_method = setter_method_name(attribute.name)
|
44
|
+
next unless resource.respond_to? setter_method
|
45
|
+
resource.public_send(setter_method, hash[attribute.name.to_s])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def deserialize_embedded(hash, resource)
|
50
|
+
embedded.each do |embed|
|
51
|
+
setter_method = setter_method_name(embed.name) or next
|
52
|
+
next unless resource.respond_to? setter_method
|
53
|
+
presenter = embed.presenter_class or next
|
54
|
+
|
55
|
+
embedded_hash = hash.dig('_embedded', embed.name.to_s)
|
56
|
+
next unless embedded_hash&.any?
|
57
|
+
|
58
|
+
embedded_resource = resource.public_send(embed.name)
|
59
|
+
embedded_resource =
|
60
|
+
if embedded_hash.is_a? Array
|
61
|
+
embedded_hash.map { |h| presenter.from_hash(h, embedded_resource) }
|
62
|
+
else
|
63
|
+
presenter.from_hash(embedded_hash, embedded_resource)
|
64
|
+
end
|
65
|
+
resource.public_send(setter_method, embedded_resource)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def deserialize_collection(hash, resource)
|
70
|
+
hash['_embedded'][collection_parameters.name].each do |resource_hash|
|
71
|
+
resource << from_hash(resource_hash, nil)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def deserialize_as_collection?(hash)
|
78
|
+
name = collection_parameters&.name
|
79
|
+
# return true/false (Hash#key? returns nil if not found..)
|
80
|
+
name && hash['_embedded']&.key?(name) || false
|
81
|
+
end
|
82
|
+
|
83
|
+
def setter_method_name(attr)
|
84
|
+
"#{attr}=".to_sym
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'hal_presenter/property'
|
2
|
+
|
3
|
+
module HALPresenter
|
4
|
+
module Embedded
|
5
|
+
class Embed < HALPresenter::Property
|
6
|
+
attr_reader :presenter_class
|
7
|
+
|
8
|
+
def initialize(name, value = nil, presenter_class: nil, &block)
|
9
|
+
super(name, value, &block)
|
10
|
+
@presenter_class = presenter_class
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def embed(*args, &block)
|
15
|
+
@_embedded ||= init_embedded
|
16
|
+
@_embedded = @_embedded.reject { |embed| embed.name == args.first }
|
17
|
+
@_embedded << Embed.new(*args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def embedded
|
23
|
+
@_embedded ||= init_embedded
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def init_embedded
|
29
|
+
return [] unless is_a? Class
|
30
|
+
return [] unless superclass.respond_to?(:embedded, true)
|
31
|
+
superclass.embedded.each do |embed|
|
32
|
+
embed.change_scope(self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'hal_presenter/property'
|
2
|
+
|
3
|
+
module HALPresenter
|
4
|
+
|
5
|
+
def self.base_href=(base)
|
6
|
+
@base_href = base&.sub(%r(/*$), '')
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.href(href)
|
10
|
+
return href if (@base_href ||= '').empty?
|
11
|
+
return href if href =~ %r(\A(\w+://)?[^/])
|
12
|
+
@base_href + href
|
13
|
+
end
|
14
|
+
|
15
|
+
module Links
|
16
|
+
|
17
|
+
class Link < HALPresenter::Property
|
18
|
+
attr_reader :http_method
|
19
|
+
def initialize(rel, value = nil, http_method: nil, &block)
|
20
|
+
if value.nil? && !block_given?
|
21
|
+
raise 'link must be called with non nil value or be given a block'
|
22
|
+
end
|
23
|
+
@http_method = http_method
|
24
|
+
super(rel, value, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def rel
|
28
|
+
name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def link(rel, value = nil, method: nil, methods: nil, &block)
|
33
|
+
@_links ||= init_links
|
34
|
+
@_links = @_links.reject { |link| link.rel == rel }
|
35
|
+
@_links << Link.new(rel, value, http_method: method || methods, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def links
|
41
|
+
@_links ||= init_links
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def init_links
|
47
|
+
return [] unless is_a? Class
|
48
|
+
return [] unless superclass.respond_to?(:links, true)
|
49
|
+
superclass.links.each do |link|
|
50
|
+
link.change_scope(self)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module HALPresenter
|
2
|
+
@presenters = {}
|
3
|
+
|
4
|
+
def HALPresenter.register(model:, presenter:)
|
5
|
+
@presenters[presenter] = model
|
6
|
+
end
|
7
|
+
|
8
|
+
def HALPresenter.unregister(presenter)
|
9
|
+
@presenters.delete_if { |d,_| d == presenter }
|
10
|
+
end
|
11
|
+
|
12
|
+
def HALPresenter.lookup_model(presenter)
|
13
|
+
@presenters[presenter]
|
14
|
+
end
|
15
|
+
|
16
|
+
def HALPresenter.lookup_presenter(model)
|
17
|
+
clazz = model.is_a?(Class) ? model : model.class
|
18
|
+
presenters = @presenters.select { |d, m| m == clazz }.keys.compact
|
19
|
+
presenters.empty? ? nil : presenters
|
20
|
+
end
|
21
|
+
|
22
|
+
module Model
|
23
|
+
def model(clazz)
|
24
|
+
HALPresenter.register(model: clazz, presenter: self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def inherited(subclass)
|
28
|
+
if model = HALPresenter.lookup_model(self)
|
29
|
+
HALPresenter.register(model: model, presenter: subclass)
|
30
|
+
end
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module HALPresenter
|
2
|
+
class << self
|
3
|
+
attr_accessor :paginate
|
4
|
+
end
|
5
|
+
|
6
|
+
# TODO: Support Kaminari and Will_paginate
|
7
|
+
|
8
|
+
class Pagination
|
9
|
+
|
10
|
+
class Uri
|
11
|
+
def self.parse(str)
|
12
|
+
uri = nil
|
13
|
+
query = nil
|
14
|
+
unless str.nil? || str.empty?
|
15
|
+
if m = str.match(%r(\A([^\?]+)\??([^\#]+)?.*\Z))
|
16
|
+
uri = m[1]
|
17
|
+
query = query_from_string m[2]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
new(uri, query)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.query_from_string(str)
|
24
|
+
return {} if str.nil? || str.empty?
|
25
|
+
str.split('&').each_with_object({}) do |pair, q|
|
26
|
+
key_value = pair.split('=')
|
27
|
+
q[key_value[0]] = key_value[1];
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(uri, query)
|
32
|
+
@uri = uri
|
33
|
+
@query = query
|
34
|
+
end
|
35
|
+
|
36
|
+
def +(query)
|
37
|
+
self.class.new(@uri, @query.merge(query))
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
return if @uri.nil?
|
42
|
+
@uri.dup.tap do |uri|
|
43
|
+
next if @query.nil? || @query.empty?
|
44
|
+
uri << "?" + @query.map { |k,v| "#{k}=#{v}" }.join('&')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.paginate!(serialized, collection)
|
50
|
+
new(serialized, collection).call
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(serialized, collection)
|
54
|
+
@serialized = serialized
|
55
|
+
@collection = collection
|
56
|
+
@self_uri = Uri.parse serialized.dig(:_links, :self, :href)
|
57
|
+
end
|
58
|
+
|
59
|
+
def call
|
60
|
+
return unless should_paginate?
|
61
|
+
add_query_to_self
|
62
|
+
add_prev_link
|
63
|
+
add_next_link
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_accessor :serialized, :collection, :self_uri
|
69
|
+
|
70
|
+
def should_paginate?
|
71
|
+
self_uri && current_page
|
72
|
+
end
|
73
|
+
|
74
|
+
def query(page)
|
75
|
+
return {} unless page
|
76
|
+
{
|
77
|
+
page: page,
|
78
|
+
per_page: per_page,
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_query_to_self
|
83
|
+
serialized[:_links][:self][:href] = (self_uri + query(current_page)).to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_prev_link
|
87
|
+
return if serialized[:_links][:prev] || !prev_page
|
88
|
+
serialized[:_links][:prev] = {
|
89
|
+
href: (self_uri + query(prev_page)).to_s
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_next_link
|
94
|
+
return if serialized[:_links][:next] || !next_page
|
95
|
+
serialized[:_links][:next] = {
|
96
|
+
href: (self_uri + query(next_page)).to_s
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
# Supported by Sequel, Kaminari and will_paginate
|
101
|
+
def current_page
|
102
|
+
collection.respond_to?(:current_page) && collection.current_page
|
103
|
+
end
|
104
|
+
|
105
|
+
def prev_page
|
106
|
+
return collection.prev_page if collection.respond_to?(:prev_page) # Sequel and Kaminari
|
107
|
+
return collection.previous_page if collection.respond_to?(:previous_page) # will_paginate
|
108
|
+
end
|
109
|
+
|
110
|
+
# Supported by Sequel, Kaminari and will_paginate
|
111
|
+
def next_page
|
112
|
+
collection.respond_to?(:next_page) && collection.next_page
|
113
|
+
end
|
114
|
+
|
115
|
+
def per_page
|
116
|
+
if collection.respond_to?(:page_size)
|
117
|
+
collection.page_size # Supported by Sequel
|
118
|
+
elsif collection.respond_to?(:max_per_page)
|
119
|
+
collection.max_per_page # Supported by Kaminari
|
120
|
+
elsif collection.respond_to?(:per_page)
|
121
|
+
collection.per_page # Supported by will_paginate
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module HALPresenter
|
2
|
+
module Policy
|
3
|
+
|
4
|
+
def policy(clazz)
|
5
|
+
@_policy = clazz
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def policy_class
|
11
|
+
@_policy ||= init_policy
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def init_policy
|
17
|
+
return unless is_a? Class
|
18
|
+
return unless superclass.respond_to?(:policy_class, true)
|
19
|
+
superclass.policy_class
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module HALPresenter
|
2
|
+
module Policy
|
3
|
+
module DSL
|
4
|
+
|
5
|
+
class Rules
|
6
|
+
|
7
|
+
def attributes
|
8
|
+
@attributes ||= Hash.new(Proc.new { false })
|
9
|
+
end
|
10
|
+
|
11
|
+
def links
|
12
|
+
@links ||= Hash.new(Proc.new { false })
|
13
|
+
end
|
14
|
+
|
15
|
+
def embedded
|
16
|
+
@embedded ||= Hash.new(Proc.new { false })
|
17
|
+
end
|
18
|
+
|
19
|
+
private :attributes, :links, :embedded
|
20
|
+
|
21
|
+
def defaults(*types, value: false)
|
22
|
+
types.each do |t|
|
23
|
+
send(t).default= Proc.new { value }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def attribute_rule_for(name)
|
28
|
+
attributes[name]
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_attribute(name, block)
|
32
|
+
attributes[name] = block
|
33
|
+
end
|
34
|
+
|
35
|
+
def link_rule_for(rel)
|
36
|
+
links[rel]
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_link(rel, block)
|
40
|
+
links[rel] = block
|
41
|
+
end
|
42
|
+
|
43
|
+
def embed_rule_for(name)
|
44
|
+
embedded[name]
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_embed(name, block)
|
48
|
+
embedded[name] = block
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
|
55
|
+
def allow_by_default(*types)
|
56
|
+
rules.defaults(*types, value: true)
|
57
|
+
end
|
58
|
+
|
59
|
+
def attribute(*names)
|
60
|
+
b = block_given? ? Proc.new : Proc.new { true }
|
61
|
+
names.each { |name| rules.add_attribute(name, b) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def link(*rels)
|
65
|
+
b = block_given? ? Proc.new : Proc.new { true }
|
66
|
+
rels.each { |rel| rules.add_link(rel, b) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def embed(*names)
|
70
|
+
b = block_given? ? Proc.new : Proc.new { true }
|
71
|
+
names.each { |name| rules.add_embed(name, b) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def rules
|
75
|
+
@rules ||= Rules.new
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.included(mod)
|
81
|
+
mod.extend(ClassMethods)
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize(current_user, resource, options = {})
|
85
|
+
@current_user = current_user
|
86
|
+
@resource = resource
|
87
|
+
@options = options
|
88
|
+
end
|
89
|
+
|
90
|
+
def attribute?(name)
|
91
|
+
run self.class.rules.attribute_rule_for(name)
|
92
|
+
end
|
93
|
+
|
94
|
+
def link?(rel)
|
95
|
+
run self.class.rules.link_rule_for(rel)
|
96
|
+
end
|
97
|
+
|
98
|
+
def embed?(name)
|
99
|
+
run self.class.rules.embed_rule_for(name)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
attr_reader :current_user, :resource, :options
|
105
|
+
|
106
|
+
def run(block)
|
107
|
+
instance_eval(&block) && true || false
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module HALPresenter
|
2
|
+
class Property
|
3
|
+
attr_reader :name, :resource, :options
|
4
|
+
|
5
|
+
alias :resources :resource
|
6
|
+
|
7
|
+
def initialize(name, value = nil, &block)
|
8
|
+
@name = name
|
9
|
+
@value = value
|
10
|
+
@scope = nil
|
11
|
+
return unless block_given?
|
12
|
+
@scope = eval 'self', block.binding
|
13
|
+
define_singleton_method(:value_from_block, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def value(resource = nil, options = {})
|
17
|
+
@resource = resource
|
18
|
+
@options = options
|
19
|
+
if scope
|
20
|
+
value_from_block
|
21
|
+
elsif resource && @value.nil?
|
22
|
+
resource.public_send(name) if resource.respond_to?(name)
|
23
|
+
else
|
24
|
+
@value
|
25
|
+
end
|
26
|
+
ensure
|
27
|
+
reset
|
28
|
+
end
|
29
|
+
|
30
|
+
def change_scope(new_scope)
|
31
|
+
return unless scope
|
32
|
+
@scope = new_scope
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(method, *args, &block)
|
36
|
+
if scope&.respond_to? method
|
37
|
+
define_singleton_method(method) { |*a, &b| scope.public_send method, *a, &b }
|
38
|
+
return public_send(method, *args, &block)
|
39
|
+
end
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def respond_to_missing?(method, include_private = false)
|
44
|
+
return true if scope&.respond_to? method
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :scope
|
51
|
+
|
52
|
+
def reset
|
53
|
+
@resource = nil
|
54
|
+
@options = nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'hal_presenter/property'
|
2
|
+
|
3
|
+
module HALPresenter
|
4
|
+
module SerializeHooks
|
5
|
+
|
6
|
+
class Hook
|
7
|
+
attr_reader :name, :resource, :options
|
8
|
+
|
9
|
+
def initialize(&block)
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(resource, options, arg)
|
14
|
+
@resource = resource
|
15
|
+
@options = options
|
16
|
+
instance_exec(arg, &@block) if @block
|
17
|
+
ensure
|
18
|
+
@resource = nil
|
19
|
+
@options = nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def post_serialize(&block)
|
24
|
+
@_post_serialize_hook = Hook.new(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def post_serialize_hook
|
30
|
+
@_post_serialize_hook ||= init_post_serialize_hook
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def init_post_serialize_hook
|
36
|
+
return unless is_a? Class
|
37
|
+
return unless superclass.respond_to?(:post_serialize_hook, true)
|
38
|
+
superclass.post_serialize_hook
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'hal_presenter/pagination'
|
3
|
+
|
4
|
+
module HALPresenter
|
5
|
+
|
6
|
+
def self.to_hal(resource, options = {})
|
7
|
+
raise Serializer::Error, "Resource is nil" if resource.nil?
|
8
|
+
presenter = options.delete(:presenter)
|
9
|
+
presenter ||= HALPresenter.lookup_presenter(resource)&.last
|
10
|
+
raise Serializer::Error, "No presenter for #{resource}" unless presenter
|
11
|
+
presenter.to_hal(resource, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.to_collection(resources, options = {})
|
15
|
+
raise Serializer::Error, "resources is nil" if resources.nil?
|
16
|
+
presenter = options.delete(:presenter)
|
17
|
+
presenter ||= HALPresenter.lookup_presenter(resources.first)&.last
|
18
|
+
raise Serializer::Error, "No presenter for #{resources.first}" unless presenter
|
19
|
+
presenter.to_collection(resources, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
module Serializer
|
23
|
+
|
24
|
+
class Error < StandardError; end
|
25
|
+
|
26
|
+
def to_hal(resource = nil, options = {})
|
27
|
+
hash = to_hash(resource, options)
|
28
|
+
JSON.generate(hash)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_collection(resources = [], options = {})
|
32
|
+
unless can_serialize_collection?
|
33
|
+
raise Error,
|
34
|
+
"Trying to serialize a collection using #{self} which has no collection info. " \
|
35
|
+
"Add a 'collection' spec to the serializer or use another serializer"
|
36
|
+
end
|
37
|
+
options[:paginate] = HALPresenter.paginate unless options.key? :paginate
|
38
|
+
hash = to_collection_hash(resources, options)
|
39
|
+
JSON.generate(hash)
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def to_hash(resource, options)
|
45
|
+
policy = policy_class&.new(options[:current_user], resource, options)
|
46
|
+
|
47
|
+
{}.tap do |serialized|
|
48
|
+
serialized.merge! serialize_attributes(resource, policy, options)
|
49
|
+
serialized.merge! serialize_links(resource, policy, options)
|
50
|
+
serialized.merge! serialize_embedded(resource, policy, options)
|
51
|
+
|
52
|
+
run_post_serialize_hook!(resource, options, serialized)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_collection_hash(resources, options)
|
57
|
+
parameters = collection_parameters
|
58
|
+
links = parameters.links
|
59
|
+
curies = parameters.curies
|
60
|
+
{}.tap do |serialized|
|
61
|
+
serialized.merge! _serialize_attributes(parameters.attributes, resources, nil, options)
|
62
|
+
serialized.merge! _serialize_links(links, curies, resources, nil, options)
|
63
|
+
Pagination.paginate!(serialized, resources) if options[:paginate]
|
64
|
+
|
65
|
+
serialized_resources = resources.map { |resource| to_hash(resource, options) }
|
66
|
+
serialized[:_embedded] = { parameters.name => serialized_resources }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def serialize_attributes(resource, policy, options)
|
71
|
+
_serialize_attributes(attributes, resource, policy, options)
|
72
|
+
end
|
73
|
+
|
74
|
+
def serialize_links(resource, policy, options)
|
75
|
+
_serialize_links(links, curies, resource, policy, options)
|
76
|
+
end
|
77
|
+
|
78
|
+
def serialize_curies(resource, policy, options)
|
79
|
+
_serialize_curies(curies, resource, policy, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
def serialize_embedded(resource, policy, options)
|
83
|
+
_serialize_embedded(embedded, resource, policy, options)
|
84
|
+
end
|
85
|
+
|
86
|
+
def run_post_serialize_hook!(resource, options, serialized)
|
87
|
+
hook = post_serialize_hook
|
88
|
+
hook&.run(resource, options, serialized)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def _serialize_attributes(attributes, resource, policy, options)
|
94
|
+
attributes.each_with_object({}) do |attribute, hash|
|
95
|
+
next if policy && !policy.attribute?(attribute.name)
|
96
|
+
hash[attribute.name] = attribute.value(resource, options)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def _serialize_links(links, curies, resource, policy, options)
|
101
|
+
serialized = links.each_with_object({}) do |link, hash|
|
102
|
+
next if policy && !policy.link?(link.rel)
|
103
|
+
href = link.value(resource, options) or next
|
104
|
+
hash[link.rel] = { href: HALPresenter.href(href) }.tap do |s|
|
105
|
+
s[:method] = link.http_method if link.http_method
|
106
|
+
end
|
107
|
+
end
|
108
|
+
curies = _serialize_curies(curies, resource, policy, options)
|
109
|
+
serialized[:curies] = curies if curies.any?
|
110
|
+
return {} if serialized.empty?
|
111
|
+
{ _links: serialized }
|
112
|
+
end
|
113
|
+
|
114
|
+
def _serialize_curies(curies, resource, policy, options)
|
115
|
+
curies.each_with_object([]) do |curie, array|
|
116
|
+
next if policy && !policy.link?(curie.name)
|
117
|
+
href = curie.value(resource, options) or next
|
118
|
+
array << {
|
119
|
+
name: curie.name,
|
120
|
+
href: HALPresenter.href(href),
|
121
|
+
templated: true
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def _serialize_embedded(embedded, object, policy, options)
|
127
|
+
serialized = embedded.each_with_object({}) do |embed, hash|
|
128
|
+
next if policy && !policy.embed?(embed.name)
|
129
|
+
resource = embed.value(object, options) or next
|
130
|
+
presenter = embed.presenter_class
|
131
|
+
hash[embed.name] =
|
132
|
+
if resource.respond_to? :each
|
133
|
+
_serialize_embedded_collection(resource, presenter, options)
|
134
|
+
else
|
135
|
+
presenter ||= HALPresenter.lookup_presenter(resource).first
|
136
|
+
presenter.to_hash(resource, options)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
return {} if serialized.empty?
|
140
|
+
{ _embedded: serialized }
|
141
|
+
end
|
142
|
+
|
143
|
+
def _serialize_embedded_collection(resources, presenter, options)
|
144
|
+
clazz = resources.first.class
|
145
|
+
presenter ||= HALPresenter.lookup_presenter(clazz)&.first
|
146
|
+
if presenter.nil?
|
147
|
+
raise Serializer::Error,
|
148
|
+
"No presenter specified to handle serializing embedded #{clazz}"
|
149
|
+
end
|
150
|
+
if presenter.respond_to?(:can_serialize_collection?, true) &&
|
151
|
+
presenter.can_serialize_collection?
|
152
|
+
presenter.to_collection_hash(resources, options)
|
153
|
+
else
|
154
|
+
resources.map do |resrc|
|
155
|
+
presenter.to_hash(resrc, options)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
|
metadata
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hal_presenter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sammy Henningsson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDljCCAn6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBIMRowGAYDVQQDDBFzYW1t
|
14
|
+
eS5oZW5uaW5nc3NvbjEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
|
15
|
+
LGQBGRYDY29tMB4XDTE3MDcwODE0MDUxOVoXDTE4MDcwODE0MDUxOVowSDEaMBgG
|
16
|
+
A1UEAwwRc2FtbXkuaGVubmluZ3Nzb24xFTATBgoJkiaJk/IsZAEZFgVnbWFpbDET
|
17
|
+
MBEGCgmSJomT8ixkARkWA2NvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
18
|
+
ggEBAK+SDC1mfyhucJ6Va21rIHUGscEtQrdvyBqxFG1s2TgPMAv4RbqwdJVPa7kj
|
19
|
+
tbCzslADlUE1oru2C+rcJsMtVGX02ukMIPHT1OjTyy0/EMqLqSy3WeRI8APyDSxC
|
20
|
+
Vbe+h5BMf3zZnYfddR6AeG7ln09T1P/tX+9lTMc+I+DW1fUlQgY48CNUayvtJR61
|
21
|
+
svXvXMrhLhi29SQig1qmH6Zoe22/JgH+m2JksPndY5Ep3gqfDc6Imwu2vGvmGErJ
|
22
|
+
D63FB0XQ/wb4WVH4l7sHQSTfKDp8SImCt1xqNgIyjw578ZG2geGLoncuxgDrbQ/U
|
23
|
+
FIJ11lDZd4vLevMhnIxTSJpPr2cCAwEAAaOBijCBhzAJBgNVHRMEAjAAMAsGA1Ud
|
24
|
+
DwQEAwIEsDAdBgNVHQ4EFgQUukjj1Cd2ea6IOHDLZe0ymzs2jWkwJgYDVR0RBB8w
|
25
|
+
HYEbc2FtbXkuaGVubmluZ3Nzb25AZ21haWwuY29tMCYGA1UdEgQfMB2BG3NhbW15
|
26
|
+
Lmhlbm5pbmdzc29uQGdtYWlsLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAGBReofO0
|
27
|
+
CDGI8uQNn/btde+zQBiCX0mONJMQ6An760MzOHMlWFi6VaVeeKmYj5xCpI2nWaJN
|
28
|
+
49ALW/v7GZ+JIW5YmbzWIuD8YfwHJYCFPoH0llzg3GCFCMxbYqI3nDtHX3dMxpOc
|
29
|
+
01A72okERe0ne/O5Z4Ym/fOECkyXr0fYondhDQcSuqxU22jsXs1ZjISN8IuHmVkA
|
30
|
+
GtGteOIqmA05lP+eCXGvyARGLg1GwZDTjnlH5OfpiT1Oy7Dghewi58Gn4/AlZ4bY
|
31
|
+
CNZdF8Vavp6xMQbPHZwqjaeZz2WRXYS7jyYSvCunjwa3OtvXtfbIEGEWE6IM+t9k
|
32
|
+
H1g6Q+B6qk9O6g==
|
33
|
+
-----END CERTIFICATE-----
|
34
|
+
date: 2018-01-28 00:00:00.000000000 Z
|
35
|
+
dependencies:
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '12.0'
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '10.0'
|
46
|
+
type: :development
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - "~>"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '12.0'
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '10.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: activesupport
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '5.0'
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '4.0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - "~>"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '5.0'
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.0'
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: minitest
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.10'
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '5.0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - "~>"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '5.10'
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '5.0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: byebug
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '9.0'
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '9.0'
|
106
|
+
type: :development
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - "~>"
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '9.0'
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '9.0'
|
116
|
+
- !ruby/object:Gem::Dependency
|
117
|
+
name: kaminari
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - "~>"
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '1.1'
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 1.1.1
|
126
|
+
type: :development
|
127
|
+
prerelease: false
|
128
|
+
version_requirements: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '1.1'
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: 1.1.1
|
136
|
+
- !ruby/object:Gem::Dependency
|
137
|
+
name: will_paginate
|
138
|
+
requirement: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - "~>"
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '3.1'
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 3.1.6
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '3.1'
|
153
|
+
- - ">="
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: 3.1.6
|
156
|
+
description: |
|
157
|
+
A DSL for serializing resources according to
|
158
|
+
HypertextApplicationLanguage.
|
159
|
+
email: sammy.henningsson@gmail.com
|
160
|
+
executables: []
|
161
|
+
extensions: []
|
162
|
+
extra_rdoc_files: []
|
163
|
+
files:
|
164
|
+
- lib/hal_presenter.rb
|
165
|
+
- lib/hal_presenter/attributes.rb
|
166
|
+
- lib/hal_presenter/collection.rb
|
167
|
+
- lib/hal_presenter/curies.rb
|
168
|
+
- lib/hal_presenter/deserializer.rb
|
169
|
+
- lib/hal_presenter/embedded.rb
|
170
|
+
- lib/hal_presenter/links.rb
|
171
|
+
- lib/hal_presenter/model.rb
|
172
|
+
- lib/hal_presenter/pagination.rb
|
173
|
+
- lib/hal_presenter/policy.rb
|
174
|
+
- lib/hal_presenter/policy/dsl.rb
|
175
|
+
- lib/hal_presenter/property.rb
|
176
|
+
- lib/hal_presenter/serialize_hooks.rb
|
177
|
+
- lib/hal_presenter/serializer.rb
|
178
|
+
homepage: https://github.com/sammyhenningsson/hal_presenter
|
179
|
+
licenses:
|
180
|
+
- MIT
|
181
|
+
metadata: {}
|
182
|
+
post_install_message:
|
183
|
+
rdoc_options: []
|
184
|
+
require_paths:
|
185
|
+
- lib
|
186
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '0'
|
196
|
+
requirements: []
|
197
|
+
rubyforge_project:
|
198
|
+
rubygems_version: 2.7.3
|
199
|
+
signing_key:
|
200
|
+
specification_version: 4
|
201
|
+
summary: HAL serializer
|
202
|
+
test_files: []
|
metadata.gz.sig
ADDED