scalingo 3.0.0.beta.1 → 3.0.0.beta.2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/CHANGELOG.md +13 -1
  4. data/README.md +41 -27
  5. data/lib/scalingo.rb +1 -1
  6. data/lib/scalingo/api/client.rb +34 -17
  7. data/lib/scalingo/api/endpoint.rb +1 -1
  8. data/lib/scalingo/api/response.rb +21 -32
  9. data/lib/scalingo/bearer_token.rb +11 -5
  10. data/lib/scalingo/billing.rb +11 -0
  11. data/lib/scalingo/billing/profile.rb +46 -0
  12. data/lib/scalingo/client.rb +52 -26
  13. data/lib/scalingo/configuration.rb +98 -0
  14. data/lib/scalingo/regional/addons.rb +2 -2
  15. data/lib/scalingo/regional/containers.rb +1 -1
  16. data/lib/scalingo/regional/events.rb +2 -2
  17. data/lib/scalingo/regional/logs.rb +1 -1
  18. data/lib/scalingo/regional/metrics.rb +1 -1
  19. data/lib/scalingo/regional/notifiers.rb +1 -1
  20. data/lib/scalingo/regional/operations.rb +9 -1
  21. data/lib/scalingo/version.rb +1 -1
  22. data/samples/billing/profile/_meta.json +23 -0
  23. data/samples/billing/profile/create-201.json +50 -0
  24. data/samples/billing/profile/create-400.json +27 -0
  25. data/samples/billing/profile/create-422.json +44 -0
  26. data/samples/billing/profile/show-200.json +41 -0
  27. data/samples/billing/profile/show-404.json +22 -0
  28. data/samples/billing/profile/update-200.json +47 -0
  29. data/samples/billing/profile/update-422.json +32 -0
  30. data/scalingo.gemspec +1 -3
  31. data/spec/scalingo/api/client_spec.rb +168 -0
  32. data/spec/scalingo/api/endpoint_spec.rb +30 -0
  33. data/spec/scalingo/api/response_spec.rb +285 -0
  34. data/spec/scalingo/auth_spec.rb +15 -0
  35. data/spec/scalingo/bearer_token_spec.rb +72 -0
  36. data/spec/scalingo/billing/profile_spec.rb +55 -0
  37. data/spec/scalingo/client_spec.rb +93 -0
  38. data/spec/scalingo/configuration_spec.rb +55 -0
  39. data/spec/scalingo/regional/operations_spec.rb +11 -3
  40. data/spec/scalingo/regional_spec.rb +14 -0
  41. metadata +33 -40
  42. data/Gemfile.lock +0 -110
  43. data/lib/scalingo/config.rb +0 -38
@@ -4,24 +4,33 @@ require "faraday_middleware"
4
4
  require "scalingo/bearer_token"
5
5
  require "scalingo/errors"
6
6
  require "scalingo/auth"
7
+ require "scalingo/billing"
7
8
  require "scalingo/regional"
8
9
 
9
10
  module Scalingo
10
11
  class Client
11
12
  extend Forwardable
12
13
 
14
+ attr_reader :config
15
+
16
+ def initialize(attributes = {})
17
+ @config = Configuration.new(attributes, Scalingo.config)
18
+
19
+ define_regions!
20
+ end
21
+
13
22
  ## Authentication helpers / Token management
14
23
  attr_reader :token
15
24
 
16
25
  def token=(input)
17
- @token = input.is_a?(BearerToken) ? input : BearerToken.new(input.to_s)
26
+ @token = input.is_a?(BearerToken) ? input : BearerToken.new(input.to_s, raise_on_expired: config.raise_on_expired_token)
18
27
  end
19
28
 
20
29
  def authenticated?
21
30
  token.present? && !token.expired?
22
31
  end
23
32
 
24
- def authenticate_with(access_token: nil, bearer_token: nil, expires_in: nil)
33
+ def authenticate_with(access_token: nil, bearer_token: nil, expires_at: nil)
25
34
  if !access_token && !bearer_token
26
35
  raise ArgumentError, "You must supply one of `access_token` or `bearer_token`"
27
36
  end
@@ -30,18 +39,19 @@ module Scalingo
30
39
  raise ArgumentError, "You cannot supply both `access_token` and `bearer_token`"
31
40
  end
32
41
 
33
- if expires_in && !bearer_token
34
- raise ArgumentError, "`expires_in` can only be used with `bearer_token`. `access_token` have a 1 hour expiration."
42
+ if expires_at && !bearer_token
43
+ raise ArgumentError, "`expires_at` can only be used with `bearer_token`. `access_token` have a 1 hour expiration."
35
44
  end
