hal_presenter 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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