shaf_client 0.6.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -26,5 +26,9 @@ class ShafClient
26
26
  args[:rel] &&= args[:rel].to_s.sub(/#{name}:/, '')
27
27
  super(**args)
28
28
  end
29
+
30
+ def to_h
31
+ {name: name}.merge(super)
32
+ end
29
33
  end
30
34
  end
@@ -21,7 +21,7 @@ class ShafClient
21
21
 
22
22
  %i[get put post delete patch, get_doc, reload!].each do |method|
23
23
  define_method(method) do |*_args|
24
- raise "EmptyResource: #{method} not available"
24
+ raise Error, "EmptyResource: #{method} not available"
25
25
  end
26
26
  end
27
27
  end
@@ -24,7 +24,7 @@ class ShafClient
24
24
 
25
25
  def fields
26
26
  template[:properties].map do |values|
27
- Field.new(values.transform_keys(&:to_sym))
27
+ Field.new(**values.transform_keys(&:to_sym))
28
28
  end
29
29
  end
30
30
 
@@ -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
@@ -27,8 +27,8 @@ class ShafClient
27
27
  return href unless templated?
28
28
 
29
29
  href
30
- .then { |href| resolve_required(href, **args) }
31
- .then { |href| resolve_optional(href, **args) }
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,8 @@
1
+ class ShafClient
2
+ module MimeTypes
3
+ MIME_TYPE_JSON = 'application/json'
4
+ MIME_TYPE_HAL = 'application/hal+json'
5
+ MIME_TYPE_PROBLEM_JSON = 'application/problem+json'
6
+ MIME_TYPE_ALPS_JSON = 'application/alps+json'
7
+ end
8
+ end
@@ -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
@@ -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("application/hal+json", self)
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 "application/hal+json;profile=#{name}"
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.build(client, payload, status = nil, headers = {})
20
- content_type = headers.fetch('content-type', '')
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[get put post delete patch].each do |method|
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 get_doc(rel)
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.get_doc(uri)
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
@@ -0,0 +1,13 @@
1
+ class ShafClient
2
+ module ResourceExtension
3
+ class Base
4
+ def self.inherited(mod)
5
+ ResourceExtension.register(mod)
6
+ end
7
+
8
+ def self.call(*args)
9
+ raise NotImplementedError, "Class '#{self}' must respond to `call`"
10
+ end
11
+ end
12
+ end
13
+ end