36
45
 
37
46
  if access_token
38
- expiration = Time.now + Scalingo.config.exchanged_token_validity
47
+ expiration = Time.now + config.exchanged_token_validity
39
48
  response = auth.tokens.exchange(access_token)
40
49
 
41
50
  if response.successful?
42
51
  self.token = BearerToken.new(
43
52
  response.data[:token],
44
- expires_in: expiration,
53
+ expires_at: expiration,
54
+ raise_on_expired: config.raise_on_expired_token,
45
55
  )
46
56
  end
47
57
 
@@ -49,35 +59,33 @@ module Scalingo
49
59
  end
50
60
 
51
61
  if bearer_token
52
- self.token = expires_in ? BearerToken.new(bearer_token.to_s, expires_in: expires_in) : bearer_token
62
+ self.token = if expires_at
63
+ token = bearer_token.is_a?(BearerToken) ? bearer_token.value : bearer_token.to_s
64
+
65
+ BearerToken.new(
66
+ token,
67
+ expires_at: expires_at,
68
+ raise_on_expired: config.raise_on_expired_token,
69
+ )
70
+ else
71
+ bearer_token
72
+ end
73
+
74
+ true
53
75
  end
54
76
  end
55
77
 
56
78
  ## API clients
57
79
  def auth
58
- @auth ||= Auth.new(self, Scalingo.config.urls.auth)
80
+ @auth ||= Auth.new(self, config.auth)
59
81
  end
60
82
 
61
- def region
62
- public_send(Scalingo.config.default_region)
83
+ def billing
84
+ @billing ||= Billing.new(self, config.billing)
63
85
  end
64
86
 
65
- Scalingo.config.regions.each do |region|
66
- if Scalingo.config.urls[region].blank?
67
- raise ArgumentError, "Scalingo: no url configured for region #{region}"
68
- end
69
-
70
- define_method(region) do
71
- region_client = instance_variable_get :"@#{region}"
72
-
73
- unless region_client
74
- region_client = Regional.new(self, Scalingo.config.urls[region])
75
-
76
- instance_variable_set :"@#{region}", region_client
77
- end
78
-
79
- region_client
80
- end
87
+ def region(name = nil)
88
+ public_send(name || config.default_region)
81
89
  end
82
90
 
83
91
  ## Delegations
@@ -101,5 +109,23 @@ module Scalingo
101
109
  def_delegator :region, :notifiers
102
110
  def_delegator :region, :operations
103
111
  def_delegator :region, :scm_repo_links
112
+
113
+ private
114
+
115
+ def define_regions!
116
+ config.regions.each_pair do |region, _|
117
+ define_singleton_method(region) do
118
+ region_client = instance_variable_get :"@#{region}"
119
+
120
+ unless region_client
121
+ region_client = Regional.new(self, config.regions.public_send(region))
122
+
123
+ instance_variable_set :"@#{region}", region_client
124
+ end
125
+
126
+ region_client
127
+ end
128
+ end
129
+ end
104
130
  end
105
131
  end
