shaf_client 0.1.0

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