hoalife 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 +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +15 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +220 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/hoalife.gemspec +42 -0
- data/lib/hoal_inflector.rb +18 -0
- data/lib/hoalife.rb +47 -0
- data/lib/hoalife/account.rb +18 -0
- data/lib/hoalife/ccr_article.rb +18 -0
- data/lib/hoalife/ccr_violation_type.rb +18 -0
- data/lib/hoalife/client/base.rb +124 -0
- data/lib/hoalife/client/delete.rb +14 -0
- data/lib/hoalife/client/get.rb +14 -0
- data/lib/hoalife/client/post.rb +16 -0
- data/lib/hoalife/client/put.rb +16 -0
- data/lib/hoalife/concern.rb +51 -0
- data/lib/hoalife/error.rb +33 -0
- data/lib/hoalife/escalation.rb +10 -0
- data/lib/hoalife/inspection.rb +6 -0
- data/lib/hoalife/property.rb +18 -0
- data/lib/hoalife/resource.rb +76 -0
- data/lib/hoalife/resources/collection.rb +112 -0
- data/lib/hoalife/resources/has_nested_object.rb +45 -0
- data/lib/hoalife/resources/has_nested_resources.rb +35 -0
- data/lib/hoalife/resources/persistable.rb +87 -0
- data/lib/hoalife/resources/requestable.rb +13 -0
- data/lib/hoalife/upload_url.rb +8 -0
- data/lib/hoalife/user.rb +5 -0
- data/lib/hoalife/version.rb +5 -0
- data/lib/hoalife/violation.rb +24 -0
- metadata +180 -0
data/lib/hoalife.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zeitwerk'
|
4
|
+
require 'hoal_inflector'
|
5
|
+
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.inflector = HOALInflector.new
|
8
|
+
tests = "#{__dir__}/**/*_test.rb"
|
9
|
+
loader.ignore(tests)
|
10
|
+
# loader.log!
|
11
|
+
|
12
|
+
if ENV['DEV']
|
13
|
+
require 'listen'
|
14
|
+
|
15
|
+
loader.enable_reloading
|
16
|
+
Listen.to('lib') do
|
17
|
+
loader.reload
|
18
|
+
end.start
|
19
|
+
end
|
20
|
+
|
21
|
+
loader.setup
|
22
|
+
|
23
|
+
require 'hoalife/error'
|
24
|
+
|
25
|
+
# :nodoc
|
26
|
+
module HOALife
|
27
|
+
@api_base = ENV.fetch('HOALIFE_API_BASE', 'https://api.hoalife.com/api')
|
28
|
+
@api_version = ENV.fetch('HOALIFE_API_VERSION', '1').to_i
|
29
|
+
@api_key = ENV['HOALIFE_API_KEY']
|
30
|
+
@signing_secret = ENV['HOALIFE_SIGNING_SECRET']
|
31
|
+
@sleep_when_rate_limited = 10.0
|
32
|
+
|
33
|
+
class << self
|
34
|
+
attr_accessor :api_key, :signing_secret, :api_base, :api_version,
|
35
|
+
:sleep_when_rate_limited
|
36
|
+
|
37
|
+
# Support configuring with a block
|
38
|
+
# HOALife.config do |config|
|
39
|
+
# config.api_key = "foo"
|
40
|
+
# end
|
41
|
+
# HOALife.api_key
|
42
|
+
# => "foo"
|
43
|
+
def config
|
44
|
+
yield self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :nodoc
|
4
|
+
class HOALife::Account < HOALife::Resource
|
5
|
+
include HOALife::Resources::Persistable
|
6
|
+
|
7
|
+
self.base_path = '/accounts'
|
8
|
+
|
9
|
+
def as_json
|
10
|
+
h = super
|
11
|
+
|
12
|
+
h.dig('data', 'relationships').merge!(
|
13
|
+
'parent' => { 'data' => { 'id' => parent_id } }
|
14
|
+
)
|
15
|
+
|
16
|
+
h
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :nodoc
|
4
|
+
class HOALife::CCRArticle < HOALife::Resource
|
5
|
+
include HOALife::Resources::Persistable
|
6
|
+
|
7
|
+
self.base_path = '/ccr_articles'
|
8
|
+
|
9
|
+
def as_json
|
10
|
+
h = super
|
11
|
+
|
12
|
+
h.dig('data', 'relationships').merge!(
|
13
|
+
'account' => { 'data' => { 'id' => account_id } }
|
14
|
+
)
|
15
|
+
|
16
|
+
h
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :nodoc
|
4
|
+
class HOALife::CCRViolationType < HOALife::Resource
|
5
|
+
include HOALife::Resources::Persistable
|
6
|
+
|
7
|
+
self.base_path = '/ccr_violation_types'
|
8
|
+
|
9
|
+
def as_json
|
10
|
+
h = super
|
11
|
+
|
12
|
+
h.dig('data', 'relationships').merge!(
|
13
|
+
'ccr_article' => { 'data' => { 'id' => ccr_article_id } }
|
14
|
+
)
|
15
|
+
|
16
|
+
h
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'openssl'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
# Base class for all HTTP requests
|
8
|
+
# Handles the implementation specific code of the API
|
9
|
+
class HOALife::Client::Base
|
10
|
+
def initialize(url, body = nil)
|
11
|
+
@url = url
|
12
|
+
@body = body
|
13
|
+
end
|
14
|
+
|
15
|
+
def status
|
16
|
+
@status ||= response.code.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def json
|
20
|
+
@json ||= JSON.parse(response.body)
|
21
|
+
end
|
22
|
+
|
23
|
+
def response
|
24
|
+
@response ||= validate_response!
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def request!
|
30
|
+
raise 'Not implemented'
|
31
|
+
end
|
32
|
+
|
33
|
+
def uri
|
34
|
+
@uri ||= URI(@url)
|
35
|
+
end
|
36
|
+
|
37
|
+
def request_headers
|
38
|
+
{
|
39
|
+
'Authorization' => authorization_header,
|
40
|
+
'ACCEPT' => api_version,
|
41
|
+
'Content-Type' => 'application/vnd.api+json'
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_response!
|
46
|
+
response = request!
|
47
|
+
|
48
|
+
verify_successful_response!(response)
|
49
|
+
|
50
|
+
verify_signature!(response) unless HOALife.signing_secret.nil?
|
51
|
+
|
52
|
+
response
|
53
|
+
end
|
54
|
+
|
55
|
+
# rubocop:disable Metrics/MethodLength
|
56
|
+
def verify_successful_response!(resp)
|
57
|
+
headers = resp.each_header.to_h
|
58
|
+
body = resp.body
|
59
|
+
code = resp.code.to_i
|
60
|
+
|
61
|
+
case code
|
62
|
+
when 400
|
63
|
+
raise HOALife::BadRequestError.new(code, headers, body)
|
64
|
+
when 401..403
|
65
|
+
auth_error(headers, body, code)
|
66
|
+
when 404..429
|
67
|
+
http_error(headers, body, code)
|
68
|
+
when 400..600
|
69
|
+
raise HOALife::HTTPError.new(code, headers, generic_error(resp))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
# rubocop:enable Metrics/MethodLength
|
73
|
+
|
74
|
+
def auth_error(headers, body, code)
|
75
|
+
case code
|
76
|
+
when 401
|
77
|
+
raise HOALife::UnauthorizedError.new(code, headers, body)
|
78
|
+
when 403
|
79
|
+
raise HOALife::ForbiddenError.new(code, headers, body)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def http_error(headers, body, code)
|
84
|
+
case code
|
85
|
+
when 404
|
86
|
+
raise HOALife::NotFoundError.new(code, headers, body)
|
87
|
+
when 429
|
88
|
+
raise HOALife::RateLimitError.new(code, headers, body)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def verify_signature!(resp)
|
93
|
+
digest = OpenSSL::Digest.new('sha256')
|
94
|
+
signature = OpenSSL::HMAC.hexdigest(
|
95
|
+
digest, HOALife.signing_secret, resp.body
|
96
|
+
)
|
97
|
+
|
98
|
+
raise HOALife::SigningMissmatchError if signature != resp['X-Signature']
|
99
|
+
end
|
100
|
+
|
101
|
+
def api_version
|
102
|
+
return nil if HOALife.api_version.nil?
|
103
|
+
|
104
|
+
"version=#{HOALife.api_version}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def authorization_header
|
108
|
+
raise HOALife::Error, 'No API Key specified' if HOALife.api_key.nil?
|
109
|
+
|
110
|
+
"Token #{HOALife.api_key}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def generic_error(resp)
|
114
|
+
JSON.parse(resp.body)
|
115
|
+
rescue JSON::ParserError
|
116
|
+
{ 'data' => {
|
117
|
+
'id' => resp['X-Request-Id'], 'type' => 'error',
|
118
|
+
'attributes' => {
|
119
|
+
'id' => resp['X-Request-Id'], 'title' => 'HTTP Error',
|
120
|
+
'status' => resp.code.to_i, 'detail' => resp.body
|
121
|
+
}
|
122
|
+
} }
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# HTTP DELETE requests
|
4
|
+
class HOALife::Client::Delete < HOALife::Client::Base
|
5
|
+
private
|
6
|
+
|
7
|
+
def request!
|
8
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
9
|
+
req = Net::HTTP::Delete.new(uri, request_headers)
|
10
|
+
|
11
|
+
http.request(req)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# HTTP Get requests
|
4
|
+
class HOALife::Client::Get < HOALife::Client::Base
|
5
|
+
private
|
6
|
+
|
7
|
+
def request!
|
8
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
9
|
+
req = Net::HTTP::Get.new(uri, request_headers)
|
10
|
+
|
11
|
+
http.request(req)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# HTTP POST requests
|
4
|
+
class HOALife::Client::Post < HOALife::Client::Base
|
5
|
+
private
|
6
|
+
|
7
|
+
def request!
|
8
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
9
|
+
req = Net::HTTP::Post.new(uri, request_headers)
|
10
|
+
|
11
|
+
req.body = @body
|
12
|
+
|
13
|
+
http.request(req)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# HTTP PUT requests
|
4
|
+
class HOALife::Client::Put < HOALife::Client::Base
|
5
|
+
private
|
6
|
+
|
7
|
+
def request!
|
8
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
9
|
+
req = Net::HTTP::Put.new(uri, request_headers)
|
10
|
+
|
11
|
+
req.body = @body
|
12
|
+
|
13
|
+
http.request(req)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Rails ActiveSupport::Concern
|
4
|
+
# https://github.com/rails/rails/blob/66cabeda2c46c582d19738e1318be8d59584cc5b/activesupport/lib/active_support/concern.rb#L163
|
5
|
+
module HOALife::Concern
|
6
|
+
class MultipleIncludedBlocks < StandardError #:nodoc:
|
7
|
+
def initialize
|
8
|
+
super "Cannot define multiple 'included' blocks for a Concern"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.extended(base) #:nodoc:
|
13
|
+
base.instance_variable_set(:@_dependencies, [])
|
14
|
+
end
|
15
|
+
|
16
|
+
def append_features(base) #:nodoc:
|
17
|
+
if base.instance_variable_defined?(:@_dependencies)
|
18
|
+
base.instance_variable_get(:@_dependencies) << self
|
19
|
+
false
|
20
|
+
else
|
21
|
+
return false if base < self
|
22
|
+
|
23
|
+
@_dependencies.each { |dep| base.include(dep) }
|
24
|
+
super
|
25
|
+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
26
|
+
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def included(base = nil, &block)
|
31
|
+
if base.nil?
|
32
|
+
if instance_variable_defined?(:@_included_block)
|
33
|
+
if @_included_block.source_location != block.source_location
|
34
|
+
raise MultipleIncludedBlocks
|
35
|
+
end
|
36
|
+
else
|
37
|
+
@_included_block = block
|
38
|
+
end
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def class_methods(&class_methods_module_definition)
|
45
|
+
mod = const_defined?(:ClassMethods, false) ?
|
46
|
+
const_get(:ClassMethods) :
|
47
|
+
const_set(:ClassMethods, Module.new)
|
48
|
+
|
49
|
+
mod.module_eval(&class_methods_module_definition)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HOALife
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# HTTP Errors
|
7
|
+
class HTTPError < Error
|
8
|
+
attr_reader :status, :headers, :details
|
9
|
+
|
10
|
+
def initialize(status, headers, details)
|
11
|
+
@status = status
|
12
|
+
@headers = headers
|
13
|
+
|
14
|
+
begin
|
15
|
+
@details = JSON.parse(details)
|
16
|
+
rescue JSON::ParserError, TypeError
|
17
|
+
@details = details
|
18
|
+
end
|
19
|
+
|
20
|
+
super(status)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class BadRequestError < HTTPError; end
|
25
|
+
class UnauthorizedError < HTTPError; end
|
26
|
+
class ForbiddenError < HTTPError; end
|
27
|
+
class NotFoundError < HTTPError; end
|
28
|
+
class RateLimitError < HTTPError; end
|
29
|
+
|
30
|
+
class SigningMissmatchError < Error; end
|
31
|
+
|
32
|
+
class UndefinedResourceError < Error; end
|
33
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :nodoc
|
4
|
+
class HOALife::Property < HOALife::Resource
|
5
|
+
include HOALife::Resources::Persistable
|
6
|
+
|
7
|
+
self.base_path = '/properties'
|
8
|
+
|
9
|
+
def as_json
|
10
|
+
h = super
|
11
|
+
|
12
|
+
h.dig('data', 'relationships').merge!(
|
13
|
+
'account' => { 'data' => { 'id' => account_id } }
|
14
|
+
)
|
15
|
+
|
16
|
+
h
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
require 'json'
|
5
|
+
require 'time'
|
6
|
+
require 'forwardable'
|
7
|
+
|
8
|
+
# :nodoc
|
9
|
+
class HOALife::Resource < OpenStruct
|
10
|
+
class << self
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_accessor :base_path
|
14
|
+
def_delegators :resource_collection, :all, :first, :last, :where, :order,
|
15
|
+
:total_pages, :current_page, :total, :count, :size, :reload
|
16
|
+
|
17
|
+
def new(obj = {}, relationships = {})
|
18
|
+
return super(obj, relationships) unless obj['type']
|
19
|
+
|
20
|
+
camelized = HOALInflector.new.camelize(obj['type'], nil)
|
21
|
+
|
22
|
+
begin
|
23
|
+
klass = Object.const_get("HOALife::#{camelized}")
|
24
|
+
rescue NameError
|
25
|
+
raise HOALife::UndefinedResourceError,
|
26
|
+
"HOALife::#{camelized} is not defined"
|
27
|
+
end
|
28
|
+
|
29
|
+
klass.new(obj['attributes'], obj['relationships'])
|
30
|
+
end
|
31
|
+
|
32
|
+
def resource_collection
|
33
|
+
@resource_collection ||= HOALife::Resources::Collection.new(
|
34
|
+
HOALife.api_base + base_path
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(obj = {}, _relationships = {})
|
40
|
+
@obj = cast_attrs(obj)
|
41
|
+
|
42
|
+
super(obj)
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_json
|
46
|
+
h = {
|
47
|
+
'data' => {
|
48
|
+
'attributes' => {},
|
49
|
+
'relationships' => {}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
each_pair do |k, _v|
|
54
|
+
h['data']['attributes'][k] = send(k)
|
55
|
+
end
|
56
|
+
|
57
|
+
h
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_json(*_args)
|
61
|
+
JSON.generate as_json
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# rubocop:disable Style/RescueModifier
|
67
|
+
def cast_attrs(obj)
|
68
|
+
obj.each do |k, v|
|
69
|
+
next unless k.match?(/_at$/)
|
70
|
+
|
71
|
+
time = Time.parse(v) rescue nil
|
72
|
+
obj[k] = time if time
|
73
|
+
end
|
74
|
+
end
|
75
|
+
# rubocop:enable Style/RescueModifier
|
76
|
+
end
|