@@ -0,0 +1,98 @@
1
+ require "active_support/core_ext/numeric/time"
2
+ require "scalingo/version"
3
+ require "ostruct"
4
+
5
+ module Scalingo
6
+ class Configuration
7
+ ATTRIBUTES = [
8
+ # URL to the authentication API
9
+ :auth,
10
+
11
+ # URL to the billing API
12
+ :billing,
13
+
14
+ # List of regions under the form {"region_id": root_url}
15
+ :regions,
16
+
17
+ # Default region. Must match a key in `regions`
18
+ :default_region,
19
+
20
+ # Wether to use https or http
21
+ :https,
22
+
23
+ # Configure the User Agent header
24
+ :user_agent,
25
+
26
+ # For how long is a bearer token considered valid (it will raise passed this delay).
27
+ # Set to nil to never raise.
28
+ :exchanged_token_validity,
29
+
30
+ # Raise an exception when trying to use an authenticated connection without a bearer token set
31
+ # Having this setting to true prevents performing requests that would fail due to lack of authentication headers.
32
+ :raise_on_missing_authentication,
33
+
34
+ # Raise an exception when the bearer token in use is supposed to be invalid
35
+ :raise_on_expired_token,
36
+
37
+ # These headers will be added to every request. Individual methods may override them.
38
+ # This should be a hash or a callable object that returns a hash.
39
+ :additional_headers
40
+ ]
41
+
42
+ ATTRIBUTES.each { |attr| attr_accessor(attr) }
43
+
44
+ def self.default
45
+ new(
46
+ auth: "https://auth.scalingo.com/v1",
47
+ billing: "https://cashmachine.scalingo.com",
48
+ regions: {
49
+ agora_fr1: "https://api.agora-fr1.scalingo.com/v1",
50
+ osc_fr1: "https://api.osc-fr1.scalingo.com/v1",
51
+ osc_secnum_fr1: "https://api.osc-secnum-fr1.scalingo.com/v1"
52
+ },
53
+ default_region: :osc_fr1,
54
+ user_agent: "Scalingo Ruby Client v#{Scalingo::VERSION}",
55
+ exchanged_token_validity: 1.hour,
56
+ raise_on_missing_authentication: true,
57
+ raise_on_expired_token: false,
58
+ additional_headers: {},
59
+ )
60
+ end
61
+
62
+ def initialize(attributes = {}, parent = nil)
63
+ ATTRIBUTES.each do |name|
64
+ public_send("#{name}=", attributes.fetch(name, parent&.public_send(name)))
65
+ end
66
+ end
67
+
68
+ def regions=(input)
69
+ if input.is_a?(OpenStruct)
70
+ @regions = input
71
+ return
72
+ end
73
+
74
+ if input.is_a?(Hash)
75
+ @regions = OpenStruct.new(input)
76
+ return
77
+ end
78
+
79
+ raise ArgumentError, "wrong type for argument"
80
+ end
81
+
82
+ def default_region=(input)
83
+ input = input.to_sym
84
+
85
+ raise ArgumentError, "No regions named `#{input}`" unless regions.respond_to?(input)
86
+
87
+ @default_region = input
88
+ end
89
+ end
90
+
91
+ def self.config
92
+ @config ||= Configuration.default
93
+ end
94
+
95
+ def self.configure
96
+ yield config
97
+ end
98
+ end
@@ -83,7 +83,7 @@ module Scalingo
83
83
  def categories(headers = nil, &block)
84
84
  data = nil
85
85
 
