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