namely 0.0.1
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 +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +51 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +125 -0
- data/Rakefile +16 -0
- data/lib/namely.rb +114 -0
- data/lib/namely/authenticator.rb +164 -0
- data/lib/namely/country.rb +9 -0
- data/lib/namely/currency_type.rb +9 -0
- data/lib/namely/event.rb +9 -0
- data/lib/namely/exceptions.rb +13 -0
- data/lib/namely/field.rb +9 -0
- data/lib/namely/job_tier.rb +9 -0
- data/lib/namely/profile.rb +13 -0
- data/lib/namely/report.rb +9 -0
- data/lib/namely/resource_gateway.rb +81 -0
- data/lib/namely/restful_model.rb +150 -0
- data/lib/namely/version.rb +3 -0
- data/namely.gemspec +30 -0
- data/spec/fixtures/vcr_cassettes/country_head.yml +51 -0
- data/spec/fixtures/vcr_cassettes/country_head_missing.yml +50 -0
- data/spec/fixtures/vcr_cassettes/country_index.yml +121 -0
- data/spec/fixtures/vcr_cassettes/country_show.yml +68 -0
- data/spec/fixtures/vcr_cassettes/country_show_missing.yml +54 -0
- data/spec/fixtures/vcr_cassettes/currencytype_index.yml +64 -0
- data/spec/fixtures/vcr_cassettes/event_head.yml +51 -0
- data/spec/fixtures/vcr_cassettes/event_head_missing.yml +50 -0
- data/spec/fixtures/vcr_cassettes/event_index.yml +88 -0
- data/spec/fixtures/vcr_cassettes/event_show.yml +62 -0
- data/spec/fixtures/vcr_cassettes/event_show_missing.yml +54 -0
- data/spec/fixtures/vcr_cassettes/field_index.yml +207 -0
- data/spec/fixtures/vcr_cassettes/jobtier_index.yml +103 -0
- data/spec/fixtures/vcr_cassettes/profile_create.yml +85 -0
- data/spec/fixtures/vcr_cassettes/profile_create_failed.yml +54 -0
- data/spec/fixtures/vcr_cassettes/profile_head.yml +51 -0
- data/spec/fixtures/vcr_cassettes/profile_head_missing.yml +50 -0
- data/spec/fixtures/vcr_cassettes/profile_index.yml +979 -0
- data/spec/fixtures/vcr_cassettes/profile_show.yml +91 -0
- data/spec/fixtures/vcr_cassettes/profile_show_missing.yml +54 -0
- data/spec/fixtures/vcr_cassettes/profile_show_updated.yml +91 -0
- data/spec/fixtures/vcr_cassettes/profile_update.yml +95 -0
- data/spec/fixtures/vcr_cassettes/profile_update_revert.yml +95 -0
- data/spec/fixtures/vcr_cassettes/report_head.yml +51 -0
- data/spec/fixtures/vcr_cassettes/report_head_missing.yml +50 -0
- data/spec/fixtures/vcr_cassettes/report_show.yml +185 -0
- data/spec/fixtures/vcr_cassettes/report_show_missing.yml +54 -0
- data/spec/fixtures/vcr_cassettes/token.yml +57 -0
- data/spec/fixtures/vcr_cassettes/token_refresh.yml +57 -0
- data/spec/namely/authenticator_spec.rb +143 -0
- data/spec/namely/configuration_spec.rb +33 -0
- data/spec/namely/country_spec.rb +11 -0
- data/spec/namely/currency_type_spec.rb +5 -0
- data/spec/namely/event_spec.rb +11 -0
- data/spec/namely/field_spec.rb +5 -0
- data/spec/namely/job_tier_spec.rb +5 -0
- data/spec/namely/profile_spec.rb +25 -0
- data/spec/namely/report_spec.rb +8 -0
- data/spec/namely/resource_gateway_spec.rb +93 -0
- data/spec/shared_examples/a_model_with_a_create_action.rb +24 -0
- data/spec/shared_examples/a_model_with_a_show_action.rb +38 -0
- data/spec/shared_examples/a_model_with_an_index_action.rb +17 -0
- data/spec/shared_examples/a_model_with_an_update_action.rb +37 -0
- data/spec/spec_helper.rb +49 -0
- metadata +280 -0
data/lib/namely/event.rb
ADDED
data/lib/namely/field.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Namely
|
2
|
+
class ResourceGateway
|
3
|
+
def initialize(options)
|
4
|
+
@access_token = options.fetch(:access_token)
|
5
|
+
@endpoint = options.fetch(:endpoint)
|
6
|
+
@resource_name = options.fetch(:resource_name)
|
7
|
+
@subdomain = options.fetch(:subdomain)
|
8
|
+
end
|
9
|
+
|
10
|
+
def json_index
|
11
|
+
get("/#{endpoint}", limit: :all)[resource_name]
|
12
|
+
end
|
13
|
+
|
14
|
+
def json_show(id)
|
15
|
+
get("/#{endpoint}/#{id}")[resource_name].first
|
16
|
+
end
|
17
|
+
|
18
|
+
def show_head(id)
|
19
|
+
head("/#{endpoint}/#{id}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def create(attributes)
|
23
|
+
response = post(
|
24
|
+
"/#{endpoint}",
|
25
|
+
endpoint => [attributes]
|
26
|
+
)
|
27
|
+
extract_id(response)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(id, changes)
|
31
|
+
put("/#{endpoint}/#{id}", endpoint => [changes])
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :access_token, :endpoint, :resource_name, :subdomain
|
37
|
+
|
38
|
+
def url(path)
|
39
|
+
"https://#{subdomain}.namely.com/api/v1#{path}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_id(response)
|
43
|
+
JSON.parse(response)[endpoint].first["id"]
|
44
|
+
rescue StandardError => e
|
45
|
+
raise(
|
46
|
+
FailedRequestError,
|
47
|
+
"Couldn't parse \"id\" from response: #{e.message}"
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def get(path, params = {})
|
52
|
+
params.merge!(access_token: access_token)
|
53
|
+
JSON.parse(RestClient.get(url(path), accept: :json, params: params))
|
54
|
+
end
|
55
|
+
|
56
|
+
def head(path, params = {})
|
57
|
+
params.merge!(access_token: access_token)
|
58
|
+
RestClient.head(url(path), accept: :json, params: params)
|
59
|
+
end
|
60
|
+
|
61
|
+
def post(path, params)
|
62
|
+
params.merge!(access_token: access_token)
|
63
|
+
RestClient.post(
|
64
|
+
url(path),
|
65
|
+
params.to_json,
|
66
|
+
accept: :json,
|
67
|
+
content_type: :json,
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def put(path, params)
|
72
|
+
params.merge!(access_token: access_token)
|
73
|
+
RestClient.put(
|
74
|
+
url(path),
|
75
|
+
params.to_json,
|
76
|
+
accept: :json,
|
77
|
+
content_type: :json
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Namely
|
2
|
+
# @abstract
|
3
|
+
class RestfulModel < OpenStruct
|
4
|
+
# Fetch a model from the server by its ID.
|
5
|
+
#
|
6
|
+
# @param [#to_s] id
|
7
|
+
#
|
8
|
+
# @raise [NoSuchModelError] if the model wasn't found.
|
9
|
+
#
|
10
|
+
# @return [RestfulModel]
|
11
|
+
def self.find(id)
|
12
|
+
new(resource_gateway.json_show(id))
|
13
|
+
rescue RestClient::ResourceNotFound
|
14
|
+
raise NoSuchModelError, "Can't find a #{name} with id \"#{id}\""
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns true if a model with this ID exists, false otherwise.
|
18
|
+
#
|
19
|
+
# @param [#to_s] id
|
20
|
+
#
|
21
|
+
# @return [Boolean]
|
22
|
+
def self.exists?(id)
|
23
|
+
resource_gateway.show_head(id)
|
24
|
+
true
|
25
|
+
rescue RestClient::ResourceNotFound
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return every instance of this model.
|
30
|
+
#
|
31
|
+
# A model might have quite a few instances. If this is the case,
|
32
|
+
# the query may take some time (several seconds) and the resulting
|
33
|
+
# array may be very large.
|
34
|
+
#
|
35
|
+
# @return [Array<RestfulModel>]
|
36
|
+
def self.all
|
37
|
+
resource_gateway.json_index.map { |model| new(model) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a new model on the server with the given attributes.
|
41
|
+
#
|
42
|
+
# @param [Hash] attributes the attributes of the model being created.
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# Profile.create!(
|
46
|
+
# first_name: "Beardsly",
|
47
|
+
# last_name: "McDog",
|
48
|
+
# email: "beardsly@namely.com"
|
49
|
+
# )
|
50
|
+
#
|
51
|
+
# @return [RestfulModel] the created model.
|
52
|
+
def self.create!(attributes)
|
53
|
+
new(attributes).save!
|
54
|
+
end
|
55
|
+
|
56
|
+
# Update the attributes of this model. Assign the attributes
|
57
|
+
# according to the hash, then persist those changes on the server.
|
58
|
+
#
|
59
|
+
# @param [Hash] attributes the attributes to be updated on the model.
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# my_profile.update(
|
63
|
+
# middle_name: "Ludwig"
|
64
|
+
# )
|
65
|
+
#
|
66
|
+
# @raise [FailedRequestError] if the request failed for any reason.
|
67
|
+
#
|
68
|
+
# @return [RestfulModel] the updated model.
|
69
|
+
def update(attributes)
|
70
|
+
attributes.each do |key, value|
|
71
|
+
self[key] = value
|
72
|
+
end
|
73
|
+
|
74
|
+
persist_model_changes(attributes)
|
75
|
+
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
# Try to persist the current object, either by creating a new
|
80
|
+
# object on the server or by updating an existing one. Raise an
|
81
|
+
# error if the object can't be saved.
|
82
|
+
#
|
83
|
+
# @raise [FailedRequestError] if the request failed for any reason.
|
84
|
+
#
|
85
|
+
# @return [RestfulModel] the model itself, if saving succeeded.
|
86
|
+
def save!
|
87
|
+
if persisted?
|
88
|
+
update(to_h)
|
89
|
+
else
|
90
|
+
self.id = resource_gateway.create(to_h)
|
91
|
+
end
|
92
|
+
self
|
93
|
+
rescue RestClient::Exception => e
|
94
|
+
raise FailedRequestError, e.message
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return true if the model exists (in some state) on the server.
|
98
|
+
#
|
99
|
+
# @return [Boolean]
|
100
|
+
def persisted?
|
101
|
+
!!id
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def resource_gateway
|
107
|
+
self.class.resource_gateway
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.resource_gateway
|
111
|
+
Namely.resource_gateway(resource_name, endpoint)
|
112
|
+
end
|
113
|
+
|
114
|
+
def resource_gateway
|
115
|
+
self.class.resource_gateway
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.endpoint
|
119
|
+
raise(
|
120
|
+
NotImplementedError,
|
121
|
+
"Namely::Model subclasses must define an `.endpoint` "\
|
122
|
+
"class method that returns their path in the API."
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.resource_name
|
127
|
+
endpoint.split("/").last
|
128
|
+
end
|
129
|
+
|
130
|
+
# The update action for certain models requires populating certain
|
131
|
+
# fields. Without populating these fields, an update with
|
132
|
+
# fail. This method is intended to be overwritten by subclasses
|
133
|
+
# with required keys.
|
134
|
+
#
|
135
|
+
# @return [Array<Symbol>]
|
136
|
+
def required_keys_for_update
|
137
|
+
[]
|
138
|
+
end
|
139
|
+
|
140
|
+
def required_attributes_for_update
|
141
|
+
to_h.select { |key, _| required_keys_for_update.include?(key) }
|
142
|
+
end
|
143
|
+
|
144
|
+
def persist_model_changes(changed_attributes)
|
145
|
+
resource_gateway.update(id, changed_attributes.merge(required_attributes_for_update))
|
146
|
+
rescue RestClient::Exception => e
|
147
|
+
raise FailedRequestError, e.message
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/namely.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "namely/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "namely"
|
8
|
+
spec.version = Namely::VERSION
|
9
|
+
spec.authors = ["Harry Schwartz"]
|
10
|
+
spec.email = ["harry@thoughtbot.com"]
|
11
|
+
spec.summary = "Wraps the Namely HTTP API in lovely Ruby."
|
12
|
+
spec.homepage = "https://github.com/namely/ruby-client"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "backports"
|
21
|
+
spec.add_dependency "rest_client"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "dotenv"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "vcr"
|
28
|
+
spec.add_development_dependency "webmock"
|
29
|
+
spec.add_development_dependency "yard"
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: head
|
5
|
+
uri: https://<TEST_SUBDOMAIN>.namely.com/api/v1/countries/US?access_token=<TEST_ACCESS_TOKEN>
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept:
|
11
|
+
- application/json
|
12
|
+
Accept-Encoding:
|
13
|
+
- gzip, deflate
|
14
|
+
User-Agent:
|
15
|
+
- Ruby
|
16
|
+
response:
|
17
|
+
status:
|
18
|
+
code: 200
|
19
|
+
message: OK
|
20
|
+
headers:
|
21
|
+
Cache-Control:
|
22
|
+
- max-age=0, private, must-revalidate
|
23
|
+
Content-Encoding:
|
24
|
+
- gzip
|
25
|
+
Content-Type:
|
26
|
+
- application/json; charset=utf-8
|
27
|
+
Date:
|
28
|
+
- Tue, 04 Nov 2014 16:34:36 GMT
|
29
|
+
Server:
|
30
|
+
- nginx/1.6.2
|
31
|
+
Status:
|
32
|
+
- 200 OK
|
33
|
+
Strict-Transport-Security:
|
34
|
+
- max-age=31536000
|
35
|
+
- max-age=31536000; includeSubDomains;
|
36
|
+
Vary:
|
37
|
+
- Accept-Encoding
|
38
|
+
X-Rack-Cache:
|
39
|
+
- miss
|
40
|
+
X-Request-Id:
|
41
|
+
- 12b71052-a0c4-4776-86ee-417d8f3b87dd
|
42
|
+
X-Runtime:
|
43
|
+
- '0.021096'
|
44
|
+
Connection:
|
45
|
+
- keep-alive
|
46
|
+
body:
|
47
|
+
encoding: UTF-8
|
48
|
+
string: ''
|
49
|
+
http_version:
|
50
|
+
recorded_at: Tue, 04 Nov 2014 16:34:36 GMT
|
51
|
+
recorded_with: VCR 2.9.3
|
@@ -0,0 +1,50 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: head
|
5
|
+
uri: https://<TEST_SUBDOMAIN>.namely.com/api/v1/countries/this-is-almost-certainly-not-the-id-of-any-model?access_token=<TEST_ACCESS_TOKEN>
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept:
|
11
|
+
- application/json
|
12
|
+
Accept-Encoding:
|
13
|
+
- gzip, deflate
|
14
|
+
User-Agent:
|
15
|
+
- Ruby
|
16
|
+
response:
|
17
|
+
status:
|
18
|
+
code: 404
|
19
|
+
message: Not Found
|
20
|
+
headers:
|
21
|
+
Cache-Control:
|
22
|
+
- no-cache
|
23
|
+
Content-Encoding:
|
24
|
+
- gzip
|
25
|
+
Content-Type:
|
26
|
+
- application/json; charset=utf-8
|
27
|
+
Date:
|
28
|
+
- Tue, 04 Nov 2014 16:34:36 GMT
|
29
|
+
Server:
|
30
|
+
- nginx
|
31
|
+
Status:
|
32
|
+
- 404 Not Found
|
33
|
+
Strict-Transport-Security:
|
34
|
+
- max-age=31536000
|
35
|
+
Vary:
|
36
|
+
- Accept-Encoding
|
37
|
+
X-Rack-Cache:
|
38
|
+
- miss
|
39
|
+
X-Request-Id:
|
40
|
+
- 15c2c8fd-5244-48b7-9f18-040318b7e9ac
|
41
|
+
X-Runtime:
|
42
|
+
- '0.026183'
|
43
|
+
Connection:
|
44
|
+
- keep-alive
|
45
|
+
body:
|
46
|
+
encoding: UTF-8
|
47
|
+
string: ''
|
48
|
+
http_version:
|
49
|
+
recorded_at: Tue, 04 Nov 2014 16:34:36 GMT
|
50
|
+
recorded_with: VCR 2.9.3
|