86
- response = connection(allow_guest: true).get(
86
+ response = connection(fallback_to_guest: true).get(
87
87
  "addon_categories",
88
88
  data,
89
89
  headers,
@@ -96,7 +96,7 @@ module Scalingo
96
96
  def providers(headers = nil, &block)
97
97
  data = nil
98
98
 
99
- response = connection(allow_guest: true).get(
99
+ response = connection(fallback_to_guest: true).get(
100
100
  "addon_providers",
101
101
  data,
102
102
  headers,
@@ -44,7 +44,7 @@ module Scalingo
44
44
  def sizes(headers = nil, &block)
45
45
  data = nil
46
46
 
47
- response = connection(allow_guest: true).get(
47
+ response = connection(fallback_to_guest: true).get(
48
48
  "features/container_sizes",
49
49
  data,
50
50
  headers,
@@ -29,7 +29,7 @@ module Scalingo
29
29
  def types(headers = nil, &block)
30
30
  data = nil
31
31
 
32
- response = connection(allow_guest: true).get(
32
+ response = connection(fallback_to_guest: true).get(
33
33
  "event_types",
34
34
  data,
35
35
  headers,
@@ -42,7 +42,7 @@ module Scalingo
42
42
  def categories(headers = nil, &block)
43
43
  data = nil
44
44
 
45
- response = connection(allow_guest: true).get(
45
+ response = connection(fallback_to_guest: true).get(
46
46
  "event_categories",
47
47
  data,
48
48
  headers,
@@ -3,7 +3,7 @@ module Scalingo
3
3
  def get(url, payload = {}, headers = nil, &block)
4
4
  data = payload.compact
5
5
 
6
- response = connection(allow_guest: true).get(
6
+ response = connection(fallback_to_guest: true).get(
7
7
  url,
8
8
  data,
9
9
  headers,
@@ -28,7 +28,7 @@ module Scalingo
28
28
  def types(headers = nil, &block)
29
29
  data = nil
30
30
 
31
- response = connection(allow_guest: true).get(
31
+ response = connection(fallback_to_guest: true).get(
32
32
  "features/metrics",
33
33
  data,
34
34
  headers,
@@ -83,7 +83,7 @@ module Scalingo
83
83
  def platforms(headers = nil, &block)
84
84
  data = nil
85
85
 
86
- response = connection(allow_guest: true).get(
86
+ response = connection(fallback_to_guest: true).get(
87
87
  "notification_platforms",
88
88
  data,
89
89
  headers,
@@ -3,10 +3,18 @@ require "scalingo/api/endpoint"
3
3
  module Scalingo
4
4
  class Regional::Operations < API::Endpoint
5
5
  def find(app_id, operation_id, headers = nil, &block)
6
+ get(
7
+ "apps/#{app_id}/operations/#{operation_id}",
8
+ headers,
9
+ &block
10
+ )
11
+ end
12
+
13
+ def get(url, headers = nil, &block)
6
14
  data = nil
7
15
 
8
16
  response = connection.get(
9
- "apps/#{app_id}/operations/#{operation_id}",
17
+ url,
10
18
  data,
11
19
  headers,
12
20
  &block
@@ -1,3 +1,3 @@
1
1
  module Scalingo
2
- VERSION = "3.0.0.beta.1"
2
+ VERSION = "3.0.0.beta.2"
3
3
  end
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "882d7733-923b-40dc-88e6-f1324e48c42a",
3
+ "create": {
4
+ "valid": {
5
+ "name": "Billing profile",
6
+ "address_line1": "1, rue de la paix",
7
+ "address_zip": "75001",
8
+ "address_city": "Paris",
9
+ "address_country": "France"
10
+ },
11
+ "invalid": {
12
+ "some": "attribute"
13
+ }
14
+ },
15
+ "update": {
16
+ "valid": {
17
+ "address_city": "New Somecity"
18
+ },
19
+ "invalid": {
20
+ "address_country": "not a country"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "path": "/profiles",
3
+ "method": "post",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token",
7
+ "Content-Type": "application/json"
8
+ },
9
+ "json_body": {
10
+ "profile": {
11
+ "name": "Billing profile",
12
+ "address_line1": "1, rue de la paix",
13
+ "address_zip": "75001",
14
+ "address_city": "Paris",
15
+ "address_country": "France"
16
+ }
17
+ }
18
+ },
19
+ "response": {
20
+ "status": 201,
21
+ "headers": {
22
+ "Date": "Fri, 12 Jun 2020 15:46:11 GMT",
23
+ "Content-Type": "application/json; charset=utf-8",
24
+ "Transfer-Encoding": "chunked",
25
+ "Connection": "keep-alive",
26
+ "Cache-Control": "no-cache"
27
+ },
28
+ "json_body": {
29
+ "profile": {
30
+ "id": "882d7733-923b-40dc-88e6-f1324e48c42a",
31
+ "name": "Billing profile",
32
+ "email": null,
33
+ "address_line1": "1, rue de la paix",
34
+ "address_line2": null,
35
+ "address_city": "Paris",
36
+ "balance": 0,
37
+ "address_zip": "75001",
38
+ "address_state": null,
39
+ "address_country": "FR",
40
+ "vat_number": null,
41
+ "company": null,
42
+ "payment_method": "sepa",
43
+ "created_at": "2020-05-29T13:01:46.217Z",
44
+ "updated_at": "2020-06-12T15:45:43.535Z",
45
+ "credit": 0,
46
+ "stripe_payment_method": null
47
+ }
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "path": "/profiles",
3
+ "method": "post",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token",
7
+ "Content-Type": "application/json"
8
+ },
9
+ "json_body": {
10
+ "profile": {
11
+ }
12
+ }
13
+ },
14
+ "response": {
15
+ "status": 400,
16
+ "headers": {
17
+ "Date": "Fri, 12 Jun 2020 15:46:10 GMT",
18
+ "Content-Type": "application/json; charset=utf-8",
19
+ "Transfer-Encoding": "chunked",
20
+ "Connection": "keep-alive",
21
+ "Cache-Control": "no-cache"
22
+ },
23
+ "json_body": {
24
+ "error": "profile is required"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "path": "/profiles",
3
+ "method": "post",
4
+ "request": {
5
+ "headers": {
6
+ "Authorization": "Bearer the-bearer-token",
7
+ "Content-Type": "application/json"
8
+ },
9
+ "json_body": {
10
+ "profile": {
11
+ "some": "attribute"
12
+ }
13
+ }
14
+ },
15
+ "response": {
16
+ "status": 422,
17
+ "headers": {
18
+ "Date": "Fri, 12 Jun 2020 15:46:11 GMT",
19
+ "Content-Type": "application/json; charset=utf-8",
20
+ "Transfer-Encoding": "chunked",
21
+ "Connection": "keep-alive",
22
+ "Cache-Control": "no-cache"
23
+ },
24
+ "json_body": {
25
+ "errors": {
26
+ "name": [
27
+ "can't be blank"
28
+ ],
29
+ "address_line1": [
30
+ "can't be blank"
31
+ ],
32
+ "address_zip": [
33
+ "can't be blank"
34
+ ],
35
+ "address_city": [
36
+ "can't be blank"
37
+ ],
38
+ "address_country": [
39
+ "can't be blank"
40
+ ]
41
+ }
42
+ }
43
+ }
44
+ }