namely 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +51 -0
  6. data/LICENSE +22 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +125 -0
  9. data/Rakefile +16 -0
  10. data/lib/namely.rb +114 -0
  11. data/lib/namely/authenticator.rb +164 -0
  12. data/lib/namely/country.rb +9 -0
  13. data/lib/namely/currency_type.rb +9 -0
  14. data/lib/namely/event.rb +9 -0
  15. data/lib/namely/exceptions.rb +13 -0
  16. data/lib/namely/field.rb +9 -0
  17. data/lib/namely/job_tier.rb +9 -0
  18. data/lib/namely/profile.rb +13 -0
  19. data/lib/namely/report.rb +9 -0
  20. data/lib/namely/resource_gateway.rb +81 -0
  21. data/lib/namely/restful_model.rb +150 -0
  22. data/lib/namely/version.rb +3 -0
  23. data/namely.gemspec +30 -0
  24. data/spec/fixtures/vcr_cassettes/country_head.yml +51 -0
  25. data/spec/fixtures/vcr_cassettes/country_head_missing.yml +50 -0
  26. data/spec/fixtures/vcr_cassettes/country_index.yml +121 -0
  27. data/spec/fixtures/vcr_cassettes/country_show.yml +68 -0
  28. data/spec/fixtures/vcr_cassettes/country_show_missing.yml +54 -0
  29. data/spec/fixtures/vcr_cassettes/currencytype_index.yml +64 -0
  30. data/spec/fixtures/vcr_cassettes/event_head.yml +51 -0
  31. data/spec/fixtures/vcr_cassettes/event_head_missing.yml +50 -0
  32. data/spec/fixtures/vcr_cassettes/event_index.yml +88 -0
  33. data/spec/fixtures/vcr_cassettes/event_show.yml +62 -0
  34. data/spec/fixtures/vcr_cassettes/event_show_missing.yml +54 -0
  35. data/spec/fixtures/vcr_cassettes/field_index.yml +207 -0
  36. data/spec/fixtures/vcr_cassettes/jobtier_index.yml +103 -0
  37. data/spec/fixtures/vcr_cassettes/profile_create.yml +85 -0
  38. data/spec/fixtures/vcr_cassettes/profile_create_failed.yml +54 -0
  39. data/spec/fixtures/vcr_cassettes/profile_head.yml +51 -0
  40. data/spec/fixtures/vcr_cassettes/profile_head_missing.yml +50 -0
  41. data/spec/fixtures/vcr_cassettes/profile_index.yml +979 -0
  42. data/spec/fixtures/vcr_cassettes/profile_show.yml +91 -0
  43. data/spec/fixtures/vcr_cassettes/profile_show_missing.yml +54 -0
  44. data/spec/fixtures/vcr_cassettes/profile_show_updated.yml +91 -0
  45. data/spec/fixtures/vcr_cassettes/profile_update.yml +95 -0
  46. data/spec/fixtures/vcr_cassettes/profile_update_revert.yml +95 -0
  47. data/spec/fixtures/vcr_cassettes/report_head.yml +51 -0
  48. data/spec/fixtures/vcr_cassettes/report_head_missing.yml +50 -0
  49. data/spec/fixtures/vcr_cassettes/report_show.yml +185 -0
  50. data/spec/fixtures/vcr_cassettes/report_show_missing.yml +54 -0
  51. data/spec/fixtures/vcr_cassettes/token.yml +57 -0
  52. data/spec/fixtures/vcr_cassettes/token_refresh.yml +57 -0
  53. data/spec/namely/authenticator_spec.rb +143 -0
  54. data/spec/namely/configuration_spec.rb +33 -0
  55. data/spec/namely/country_spec.rb +11 -0
  56. data/spec/namely/currency_type_spec.rb +5 -0
  57. data/spec/namely/event_spec.rb +11 -0
  58. data/spec/namely/field_spec.rb +5 -0
  59. data/spec/namely/job_tier_spec.rb +5 -0
  60. data/spec/namely/profile_spec.rb +25 -0
  61. data/spec/namely/report_spec.rb +8 -0
  62. data/spec/namely/resource_gateway_spec.rb +93 -0
  63. data/spec/shared_examples/a_model_with_a_create_action.rb +24 -0
  64. data/spec/shared_examples/a_model_with_a_show_action.rb +38 -0
  65. data/spec/shared_examples/a_model_with_an_index_action.rb +17 -0
  66. data/spec/shared_examples/a_model_with_an_update_action.rb +37 -0
  67. data/spec/spec_helper.rb +49 -0
  68. metadata +280 -0
@@ -0,0 +1,9 @@
1
+ module Namely
2
+ class Country < RestfulModel
3
+ def self.endpoint
4
+ "countries"
5
+ end
6
+
7
+ private_class_method :endpoint
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Namely
2
+ class CurrencyType < RestfulModel
3
+ def self.endpoint
4
+ "currency_types"
5
+ end
6
+
7
+ private_class_method :endpoint
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Namely
2
+ class Event < RestfulModel
3
+ def self.endpoint
4
+ "events"
5
+ end
6
+
7
+ private_class_method :endpoint
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Namely
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ImproperlyConfiguredError < Error
6
+ end
7
+
8
+ class NoSuchModelError < Error
9
+ end
10
+
11
+ class FailedRequestError < Error
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module Namely
2
+ class Field < RestfulModel
3
+ def self.endpoint
4
+ "profiles/fields"
5
+ end
6
+
7
+ private_class_method :endpoint, :resource_name
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Namely
2
+ class JobTier < RestfulModel
3
+ def self.endpoint
4
+ "job_tiers"
5
+ end
6
+
7
+ private_class_method :endpoint
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Namely
2
+ class Profile < RestfulModel
3
+ def self.endpoint
4
+ "profiles"
5
+ end
6
+
7
+ def required_keys_for_update
8
+ [:email]
9
+ end
10
+
11
+ private_class_method :endpoint, :resource_name
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module Namely
2
+ class Report < RestfulModel
3
+ def self.endpoint
4
+ "reports"
5
+ end
6
+
7
+ private_class_method :endpoint
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Namely
2
+ VERSION = "0.0.1"
3
+ end
@@ -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