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.
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