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

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