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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fd4bd22f57705fe4066a2f1de8d830c4557383777a49b7a7258a9e8c7b99077
4
- data.tar.gz: fbf139e41c1a12c007e08b6c64e4e8d3b54d14500f7b44b14c7ba4ba8bfce7be
3
+ metadata.gz: 6582ca860b783b598a3f31874afc37958ead26c29c17c099e6cba1ef094f933d
4
+ data.tar.gz: c0206b25118775599be97e0c6a7ef06c40914a34711c882a21268521f964ae24
5
5
  SHA512:
6
- metadata.gz: 06271d0e9010d2d4835be48fa43b49545600b63f938e75ef504d17cec8434f07a9f8a5efc15cf411c9a1729c308c99a9b3504539d134530bf93acd73ec2f4565
7
- data.tar.gz: 819dd71a0fb81443f934c5ed6f500c7c03105cc1113f4df60cbdb2655d4cfd057d39d38d8535b78f58d24f6deabe4c4b800c31e1022f82d719547e7646ed33d3
6
+ metadata.gz: 1e7e41bb52927a17d3de0c840f281754ee1907a000db36735ec779d5115a345f91bf944aad068421e9292fcb3ab9f92db49690a25e61f4f81cd8ff45c625f550
7
+ data.tar.gz: e2ebc1468f18304b3086ad927db4e89f09ef1d6256395a6f72a089776eeba8643b923bec88af1690f0264786dd46d6982b421f741b99f23a86ec513a3841fba9
data/.gitignore CHANGED
@@ -1,5 +1,5 @@
1
1
  *.swp
2
-
2
+ Gemfile.lock
3
3
  coverage/
4
4
  /pkg
5
5
 
