shaf_client 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6522a9d02450366d188195b580b2880a643cd596d284e6ada3d13228160b0038
4
+ data.tar.gz: 3aa9f908ed8f9e7b3ce423c27fcd640bd4569cdbf1e533d64c67d76dd8da5962
5
+ SHA512:
6
+ metadata.gz: 775eb0e67adf697b7213a407cf836a829ba2f73555cbb68b234ad94461a37966e4a278c7738259b0e9b127c40aacdc2cbae6adfa56d697655282c7d1c28b3cbf
7
+ data.tar.gz: c1cc2969026ab53b58d12a5c5404922afffa89f1be47a08f4b92eb0c3d9bd9d9e190265bd690a0939bcee1ca8dca9926884ecc88f99b625280f5104a13960ff7
checksums.yaml.gz.sig ADDED
Binary file
data.tar.gz.sig ADDED
Binary file
@@ -0,0 +1,87 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'shaf_client/middleware/cache'
4
+ require 'shaf_client/resource'
5
+ require 'shaf_client/form'
6
+
7
+ class ShafClient
8
+ extend Middleware::Cache::Control
9
+
10
+ MIME_TYPE_JSON = 'application/json'
11
+
12
+ def initialize(root_uri, **options)
13
+ @root_uri = root_uri.dup
14
+ adapter = options.fetch(:faraday_adapter, :net_http)
15
+ setup options
16
+
17
+ @client = Faraday.new(url: root_uri) do |conn|
18
+ conn.basic_auth(@user, @pass) if basic_auth?
19
+ conn.use Middleware::Cache, auth_header: auth_header
20
+ conn.adapter adapter
21
+ end
22
+ end
23
+
24
+ def get_root
25
+ get(@root_uri)
26
+ end
27
+
28
+ def get_form(uri)
29
+ response = request(method: :get, uri: uri)
30
+ Form.new(self, response.body)
31
+ end
32
+
33
+ def get_doc(uri)
34
+ response = request(method: :get, uri: uri)
35
+ response&.body || ''
36
+ end
37
+
38
+ %i[get put post delete patch].each do |method|
39
+ define_method(method) do |uri, payload = nil|
40
+ with_resource do
41
+ request(method: method, uri: uri, payload: payload)
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :auth_header
49
+
50
+ def setup(options)
51
+ setup_default_headers options
52
+ setup_basic_auth options
53
+ end
54
+
55
+ def setup_default_headers(options)
56
+ @default_headers = {
57
+ 'Content-Type' => options.fetch(:content_type, MIME_TYPE_JSON)
58
+ }
59
+ return unless token = options[:auth_token]
60
+
61
+ @auth_header = options.fetch(:auth_header, 'X-Auth-Token')
62
+ @default_headers[@auth_header] = token
63
+ end
64
+
65
+ def setup_basic_auth(options)
66
+ @user, @pass = options.slice(:user, :password).values
67
+ @auth_header = options.fetch(:auth_header, 'Authorization') if basic_auth?
68
+ end
69
+
70
+ def basic_auth?
71
+ @user && @pass
72
+ end
73
+
74
+ def with_resource
75
+ response = yield
76
+ Resource.new(self, response.body)
77
+ end
78
+
79
+ def request(method:, uri:, payload: nil, headers: {})
80
+ payload = JSON.generate(payload) if payload&.is_a?(Hash)
81
+ @client.send(method) do |req|
82
+ req.url uri
83
+ req.body = payload if payload
84
+ req.headers.merge! @default_headers.merge(headers)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,118 @@
1
+ require 'json'
2
+ require 'shaf_client/link'
3
+ require 'shaf_client/curie'
4
+
5
+ class ShafClient
6
+ class BaseResource
7
+ attr_reader :attributes, :links, :curies, :embedded_resources
8
+
9
+ def initialize(payload)
10
+ @payload =
11
+ if payload&.is_a? String
12
+ JSON.parse(payload)
13
+ else
14
+ payload
15
+ end
16
+
17
+ parse
18
+ end
19
+
20
+ def to_s
21
+ attributes
22
+ .merge(_links: transform_values_to_s(links))
23
+ .merge(_embedded: transform_values_to_s(embedded_resources))
24
+ .to_s
25
+ end
26
+
27
+ def attribute(key)
28
+ attributes.fetch(key.to_sym)
29
+ end
30
+
31
+ def link(rel)
32
+ links.fetch(rel.to_sym)
33
+ end
34
+
35
+ def curie(rel)
36
+ curies.fetch(rel.to_sym)
37
+ end
38
+
39
+ def embedded(rel)
40
+ embedded_resources.fetch(rel.to_sym)
41
+ end
42
+
43
+ def [](key)
44
+ attributes[key]
45
+ end
46
+
47
+ def actions
48
+ links.keys
49
+ end
50
+
51
+ private
52
+
53
+ def payload
54
+ @payload ||= {}
55
+ end
56
+
57
+ def parse
58
+ @attributes = payload.transform_keys(&:to_sym)
59
+ parse_links
60
+ parse_embedded
61
+ end
62
+
63
+ def parse_links
64
+ links = attributes.delete(:_links) || {}
65
+ @links ||= {}
66
+ @curies ||= {}
67
+
68
+ links.each do |key, value|
69
+ next parse_curies(value) if key == 'curies'
70
+ @links[key.to_sym] = Link.from(value)
71
+ end
72
+ end
73
+
74
+ def parse_curies(curies)
75
+ curies.each do |value|
76
+ curie = Curie.from(value)
77
+ @curies[curie.name.to_sym] = curie
78
+ end
79
+ end
80
+
81
+ def parse_embedded
82
+ embedded = @attributes.delete(:_embedded) || {}
83
+ @embedded_resources ||= {}
84
+
85
+ embedded.each do |key, value|
86
+ @embedded_resources[key.to_sym] =
87
+ if value.is_a? Array
88
+ value.map { |d| BaseResource.new(d) }
89
+ else
90
+ BaseResource.new(value)
91
+ end
92
+ end
93
+ end
94
+
95
+ def method_missing(method_name, *args, &block)
96
+ if attributes.key?(method_name)
97
+ attribute(method_name)
98
+ else
99
+ super
100
+ end
101
+ end
102
+
103
+ def respond_to_missing?(method_name, include_private = false)
104
+ return true if attributes.key?(method_name)
105
+ super
106
+ end
107
+
108
+ def transform_values_to_s(hash)
109
+ hash.transform_values do |value|
110
+ if value.is_a? Array
111
+ value.map(&:to_s)
112
+ else
113
+ value.to_s
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shaf_client/link'
4
+
5
+ class ShafClient
6
+ class Curie < Link
7
+ attr_reader :name
8
+
9
+ def self.from(data)
10
+ if data.is_a? Array
11
+ data.map do |d|
12
+ new(name: data['name'], href: d['href'], templated: d['templated'])
13
+ end
14
+ else
15
+ new(name: data['name'], href: data['href'], templated: data['templated'])
16
+ end
17
+ end
18
+
19
+ def initialize(name:, href:, templated: nil)
20
+ @name = name
21
+ @href = href
22
+ @templated = !!templated
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ require 'json'
2
+ require 'shaf_client/link'
3
+
4
+ class ShafClient
5
+ class Form < Resource
6
+
7
+ def values
8
+ return @values if defined? @values
9
+
10
+ @values = attribute(:fields).each_with_object({}) do |d, v|
11
+ v[d['name'].to_sym] = d['value']
12
+ end
13
+ end
14
+
15
+ def [](key)
16
+ values[key]
17
+ end
18
+
19
+ def []=(key, value)
20
+ values[key] = value
21
+ end
22
+
23
+ def target
24
+ attribute(:href)
25
+ end
26
+
27
+ def http_method
28
+ attribute(:method).downcase.to_sym
29
+ end
30
+
31
+ # def validate; end
32
+
33
+ def submit
34
+ client.send(http_method, target, @values)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShafClient
4
+ class Link
5
+ attr_reader :templated
6
+
7
+ def self.from(data)
8
+ if data.is_a? Array
9
+ data.map { |d| new(href: d['href'], templated: d['templated']) }
10
+ else
11
+ new(href: data['href'], templated: data['templated'])
12
+ end
13
+ end
14
+
15
+ def initialize(href:, templated: nil)
16
+ @href = href
17
+ @templated = !!templated
18
+ end
19
+
20
+ alias templated? templated
21
+
22
+ def href
23
+ @href.dup
24
+ end
25
+
26
+ def resolve_templated(**args)
27
+ return unless templated?
28
+
29
+ args.inject(href) do |uri, (key, value)|
30
+ value = value.to_s.sub(/.+:/, '')
31
+ uri.sub(/{#{key}}/, value)
32
+ end
33
+ end
34
+
35
+ def to_s
36
+ {
37
+ href: href,
38
+ templated: templated?
39
+ }.to_s
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,181 @@
1
+ # FIXME zip payload
2
+
3
+ class ShafClient
4
+ module Middleware
5
+ class Cache
6
+ DEFAULT_THRESHOLD = 10_000
7
+
8
+ Response = Struct.new(:status, :body, :headers, keyword_init: true)
9
+
10
+ module Control
11
+ def cache_size
12
+ Cache.size
13
+ end
14
+
15
+ def clear_cache
16
+ Cache.clear
17
+ end
18
+
19
+ def clear_stale_cache
20
+ Cache.clear_stale
21
+ end
22
+ end
23
+
24
+ class << self
25
+ attr_writer :threshold
26
+
27
+ def inc_request_count
28
+ @request_count ||= 0
29
+ @request_count += 1
30
+ return if (@request_count % 500 != 0)
31
+ clear_stale
32
+ check_threshold
33
+ end
34
+
35
+ def size
36
+ cache.size
37
+ end
38
+
39
+ def clear
40
+ mutex.synchronize do
41
+ @cache = {}
42
+ end
43
+ end
44
+
45
+ def clear_stale
46
+ mutex.synchronize do
47
+ cache.delete_if { |_key, entry| expired? entry }
48
+ end
49
+ end
50
+
51
+ def get(key:, check_expiration: true)
52
+ entry = nil
53
+ mutex.synchronize do
54
+ entry = cache[key.to_sym].dup
55
+ end
56
+ return entry[:payload] if valid?(entry, check_expiration)
57
+ yield if block_given?
58
+ end
59
+
60
+ def get_etag(key:)
61
+ mutex.synchronize do
62
+ cache.dig(key.to_sym, :etag)
63
+ end
64
+ end
65
+
66
+ def store(key:, payload:, etag: nil, expire_at: nil)
67
+ return unless payload && key && (etag || expire_at)
68
+
69
+ mutex.synchronize do
70
+ cache[key.to_sym] = {
71
+ payload: payload,
72
+ etag: etag,
73
+ expire_at: expire_at
74
+ }
75
+ end
76
+ end
77
+
78
+ def threshold
79
+ @threshold ||= DEFAULT_THRESHOLD
80
+ end
81
+
82
+ private
83
+
84
+ def mutex
85
+ @mutex = Mutex.new
86
+ end
87
+
88
+ def cache
89
+ mutex.synchronize do
90
+ @cache ||= {}
91
+ end
92
+ end
93
+
94
+ def valid?(entry, check_expiration = true)
95
+ return false unless entry
96
+ return false unless entry[:payload]
97
+ return true unless check_expiration
98
+ !expired? entry
99
+ end
100
+
101
+ def expired?(entry)
102
+ return true unless entry[:expire_at]
103
+ entry[:expire_at] < Time.now
104
+ end
105
+
106
+ def check_threshold
107
+ return if size < threshold
108
+
109
+ count = 500
110
+ cache.each do |key, _value|
111
+ break if count <= 0
112
+ cache[key] = nil
113
+ count -= 1
114
+ end
115
+ cache.compact!
116
+ end
117
+ end
118
+
119
+ def initialize(app, **options)
120
+ @app = app
121
+ @options = options
122
+ end
123
+
124
+ def call(request_env)
125
+ key = cache_key(request_env)
126
+
127
+ if request_env[:method] == :get
128
+ cached = self.class.get(key: key)
129
+ return Response.new(body: cached, headers: {}) if cached
130
+ end
131
+
132
+ add_etag(request_env, key)
133
+
134
+ @app.call(request_env).on_complete do |response_env|
135
+ add_cached_payload(response_env, key)
136
+ cache_response(response_env, key)
137
+ self.class.inc_request_count
138
+ end
139
+ end
140
+
141
+ def add_etag(env, key = nil)
142
+ key ||= cache_key(env)
143
+ etag = self.class.get_etag(key: key)
144
+ env[:request_headers]['If-None-Match'] = etag if etag
145
+ end
146
+
147
+ def add_cached_payload(response_env, key)
148
+ return if response_env[:status] != 304
149
+ cached = self.class.get(key: key, check_expiration: false)
150
+ response_env[:body] = cached if cached
151
+ end
152
+
153
+ def cache_response(response_env, key)
154
+ etag = response_env[:response_headers]['etag']
155
+ cache_control = response_env[:response_headers]['cache-control']
156
+ expire_at = expiration(cache_control)
157
+ self.class.store(
158
+ key: key,
159
+ payload: response_env[:body],
160
+ etag: etag,
161
+ expire_at: expire_at
162
+ )
163
+ end
164
+
165
+ def cache_key(env)
166
+ :"#{env[:url]}.#{env[:request_headers][auth_header]}"
167
+ end
168
+
169
+ def auth_header
170
+ @options.fetch(:auth_header, 'X-AUTH-TOKEN')
171
+ end
172
+
173
+ def expiration(cache_control)
174
+ return unless cache_control
175
+
176
+ max_age = cache_control[/max-age=\s?(\d+)/, 1]
177
+ Time.now + max_age.to_i if max_age
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,39 @@
1
+ require 'json'
2
+ require 'shaf_client/base_resource'
3
+
4
+ class ShafClient
5
+ class Resource < BaseResource
6
+
7
+ def initialize(client, payload)
8
+ @client = client
9
+ super(payload)
10
+ end
11
+
12
+ %i[get put post delete patch get_form].each do |method|
13
+ define_method(method) do |rel, payload = nil|
14
+ href = link(rel).href
15
+ args = [method, href]
16
+ args << payload unless method.to_s.start_with? 'get'
17
+ client.send(*args)
18
+ end
19
+ end
20
+
21
+ def get_doc(rel:)
22
+ rel = rel.to_s
23
+ curie_name, rel =
24
+ if rel.include? ':'
25
+ rel.split(':')
26
+ else
27
+ [:doc, rel]
28
+ end
29
+
30
+ curie = curie(curie_name)
31
+ uri = curie.resolve_templated(rel: rel)
32
+ client.get_doc(uri)
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :client
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shaf_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sammy Henningsson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDXjCCAkagAwIBAgIBATANBgkqhkiG9w0BAQsFADAsMSowKAYDVQQDDCFzYW1t
14
+ eS5oZW5uaW5nc3Nvbi9EQz1nbWFpbC9EQz1jb20wHhcNMTgwNzE2MTYzNTMzWhcN
15
+ MjAwNzE2MTYzNTMzWjAsMSowKAYDVQQDDCFzYW1teS5oZW5uaW5nc3Nvbi9EQz1n
16
+ bWFpbC9EQz1jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvkgwt
17
+ Zn8obnCelWttayB1BrHBLUK3b8gasRRtbNk4DzAL+EW6sHSVT2u5I7Wws7JQA5VB
18
+ NaK7tgvq3CbDLVRl9NrpDCDx09To08stPxDKi6kst1nkSPAD8g0sQlW3voeQTH98
19
+ 2Z2H3XUegHhu5Z9PU9T/7V/vZUzHPiPg1tX1JUIGOPAjVGsr7SUetbL171zK4S4Y
20
+ tvUkIoNaph+maHttvyYB/ptiZLD53WORKd4Knw3OiJsLtrxr5hhKyQ+txQdF0P8G
21
+ +FlR+Je7B0Ek3yg6fEiJgrdcajYCMo8Oe/GRtoHhi6J3LsYA620P1BSCddZQ2XeL
22
+ y3rzIZyMU0iaT69nAgMBAAGjgYowgYcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAw
23
+ HQYDVR0OBBYEFLpI49QndnmuiDhwy2XtMps7No1pMCYGA1UdEQQfMB2BG3NhbW15
24
+ Lmhlbm5pbmdzc29uQGdtYWlsLmNvbTAmBgNVHRIEHzAdgRtzYW1teS5oZW5uaW5n
25
+ c3NvbkBnbWFpbC5jb20wDQYJKoZIhvcNAQELBQADggEBAFJZqH6sgeiTLvMLpxaK
26
+ K1GaYSCyZbMutf5C3tIAgkmU5UD6B8R0bw6gTM1deM5NJ60LjzqY7rlK3YKDIbTn
27
+ iXMCe9vd4yE/jb5Zi8Wk//9n8CMG68dQpBvmcQ58/M4gTtgsx+lIgXuI5dPQMmRi
28
+ bhWQqqWqxT9X6njjfXFk4xn3z6mfFQNPAYqRVeTHUpXBQZPt+bYXRwHPFZGWkx4l
29
+ BnuuhYKt3CR7YIgvnsQWlTAcU1Ipdayj6UfYqUtlc6cF3CL96NOx7mgZXV8URFiX
30
+ ZMhjYR7sRczGJx+GxGU2EaR0bjRsPVlC4ywtFxoOfRG3WaJcpWGEoAoMJX6Z0bRv
31
+ M40=
32
+ -----END CERTIFICATE-----
33
+ date: 2018-12-09 00:00:00.000000000 Z
34
+ dependencies:
35
+ - !ruby/object:Gem::Dependency
36
+ name: faraday
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.15'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.15'
49
+ description: A HAL client customized for Shaf APIs
50
+ email: sammy.henningsson@gmail.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - lib/shaf_client.rb
56
+ - lib/shaf_client/base_resource.rb
57
+ - lib/shaf_client/curie.rb
58
+ - lib/shaf_client/form.rb
59
+ - lib/shaf_client/link.rb
60
+ - lib/shaf_client/middleware/cache.rb
61
+ - lib/shaf_client/resource.rb
62
+ homepage: https://github.com/sammyhenningsson/shafClient
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '2.5'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.7.6
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: HAL client for Shaf
86
+ test_files: []
metadata.gz.sig ADDED
Binary file