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