@@ -1,4 +1,16 @@
1
- ## 3.0.0.beta1 - 2020/06/12
1
+ ## Unreleased
2
+
3
+ ## 3.0.0.beta.2 - 2020/06/18
4
+
5
+ * Bugfix: do not "dig" into the response body if it is not a 2XX (#18, #19)
6
+ * Rework configuration, add specs over it (#17)
7
+ * Rename `BearerToken#expires_in` to `expires_at` (#16)
8
+ * Rename argument `allow_guest:` for `API::Endpoint#connection` to `fallback_to_guest:` (#16)
9
+ * Response objects can access the operation directly using `.operation` (#15)
10
+ * New API: billing api (#14)
11
+ * New endpoint: `profile`. Methods: `show`, `create`, `update`
12
+
13
+ ## 3.0.0.beta.1 - 2020/06/12
2
14
 
3
15
  * Full rewrite of the gem, **zero** backwards compatibility. Refer to the `README` for more information.
4
16
 
data/README.md CHANGED
@@ -13,12 +13,12 @@ You can check the version 2 at [the v2 branch of this repository](https://github
13
13
  ### The road to 3.0.0
14
14
 
15
15
  This gem is still in beta version, but its API should not change a lot until a final release.
16
- However, configuration may change. An issue will be opened and pinned to follow up the work that remains to be done.
16
+ [This issue tracks the remaining work](https://github.com/Scalingo/scalingo-ruby-api/issues/13).
17
17
 
18
18
  ## Installation
19
19
 
20
20
  ```ruby
21
- gem "scalingo", "3.0.0.beta.1"
21
+ gem "scalingo", "3.0.0.beta.2"
22
22
  ```
23
23
 
24
24
  And then execute:
@@ -55,18 +55,29 @@ client.section.request(id, payload = {}, headers = nil, &block)
55
55
 
56
56
  ## Configuration
57
57
 
58
+ You can refer to the code below to configure the gem globally.
59
+ The values displayed match the default ones.
60
+
61
+ :warning: Configuration is copied when instanciating a `Scalingo::Client` object;
62
+ changing the configuration globally will therefore not affect already existing objects.
63
+
58
64
  ```ruby
59
65
  Scalingo.configure do |config|
60
- # Known regions. Each region should have a corresponding entry in the urls settings below
61
- config.regions = %i[osc_fr1 osc_secnum_fr1]
66
+ # Authentication API url
67
+ config.auth = "https://auth.scalingo.com/v1"
68
+
69
+ # Billing API url
70
+ config.billing = "https://cashmachine.scalingo.com"
62
71
 
63
- # Default region
64
- config.defaul_region = :osc_fr1
72
+ # Known regions and their api's url
73
+ config.regions = {
74
+ agora_fr1: "https://api.agora-fr1.scalingo.com/v1",
75
+ osc_fr1: "https://api.osc-fr1.scalingo.com/v1",
76
+ osc_secnum_fr1: "https://api.osc-secnum-fr1.scalingo.com/v1"
77
+ }
65
78
 
66
- # Endpoints URLS
67
- config.urls.auth = "https://auth.scalingo.com/v1"
68
- config.urls.osc_fr1 = "https://api.osc-fr1.scalingo.com/v1"
69
- config.urls.osc_secnum_fr1 = "https://api.osc-secnum-fr1.scalingo.com/v1"
79
+ # Default region. Must match an entry in `regions`
80
+ config.default_region = :osc_fr1
70
81
 
71
82
  # Configure the User Agent header
72
83
  config.user_agent = "Scalingo Ruby Client v#{Scalingo::VERSION}"
@@ -75,38 +86,32 @@ Scalingo.configure do |config|
75
86
  # Set to nil to never raise.
76
87
  config.exchanged_token_validity = 1.hour
77
88
 
78
- # Raise an exception when trying to use an authenticated connection without a bearer token set
79
89
  # Having this setting to true prevents performing requests that would fail due to lack of authentication headers.
80
90
  config.raise_on_missing_authentication = true
81
91
 
92
+ # Raise an exception when the bearer token in use is supposed to be invalid
93
+ config.raise_on_expired_token = false
94
+
82
95
  # These headers will be added to every request. Individual methods may override them.
83
96
  # This should be a hash or a callable object that returns a hash.
84
97
  config.additional_headers = {}
85
-
86
- # Raise an exception when the bearer token in use is supposed to be invalid
87
- config.raise_on_expired_token = false
88
98
  end
89
99
  ```
90
100
 
91
- :warning: If you change the settings for the list of regions, you **cannot** use `require "scalingo"`; you must follow this template :
101
+ You can also configure each client separately.
102
+ Values not supplied will be copied from the global configuration.
92
103
 
93
104
  ```ruby
94
-
95
- require "scalingo/config"
96
-
97
- Scalingo.configure do |config|
98
- config.regions = %i[my-regions]
99
- end
100
-
101
- require "scalingo/client"
105
+ scalingo = Scalingo::Client.new(raise_on_expired_token: false)
102
106
  ```
103
107
 
104
108
  ## Response object
105
109
 
106
110
  Responses are parsed with the keys symbolized and then encapsulated in a `Scalingo::API::Response` object:
111
+
107
112
  * `response.status` containts the HTTP status code
108
113
  * `response.data` contains the "relevant" data, without the json root key (when relevant)
109
- * `response.full_data` contains the full response body
114
+ * `response.full_body` contains the full response body
110
115
  * `response.meta` contains the meta object, if there's any
111
116
  * `response.headers` containts all the response headers
112
117
 
@@ -114,7 +119,16 @@ Some helper methods are defined on this object:
114
119
  * `response.successful?` returns true when the code is 2XX
115
120
  * `response.paginated?` returns true if the reponse has metadata relative to pagination
116
121
  * `response.operation?` returns true if the response contains a header relative to an ongoing operation
117
- * `response.operation` returns the URL to query to get the status of the operation
122
+ * `response.operation_url` returns the URL to query to get the status of the operation
123
+ * `response.operation` performs a request to retrieve the operation
124
+
125
+ ## Other details on the code architecture
126
+
127
+ * `Scalingo::Client` instances hold configuration and the token used for authentication
128
+ * `Scalingo::API::Client` subclasses (`Scalingo::Auth`, `Scalingo::Billing`, `Scalingo::Regional`) provides access to the APIs.
129
+ You can use `connection` (returns a faraday instance) on those objects to perform any request freely.
130
+ * `Scalingo::API::Endpoint` subclasses (`Scalingo::Auth::User`) instances belong to an api client (cf previous point).
131
+ They provide quick and uniform access to expected requests.
118
132
 
119
133
  ## Examples
120
134
 
@@ -128,7 +142,7 @@ scalingo.authenticate_with(access_token: "my_access_token")
128
142
  scalingo.authenticate_with(bearer_token: "my_bearer_jwt")
129
143
 
130
144
  # Return your profile
131
- scalingo.self
145
+ scalingo.user.self
132
146
 
133
147
  # List your SSH Keys
134
148
  scalingo.keys.all # OR scalingo.auth.keys.all
@@ -140,7 +154,7 @@ scalingo.keys.show("my-key-id")
140
154
  scalingo.apps.all # OR scalingo.region.apps.all
141
155
 
142
156
  # List your apps on osc-fr1
143
- scalingo.osc_fr1.apps.all
157
+ scalingo.osc_fr1.apps.all # OR scalingo.region(:osc_fr1).apps.all
144
158
 
145
159
  # Preview the creation of an app on the default region
146
160
  scalingo.apps.create(name: "my-new-app", dry_run: true)
@@ -1,2 +1,2 @@
1
- require "scalingo/config"
1
+ require "scalingo/configuration"
2
2
  require "scalingo/client"
@@ -10,40 +10,51 @@ module Scalingo
10
10
 
11
11
  def self.register_handlers!(handlers)
12
12
  handlers.each do |method_name, klass|
13
- define_method(method_name) do
14
- value = instance_variable_get("@#{method_name}")
13
+ register_handler!(method_name, klass)
14
+ end
15
+ end
15
16
 
16
- if value.nil?
17
- value = klass.new(self)
18
- instance_variable_set("@#{method_name}", value)
19
- end
17
+ def self.register_handler!(method_name, klass)
18
+ define_method(method_name) do
19
+ value = instance_variable_get("@#{method_name}")
20
20
 
21
- value
21
+ if value.nil?
22
+ value = klass.new(self)
23
+ instance_variable_set("@#{method_name}", value)
22
24
  end
25
+
26
+ value
23
27
  end
24
28
  end
25
29
 
26
30
  ## Faraday objects
27
- def connection_options
28
- headers = {
29
- "User-Agent" => Scalingo.config.user_agent
31
+ def headers
32
+ hash = {
33
+ "User-Agent" => scalingo.config.user_agent
30
34
  }
31
35
 
32
- if (extra = Scalingo.config.additional_headers).present?
33
- extra.respond_to?(:call) ? headers.update(extra.call) : headers.update(extra)
36
+ if (extra = scalingo.config.additional_headers).present?
37
+ extra.respond_to?(:call) ? hash.update(extra.call) : hash.update(extra)
34
38
  end
35
39
 
40
+ hash
41
+ end
42
+
43
+ def connection_options
36
44
  {
37
45
  url: url,
38
46
  headers: headers
39
47
  }
40
48
  end
41
49
 
42
- def connection(allow_guest: false)
43
- if allow_guest
50
+ # Note: when `config.raise_on_missing_authentication` is set to false,
51
+ # this method may return the unauthenticated connection
52
+ # even with `fallback_to_guest: false`
53
+ def connection(fallback_to_guest: false)
54
+ if fallback_to_guest
44
55
  begin
45
56
  authenticated_connection
46
- rescue
57
+ rescue Error::Unauthenticated
47
58
  unauthenticated_connection
48
59
  end
49
60
  else
@@ -54,14 +65,20 @@ module Scalingo
54
65
  def unauthenticated_connection
55
66
  @unauthenticated_conn ||= Faraday.new(connection_options) { |conn|
56
67
  conn.response :json, content_type: /\bjson$/, parser_options: {symbolize_names: true}
68
+ conn.request :json
57
69
  }
58
70
  end
59
71
 
60
72
  def authenticated_connection
61
73
  return @connection if @connection
62
74
 
63
- if Scalingo.config.raise_on_missing_authentication && !scalingo.token&.value
64
- raise Error::Unauthenticated
75
+ # Missing token handling. Token expiration is handled in the `value` method.
76
+ unless scalingo.token&.value
77
+ if scalingo.config.raise_on_missing_authentication
78
+ raise Error::Unauthenticated
79
+ else
80
+ return unauthenticated_connection
81
+ end
65
82
  end
66
83
 
67
84
  @connection = Faraday.new(connection_options) { |conn|
@@ -16,7 +16,7 @@ module Scalingo
16
16
  private
17
17
 
18
18
  def unpack(*args)
19
- Response.unpack(*args)
19
+ Response.unpack(client, *args)
20
20
  end
21
21
  end
22
22
  end
@@ -1,49 +1,32 @@
1
1
  module Scalingo
2
2
  module API
3
3
  class Response
4
- def self.transform_object(object, resource_class: nil)
5
- resource_class&.new(object) || object
6
- end
4
+ def self.unpack(client, response, key: nil)
5
+ body = response.body
6
+ has_hash_body = body.present? && body.respond_to?(:key)
7
7
 
8
- def self.transform_body(body, resource_class: nil)
9
- case body
10
- when Hash
11
- transform_object(body, resource_class: resource_class)
12
- when Array
13
- body.map { |item| transform_object(item, resource_class: resource_class) }
14
- else
15
- body
16
- end
17
- end
18
-
19
- def self.transform_meta(body)
20
- if body.present? && body.respond_to?(:key) && body.key?(:meta)
21
- body[:meta]
22
- end
23
- end
8
+ data = body
9
+ meta = nil
24
10
 
25
- def self.unpack(response, key: nil, resource_class: nil)
26
- data = if response.body.respond_to?(:key?) && key.present?
27
- response.body[key]
28
- else
29
- response.body
11
+ if has_hash_body
12
+ data = body[key] if response.success? && key.present?
13
+ meta = body[:meta]
30
14
  end
31
15
 
32
- parsed = transform_body(data, resource_class: resource_class)
33
- meta = transform_meta(response.body)
34
-
35
16
  new(
17
+ client: client,
36
18
  status: response.status,
37
19
  headers: response.headers,
38
- data: parsed,
20
+ data: data,
39
21
  meta: meta,
40
- full_body: response.body,
22
+ full_body: body,
41
23
  )
42
24
  end
43
25
 
44
- attr_reader :status, :headers, :data, :full_body, :meta
26
+ attr_reader :client, :status, :headers, :data, :full_body, :meta
45
27
 
46
- def initialize(status:, headers:, data:, full_body:, meta: nil)
28
+ def initialize(client:, status:, headers:, data:, full_body:, meta: nil)
29
+ @client = client
47
30
  @status = status
48
31
  @headers = headers
49
32
  @data = data
@@ -60,11 +43,17 @@ module Scalingo
60
43
  end
61
44
 
62
45
  def operation
46
+ if operation? && client.respond_to?(:operations)
47
+ client.operations.get(operation_url)
48
+ end
49
+ end
50
+
51
+ def operation_url
63
52
  headers[:location]
64
53
  end
65
54
 
66
55
  def operation?
67
- operation.present
56
+ operation_url.present?
68
57
  end
69
58
  end
70
59
  end
@@ -1,18 +1,24 @@
1
1
  module Scalingo
2
2
  class BearerToken
3
- attr_reader :expires_in
3
+ attr_reader :expires_at
4
+ attr_writer :raise_on_expired
4
5
 
5
- def initialize(value, expires_in: nil)
6
+ def initialize(value, expires_at: nil, raise_on_expired: false)
6
7
  @value = value
7
- @expires_in = expires_in if expires_in
8
+ @expires_at = expires_at if expires_at
9
+ @raise_on_expired = raise_on_expired
10
+ end
11
+
12
+ def raise_on_expired?
13
+ @raise_on_expired
8
14
  end
9
15
 
10
16
  def expired?
11
- expires_in && expires_in <= Time.now
17
+ expires_at && expires_at <= Time.now
12
18
  end
13
19
 
14
20
  def value
15
- raise Error::ExpiredToken if expired? && Scalingo.config.raise_on_expired_token
21
+ raise Error::ExpiredToken if expired? && raise_on_expired?
16
22
 
17
23
  @value
18
24
  end
@@ -0,0 +1,11 @@
1
+ require "scalingo/api/client"
2
+
3
+ module Scalingo
4
+ class Billing < API::Client
5
+ require "scalingo/billing/profile"
6
+
7
+ register_handlers!(
8
+ profile: Profile,
9
+ )
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ require "scalingo/api/endpoint"
2
+
3
+ module Scalingo
4
+ class Billing::Profile < API::Endpoint
5
+ def show(headers = nil, &block)
6
+ data = nil
7
+
8
+ response = connection.get(
9
+ "profile",
10
+ data,
11
+ headers,
12
+ &block
13
+ )
14
+
15
+ unpack(response, key: :profile)
16
+ end
17
+
18
+ def create(payload = {}, headers = nil, &block)
19
+ data = {profile: payload}
20
+
21
+ response = connection.post(
22
+ "profiles",
23
+ data,
24
+ headers,
25
+ &block
26
+ )
27
+
28
+ unpack(response, key: :profile)
29
+ end
30
+
31
+ def update(id, payload = {}, headers = nil, &block)
32
+ data = {profile: payload}
33
+
34
+ response = connection.put(
35
+ "profiles/#{id}",
36
+ data,
37
+ headers,
38
+ &block
39
+ )
40
+
41
+ unpack(response, key: :profile)
42
+ end
43
+
44
+ alias_method :self, :show
45
+ end
46
+ end