shaf_client 0.6.2 → 1.1.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/bin/shaf_client +30 -0
- data/lib/faraday_http_cache_patch.rb +17 -0
- data/lib/shaf_client.rb +27 -19
- data/lib/shaf_client/alps/descriptor.rb +60 -0
- data/lib/shaf_client/alps/extension.rb +35 -0
- data/lib/shaf_client/alps_json.rb +44 -0
- data/lib/shaf_client/api_error.rb +2 -1
- data/lib/shaf_client/base_resource.rb +63 -23
- data/lib/shaf_client/content_type_map.rb +53 -0
- data/lib/shaf_client/curie.rb +4 -0
- data/lib/shaf_client/empty_resource.rb +1 -1
- data/lib/shaf_client/hal_form.rb +1 -1
- data/lib/shaf_client/hypertext_cache_strategy.rb +43 -0
- data/lib/shaf_client/link.rb +2 -2
- data/lib/shaf_client/mime_types.rb +8 -0
- data/lib/shaf_client/problem_json.rb +38 -0
- data/lib/shaf_client/resource.rb +74 -10
- data/lib/shaf_client/resource_extension.rb +36 -0
- data/lib/shaf_client/resource_extension/alps_http_method.rb +67 -0
- data/lib/shaf_client/resource_extension/base.rb +13 -0
- data/lib/shaf_client/resource_mapper.rb +86 -10
- data/lib/shaf_client/shaf_form.rb +3 -2
- data/lib/shaf_client/status_codes.rb +70 -0
- data/lib/shaf_client/test/stubbing.rb +16 -0
- data/lib/shaf_client/unknown_resource.rb +3 -3
- data/lib/shaf_client/version.rb +3 -0
- metadata +58 -29
- metadata.gz.sig +0 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
class ShafClient
|
4
|
+
class ContentTypeMap
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def_delegators :@map, :default, :default=, :keys, :values, :each
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@map = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](content_type, profile = nil)
|
14
|
+
key = key_for(content_type, profile)
|
15
|
+
map.fetch(key) do
|
16
|
+
key = key_for(content_type, nil)
|
17
|
+
map[key]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def []=(content_type, profile = nil, value)
|
22
|
+
key = key_for(content_type, profile)
|
23
|
+
map[key] = value
|
24
|
+
end
|
25
|
+
|
26
|
+
def key?(content_type, profile = nil)
|
27
|
+
key = key_for(content_type, profile)
|
28
|
+
map.key? key
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete(content_type, profile = nil)
|
32
|
+
key = key_for(content_type, profile)
|
33
|
+
map.delete(key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def key_for(content_type, profile)
|
37
|
+
return unless content_type
|
38
|
+
|
39
|
+
key = content_type.to_s.downcase
|
40
|
+
key = strip_parameters(key)
|
41
|
+
key << "_#{profile.to_s.downcase}" if profile
|
42
|
+
key
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :map
|
48
|
+
|
49
|
+
def strip_parameters(content_type)
|
50
|
+
content_type&.sub(/;.*/, '')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/shaf_client/curie.rb
CHANGED
data/lib/shaf_client/hal_form.rb
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
class ShafClient
|
2
|
+
module HypertextCacheStrategy
|
3
|
+
AVAILABLE_CACHE_STRATEGIES = [
|
4
|
+
CACHE_STRATEGY_NO_CACHE = :no_cache,
|
5
|
+
CACHE_STRATEGY_EMBEDDED = :use_embedded,
|
6
|
+
CACHE_STRATEGY_FETCH_HEADERS = :fetch_headers
|
7
|
+
]
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def cacheable?(strategy)
|
11
|
+
[CACHE_STRATEGY_EMBEDDED, CACHE_STRATEGY_FETCH_HEADERS].include? strategy
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_headers?(strategy)
|
15
|
+
CACHE_STRATEGY_FETCH_HEADERS == strategy
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_http_status
|
19
|
+
203
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_hypertext_cache_strategy
|
24
|
+
@__default_hypertext_cache_strategy ||= CACHE_STRATEGY_EMBEDDED
|
25
|
+
end
|
26
|
+
|
27
|
+
def default_hypertext_cache_strategy=(strategy)
|
28
|
+
unless __valid_cache? strategy
|
29
|
+
raise Error, <<~ERR
|
30
|
+
Unsupported hypertext cache strategy: #{strategy}
|
31
|
+
Possible strategies are: #{AVAILABLE_CACHE_STRATEGIES.join(', ')}
|
32
|
+
ERR
|
33
|
+
end
|
34
|
+
@__default_hypertext_cache_strategy = value
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def __valid_cache?(strategy)
|
40
|
+
AVAILABLE_CACHE_STRATEGIES.include? strategy
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/shaf_client/link.rb
CHANGED
@@ -27,8 +27,8 @@ class ShafClient
|
|
27
27
|
return href unless templated?
|
28
28
|
|
29
29
|
href
|
30
|
-
.
|
31
|
-
.
|
30
|
+
.yield_self { |href| resolve_required(href, **args) }
|
31
|
+
.yield_self { |href| resolve_optional(href, **args) }
|
32
32
|
end
|
33
33
|
|
34
34
|
def to_h
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'shaf_client/form'
|
2
|
+
require 'shaf_client/status_codes'
|
3
|
+
|
4
|
+
class ShafClient
|
5
|
+
class ProblemJson < Resource
|
6
|
+
include StatusCodes
|
7
|
+
|
8
|
+
content_type MIME_TYPE_PROBLEM_JSON
|
9
|
+
|
10
|
+
def type
|
11
|
+
attribute(:type) { 'about:blank' }
|
12
|
+
end
|
13
|
+
|
14
|
+
def title
|
15
|
+
attribute(:title) do
|
16
|
+
next unless type == 'about:blank'
|
17
|
+
|
18
|
+
StatusCode[status] if (400..599).include? status.to_i
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def status
|
23
|
+
attribute(:status) { http_status }
|
24
|
+
end
|
25
|
+
|
26
|
+
def detail
|
27
|
+
attribute(:detail)
|
28
|
+
end
|
29
|
+
|
30
|
+
def instance
|
31
|
+
attribute(:instance)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_h
|
35
|
+
attributes
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/shaf_client/resource.rb
CHANGED
@@ -4,21 +4,34 @@ require 'shaf_client/base_resource'
|
|
4
4
|
|
5
5
|
class ShafClient
|
6
6
|
class Resource < BaseResource
|
7
|
+
include MimeTypes
|
8
|
+
|
7
9
|
attr_reader :http_status, :headers
|
8
10
|
|
9
|
-
ResourceMapper.register(
|
11
|
+
ResourceMapper.register(MIME_TYPE_HAL, self)
|
10
12
|
|
11
|
-
def self.content_type(type)
|
12
|
-
ResourceMapper.register(type, self)
|
13
|
+
def self.content_type(type, profile: nil)
|
14
|
+
ResourceMapper.register(type, profile, self)
|
13
15
|
end
|
14
16
|
|
15
17
|
def self.profile(name)
|
16
|
-
content_type
|
18
|
+
content_type MIME_TYPE_HAL, profile: name
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.build(client, payload, content_type = MIME_TYPE_HAL, status = nil, headers = {})
|
22
|
+
resource_class, extensions = ResourceMapper.for(
|
23
|
+
content_type: content_type,
|
24
|
+
headers: headers,
|
25
|
+
payload: payload,
|
26
|
+
client: client,
|
27
|
+
)
|
28
|
+
resrc = resource_class.new(client, payload, status, headers)
|
29
|
+
extensions.compact.each { |extension| resrc.extend extension }
|
30
|
+
resrc
|
17
31
|
end
|
18
32
|
|
19
|
-
def self.
|
20
|
-
|
21
|
-
ResourceMapper.for(content_type).new(client, payload, status, headers)
|
33
|
+
def self.default_resource_class!
|
34
|
+
ResourceMapper.default = self
|
22
35
|
end
|
23
36
|
|
24
37
|
def initialize(client, payload, status = nil, headers = {})
|
@@ -36,14 +49,21 @@ class ShafClient
|
|
36
49
|
RESOURCE
|
37
50
|
end
|
38
51
|
|
39
|
-
%i[
|
52
|
+
%i[put post delete patch].each do |method|
|
40
53
|
define_method(method) do |rel, payload = nil, **options|
|
41
54
|
href = link(rel).href
|
42
55
|
client.send(method, href, payload: payload, **options)
|
43
56
|
end
|
44
57
|
end
|
45
58
|
|
46
|
-
def
|
59
|
+
def get(rel, **options)
|
60
|
+
href = link(rel).href
|
61
|
+
embedded_resource = _embedded(rel)
|
62
|
+
cached_resource = hypertext_cache_resource(href, embedded_resource, options)
|
63
|
+
cached_resource || client.get(href, **options)
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_doc(rel, **options)
|
47
67
|
rel = rel.to_s
|
48
68
|
curie_name, rel =
|
49
69
|
if rel.include? ':'
|
@@ -54,7 +74,7 @@ class ShafClient
|
|
54
74
|
|
55
75
|
curie = curie(curie_name)
|
56
76
|
uri = curie.resolve_templated(rel: rel)
|
57
|
-
client.
|
77
|
+
client.get(uri, **options)
|
58
78
|
end
|
59
79
|
|
60
80
|
def get_hal_form(rel)
|
@@ -80,9 +100,14 @@ class ShafClient
|
|
80
100
|
delete(:delete)
|
81
101
|
end
|
82
102
|
|
103
|
+
def content_type
|
104
|
+
headers['content-type']
|
105
|
+
end
|
106
|
+
|
83
107
|
protected
|
84
108
|
|
85
109
|
def <<(other)
|
110
|
+
@client = other.client
|
86
111
|
@http_status = other.http_status.dup
|
87
112
|
@headers = other.headers.dup
|
88
113
|
super
|
@@ -91,5 +116,44 @@ class ShafClient
|
|
91
116
|
private
|
92
117
|
|
93
118
|
attr_reader :client
|
119
|
+
|
120
|
+
def build_embedded_resource(payload)
|
121
|
+
self.class.build(
|
122
|
+
client,
|
123
|
+
payload,
|
124
|
+
headers['content-type'],
|
125
|
+
http_status,
|
126
|
+
headers
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
def hypertext_cache_strategy(options)
|
131
|
+
options.fetch(:hypertext_cache_strategy) do
|
132
|
+
ShafClient.default_hypertext_cache_strategy
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def hypertext_cache?(options)
|
137
|
+
HypertextCacheStrategy.cacheable? hypertext_cache_strategy(options)
|
138
|
+
end
|
139
|
+
|
140
|
+
def hypertext_cache_resource(href, embedded_resource, options)
|
141
|
+
return unless embedded_resource
|
142
|
+
|
143
|
+
cache_strategy = hypertext_cache_strategy(options)
|
144
|
+
return unless HypertextCacheStrategy.cacheable? cache_strategy
|
145
|
+
|
146
|
+
if HypertextCacheStrategy.fetch_headers? cache_strategy
|
147
|
+
resource = client.head(href, **options)
|
148
|
+
status = resource.http_status
|
149
|
+
headers = resource.headers
|
150
|
+
embedded_resource = embedded_resource.payload
|
151
|
+
else
|
152
|
+
status = HypertextCacheStrategy.default_http_status
|
153
|
+
headers = embedded_resource.headers
|
154
|
+
end
|
155
|
+
|
156
|
+
self.class.build(client, embedded_resource, headers['content-type'], status, headers)
|
157
|
+
end
|
94
158
|
end
|
95
159
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
class ShafClient
|
4
|
+
module ResourceExtension
|
5
|
+
class << self
|
6
|
+
def register(extender)
|
7
|
+
extenders << extender
|
8
|
+
end
|
9
|
+
|
10
|
+
def unregister(extender)
|
11
|
+
extenders.delete(extender)
|
12
|
+
end
|
13
|
+
|
14
|
+
def for(profile, base, link_relations, client)
|
15
|
+
link_relations = remove_curies(link_relations)
|
16
|
+
extenders.map { |extender| extender.call(profile, base, link_relations, client) }
|
17
|
+
.compact
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def extenders
|
23
|
+
@extenders ||= Set.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove_curies(link_relations)
|
27
|
+
Array(link_relations).map do |rel|
|
28
|
+
rel.to_s.sub(/[^:]*:/, '').to_sym
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
require 'shaf_client/resource_extension/base'
|
36
|
+
require 'shaf_client/resource_extension/alps_http_method'
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class ShafClient
|
2
|
+
module ResourceExtension
|
3
|
+
class AlpsHttpMethod < Base
|
4
|
+
class << self
|
5
|
+
def call(profile, base, link_relations, _client)
|
6
|
+
return unless profile.is_a? AlpsJson
|
7
|
+
return unless base <= Resource
|
8
|
+
|
9
|
+
link_relations = Array(link_relations).compact
|
10
|
+
descriptors = descriptors_with_http_method(profile)
|
11
|
+
descriptors.keep_if do |descriptor|
|
12
|
+
link_relations.include? identifier_for(descriptor)&.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
extension_for(descriptors)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def descriptors_with_http_method(profile)
|
21
|
+
profile.each_descriptor.each_with_object([]) do |descriptor, descriptors|
|
22
|
+
next unless descriptor.extension(:http_method)
|
23
|
+
descriptors << descriptor
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def extension_for(descriptors)
|
28
|
+
return if descriptors.empty?
|
29
|
+
|
30
|
+
Module.new.tap do |mod|
|
31
|
+
descriptors.each do |descriptor|
|
32
|
+
add_method(mod, descriptor, methods.first)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_method(mod, descriptor, method)
|
38
|
+
rel = identifier_for(descriptor)
|
39
|
+
return unless rel
|
40
|
+
|
41
|
+
ext = descriptor.extension(:http_method)
|
42
|
+
methods = Array(ext&.value)
|
43
|
+
|
44
|
+
# We only know what method to use when size is 1
|
45
|
+
return unless methods.size == 1
|
46
|
+
|
47
|
+
http_method = methods.first.downcase.to_sym
|
48
|
+
name = method_name_from(rel)
|
49
|
+
|
50
|
+
mod.define_method(name) do |payload: nil, **options|
|
51
|
+
href = link(rel).href
|
52
|
+
client.send(http_method, href, payload: payload, **options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_name_from(rel)
|
57
|
+
"#{rel.to_s.downcase.tr('-', '_')}!"
|
58
|
+
end
|
59
|
+
|
60
|
+
def identifier_for(descriptor)
|
61
|
+
# Currently we only support `id` (i.e no support for descriptors with `href`)
|
62
|
+
descriptor.id
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|