pact_broker 2.100.0 → 2.101.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Gemfile +1 -0
  4. data/docs/api/PACTICIPANTS.md +290 -0
  5. data/docs/api/WEBHOOKS.md +40 -40
  6. data/lib/db.rb +1 -1
  7. data/lib/pact_broker/api/decorators/triggered_webhook_decorator.rb +1 -2
  8. data/lib/pact_broker/api/resources/all_webhooks.rb +1 -4
  9. data/lib/pact_broker/api/resources/base_resource.rb +51 -5
  10. data/lib/pact_broker/api/resources/branch_version.rb +10 -1
  11. data/lib/pact_broker/api/resources/clean.rb +11 -9
  12. data/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb +0 -4
  13. data/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb +0 -4
  14. data/lib/pact_broker/api/resources/deployed_version.rb +12 -14
  15. data/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb +4 -0
  16. data/lib/pact_broker/api/resources/environment.rb +5 -5
  17. data/lib/pact_broker/api/resources/environments.rb +1 -5
  18. data/lib/pact_broker/api/resources/label.rb +4 -0
  19. data/lib/pact_broker/api/resources/pact.rb +10 -5
  20. data/lib/pact_broker/api/resources/pact_webhooks.rb +1 -4
  21. data/lib/pact_broker/api/resources/pacticipant.rb +11 -5
  22. data/lib/pact_broker/api/resources/pacticipant_webhooks.rb +1 -4
  23. data/lib/pact_broker/api/resources/pacticipants.rb +1 -4
  24. data/lib/pact_broker/api/resources/provider_pacts_for_verification.rb +19 -11
  25. data/lib/pact_broker/api/resources/publish_contracts.rb +11 -15
  26. data/lib/pact_broker/api/resources/released_version.rb +12 -6
  27. data/lib/pact_broker/api/resources/released_versions_for_version_and_environment.rb +10 -6
  28. data/lib/pact_broker/api/resources/tag.rb +7 -3
  29. data/lib/pact_broker/api/resources/verifications.rb +7 -9
  30. data/lib/pact_broker/api/resources/version.rb +8 -8
  31. data/lib/pact_broker/api/resources/webhook.rb +5 -4
  32. data/lib/pact_broker/api/resources/webhook_execution.rb +4 -6
  33. data/lib/pact_broker/doc/views/index/pacticipant-branch-version.markdown +13 -2
  34. data/lib/pact_broker/doc/views/provider-pacts-for-verification.markdown +1 -1
  35. data/lib/pact_broker/domain/pacticipant.rb +1 -0
  36. data/lib/pact_broker/locale/en.yml +1 -0
  37. data/lib/pact_broker/pacticipants/repository.rb +5 -4
  38. data/lib/pact_broker/pacts/generate_sha.rb +1 -0
  39. data/lib/pact_broker/pacts/verifiable_pact_messages.rb +1 -1
  40. data/lib/pact_broker/test/test_data_builder.rb +20 -0
  41. data/lib/pact_broker/version.rb +1 -1
  42. data/lib/pact_broker/versions/branch_service.rb +7 -0
  43. data/lib/pact_broker/versions/branch_version_repository.rb +17 -0
  44. data/lib/rack/pact_broker/cascade.rb +87 -0
  45. data/lib/webmachine/describe_routes.rb +43 -9
  46. metadata +5 -4
  47. data/lib/pact_broker/api/resources/default_base_resource.rb +0 -0
@@ -15,6 +15,7 @@ module PactBroker
15
15
  module Api
16
16
  module Resources
17
17
  class InvalidJsonError < PactBroker::Error ; end
18
+ class NonUTF8CharacterFound < PactBroker::Error ; end
18
19
 
19
20
  class BaseResource < Webmachine::Resource
20
21
  include PactBroker::Services
@@ -39,6 +40,10 @@ module PactBroker
39
40
  super + ["PATCH"]
40
41
  end
41
42
 
43
+ def malformed_request?
44
+ content_type_is_json_but_invalid_json_provided?
45
+ end
46
+
42
47
  def finish_request
43
48
  application_context.after_resource&.call(self)
44
49
  PactBroker.configuration.after_resource.call(self)
@@ -125,17 +130,32 @@ module PactBroker
125
130
  else
126
131
  @params_with_string_keys ||= JSON.parse(request_body, { symbolize_names: false }.merge(PACT_PARSING_OPTIONS)) #Not load! Otherwise it will try to load Ruby classes.
127
132
  end
128
- rescue JSON::JSONError => e
129
- raise InvalidJsonError.new("Error parsing JSON - #{e.message}")
133
+ rescue StandardError => e
134
+ fragment = fragment_before_invalid_utf_8_char
135
+
136
+ if fragment
137
+ raise NonUTF8CharacterFound.new(message("errors.non_utf_8_char_in_request_body", char_number: fragment.length + 1, fragment: fragment))
138
+ else
139
+ raise InvalidJsonError.new(e.message)
140
+ end
130
141
  end
131
142
  # rubocop: enable Metrics/CyclomaticComplexity
132
143
 
144
+ def fragment_before_invalid_utf_8_char
145
+ request_body.each_char.with_index do | char, index |
146
+ if !char.valid_encoding?
147
+ return index < 100 ? request_body[0...index] : request_body[index-100...index]
148
+ end
149
+ end
150
+ nil
151
+ end
152
+
133
153
  def params_with_string_keys
134
154
  params(symbolize_names: false)
135
155
  end
136
156
 
137
157
  def pact_params
138
- @pact_params ||= PactBroker::Pacts::PactParams.from_request request, identifier_from_path
158
+ @pact_params ||= PactBroker::Pacts::PactParams.from_request(request, identifier_from_path)
139
159
  end
140
160
 
141
161
  def set_json_error_message message
@@ -192,9 +212,15 @@ module PactBroker
192
212
  begin
193
213
  params
194
214
  false
215
+ rescue NonUTF8CharacterFound => e
216
+ logger.info(e.message) # Don't use the default SemanticLogger error logging method because it will try and print out the cause which will contain non UTF-8 chars in the message
217
+ set_json_error_message(e.message)
218
+ response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
219
+ true
195
220
  rescue StandardError => e
196
- logger.info "Error parsing JSON #{e} - #{request_body}"
197
- set_json_error_message "Error parsing JSON - #{e.message}"
221
+ message = "#{e.cause ? e.cause.class.name : e.class.name} - #{e.message}"
222
+ logger.info(message)
223
+ set_json_error_message(message)
198
224
  response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
199
225
  true
200
226
  end
@@ -279,6 +305,26 @@ module PactBroker
279
305
  def malformed_request_for_json_with_schema?(schema_to_use = schema, params_to_validate = params)
280
306
  invalid_json? || validation_errors_for_schema?(schema_to_use, params_to_validate)
281
307
  end
308
+
309
+ def content_type_is_json_but_invalid_json_provided?
310
+ content_type_json? && any_request_body? && invalid_json?
311
+ end
312
+
313
+ def content_type_json?
314
+ request.content_type&.include?("json")
315
+ end
316
+
317
+ # Not a Webmachine method. This is used by security policy code to identify whether
318
+ # a PUT to a non existing resource can create a new object.
319
+ def put_can_create?
320
+ false
321
+ end
322
+
323
+ # Not a Webmachine method. This is used by security policy code to identify whether
324
+ # a PATCH to a non existing resource can create a new object.
325
+ def patch_can_create?
326
+ false
327
+ end
282
328
  end
283
329
  end
284
330
  end
@@ -14,7 +14,11 @@ module PactBroker
14
14
  end
15
15
 
16
16
  def allowed_methods
17
- ["GET", "PUT", "OPTIONS"]
17
+ ["GET", "PUT", "DELETE", "OPTIONS"]
18
+ end
19
+
20
+ def put_can_create?
21
+ true
18
22
  end
19
23
 
20
24
  def resource_exists?
@@ -25,6 +29,11 @@ module PactBroker
25
29
  decorator_class(:branch_version_decorator).new(branch_version).to_json(decorator_options)
26
30
  end
27
31
 
32
+ def delete_resource
33
+ branch_service.delete_branch_version(branch_version)
34
+ true
35
+ end
36
+
28
37
  def from_json
29
38
  already_existed = !!branch_version
30
39
  @branch_version = branch_service.find_or_create_branch_version(identifier_from_path)
@@ -2,14 +2,12 @@ require "pact_broker/api/resources/base_resource"
2
2
  require "pact_broker/db/clean"
3
3
  require "pact_broker/matrix/unresolved_selector"
4
4
 
5
+ # Not exposed yet as we'd need to support administrator auth first
6
+
5
7
  module PactBroker
6
8
  module Api
7
9
  module Resources
8
10
  class Clean < BaseResource
9
- def content_types_accepted
10
- [["application/json"]]
11
- end
12
-
13
11
  def content_types_provided
14
12
  [["application/hal+json"]]
15
13
  end
@@ -19,12 +17,16 @@ module PactBroker
19
17
  end
20
18
 
21
19
  def process_post
22
- keep_selectors = (params[:keep] || []).collect do | hash |
23
- PactBroker::Matrix::UnresolvedSelector.new(hash)
24
- end
20
+ if content_type_json?
21
+ keep_selectors = (params[:keep] || []).collect do | hash |
22
+ PactBroker::Matrix::UnresolvedSelector.new(hash)
23
+ end
25
24
 
26
- result = PactBroker::DB::Clean.call(Sequel::Model.db, { keep: keep_selectors })
27
- response.body = result.to_json
25
+ result = PactBroker::DB::Clean.call(Sequel::Model.db, { keep: keep_selectors })
26
+ response.body = result.to_json
27
+ else
28
+ 415
29
+ end
28
30
  end
29
31
 
30
32
  def policy_name
@@ -8,10 +8,6 @@ module PactBroker
8
8
  class CurrentlyDeployedVersionsForEnvironment < BaseResource
9
9
  using PactBroker::StringRefinements
10
10
 
11
- def content_types_accepted
12
- [["application/json", :from_json]]
13
- end
14
-
15
11
  def content_types_provided
16
12
  [["application/hal+json", :to_json]]
17
13
  end
@@ -8,10 +8,6 @@ module PactBroker
8
8
  class CurrentlySupportedVersionsForEnvironment < BaseResource
9
9
  using PactBroker::StringRefinements
10
10
 
11
- def content_types_accepted
12
- [["application/json", :from_json]]
13
- end
14
-
15
11
  def content_types_provided
16
12
  [["application/hal+json", :to_json]]
17
13
  end
@@ -8,11 +8,6 @@ module PactBroker
8
8
  class DeployedVersion < BaseResource
9
9
  include PactBroker::Messages
10
10
 
11
- def initialize
12
- super
13
- @currently_deployed_param = params(default: {})[:currentlyDeployed]
14
- end
15
-
16
11
  def content_types_provided
17
12
  [
18
13
  ["application/hal+json", :to_json]
@@ -29,16 +24,12 @@ module PactBroker
29
24
  ["GET", "PATCH", "OPTIONS"]
30
25
  end
31
26
 
32
- def resource_exists?
33
- !!deployed_version
27
+ def patch_can_create?
28
+ false
34
29
  end
35
30
 
36
- def malformed_request?
37
- if request.patch?
38
- return invalid_json?
39
- else
40
- false
41
- end
31
+ def resource_exists?
32
+ !!deployed_version
42
33
  end
43
34
 
44
35
  def to_json
@@ -73,7 +64,14 @@ module PactBroker
73
64
 
74
65
  private
75
66
 
76
- attr_reader :currently_deployed_param
67
+ # can't use ||= with a potentially nil value
68
+ def currently_deployed_param
69
+ if defined?(@currently_deployed_param)
70
+ @currently_deployed_param
71
+ else
72
+ @currently_deployed_param = params(default: {})[:currentlyDeployed]
73
+ end
74
+ end
77
75
 
78
76
  def process_currently_deployed_param
79
77
  if currently_deployed_param == false
@@ -43,6 +43,10 @@ module PactBroker
43
43
  :'versions::deployed_versions'
44
44
  end
45
45
 
46
+ def policy_record
47
+ environment
48
+ end
49
+
46
50
  private
47
51
 
48
52
  attr_reader :deployed_version, :existing_deployed_version
@@ -17,16 +17,16 @@ module PactBroker
17
17
  ["GET", "PUT", "DELETE", "OPTIONS"]
18
18
  end
19
19
 
20
+ def put_can_create?
21
+ false
22
+ end
23
+
20
24
  def resource_exists?
21
25
  !!environment
22
26
  end
23
27
 
24
28
  def malformed_request?
25
- if request.put? && environment
26
- invalid_json? || validation_errors_for_schema?(schema, params.merge(uuid: uuid))
27
- else
28
- false
29
- end
29
+ super || (request.put? && environment && validation_errors_for_schema?(schema, params.merge(uuid: uuid)))
30
30
  end
31
31
 
32
32
  def from_json
@@ -27,11 +27,7 @@ module PactBroker
27
27
  end
28
28
 
29
29
  def malformed_request?
30
- if request.post?
31
- invalid_json? || validation_errors_for_schema?(schema, params.merge(uuid: uuid))
32
- else
33
- false
34
- end
30
+ super || (request.post? && validation_errors_for_schema?(schema, params.merge(uuid: uuid)))
35
31
  end
36
32
 
37
33
  def create_path
@@ -17,6 +17,10 @@ module PactBroker
17
17
  ["GET", "PUT", "DELETE", "OPTIONS"]
18
18
  end
19
19
 
20
+ def put_can_create?
21
+ true
22
+ end
23
+
20
24
  def from_json
21
25
  unless label
22
26
  @label = label_service.create(identifier_from_path)
@@ -36,18 +36,23 @@ module PactBroker
36
36
  ["GET", "PUT", "DELETE", "PATCH", "OPTIONS"]
37
37
  end
38
38
 
39
+ def put_can_create?
40
+ true
41
+ end
42
+
43
+ def patch_can_create?
44
+ true
45
+ end
46
+
39
47
  def is_conflict?
40
48
  merge_conflict = request.patch? && resource_exists? && Pacts::Merger.conflict?(pact.json_content, pact_params.json_content)
41
49
 
42
50
  potential_duplicate_pacticipants?(pact_params.pacticipant_names) || merge_conflict || disallowed_modification?
43
51
  end
44
52
 
53
+
45
54
  def malformed_request?
46
- if request.patch? || request.put?
47
- invalid_json? || contract_validation_errors?(Contracts::PutPactParamsContract.new(pact_params), pact_params)
48
- else
49
- false
50
- end
55
+ super || ((request.patch? || request.really_put?) && contract_validation_errors?(Contracts::PutPactParamsContract.new(pact_params), pact_params))
51
56
  end
52
57
 
53
58
  def resource_exists?
@@ -25,10 +25,7 @@ module PactBroker
25
25
  end
26
26
 
27
27
  def malformed_request?
28
- if request.post?
29
- return invalid_json? || validation_errors?(webhook)
30
- end
31
- false
28
+ super || (request.post? && validation_errors?(webhook))
32
29
  end
33
30
 
34
31
  def validation_errors? webhook
@@ -21,18 +21,23 @@ module PactBroker
21
21
  ["GET", "PUT", "PATCH", "DELETE", "OPTIONS"]
22
22
  end
23
23
 
24
+ def put_can_create?
25
+ false
26
+ end
27
+
28
+ def patch_can_create?
29
+ true
30
+ end
31
+
24
32
  def known_methods
25
33
  super + ["PATCH"]
26
34
  end
27
35
 
28
36
  def malformed_request?
29
- if request.patch? || request.put?
30
- invalid_json? || validation_errors_for_schema?
31
- else
32
- false
33
- end
37
+ super || ((request.patch? || request.really_put?) && validation_errors_for_schema?)
34
38
  end
35
39
 
40
+ # PUT or PATCH with content-type application/json
36
41
  def from_json
37
42
  if pacticipant
38
43
  @pacticipant = update_existing_pacticipant
@@ -47,6 +52,7 @@ module PactBroker
47
52
  response.body = to_json
48
53
  end
49
54
 
55
+ # PUT or PATCH with content-type application/merge-patch+json
50
56
  def from_merge_patch_json
51
57
  if request.patch?
52
58
  from_json
@@ -27,10 +27,7 @@ module PactBroker
27
27
  end
28
28
 
29
29
  def malformed_request?
30
- if request.post?
31
- return invalid_json? || webhook_validation_errors?(webhook)
32
- end
33
- false
30
+ super || (request.post? && webhook_validation_errors?(webhook))
34
31
  end
35
32
 
36
33
  def create_path
@@ -23,10 +23,7 @@ module PactBroker
23
23
  end
24
24
 
25
25
  def malformed_request?
26
- if request.post?
27
- return invalid_json? || validation_errors_for_schema?
28
- end
29
- false
26
+ super || (request.post? && validation_errors_for_schema?)
30
27
  end
31
28
 
32
29
  def post_is_create?
@@ -11,28 +11,27 @@ module PactBroker
11
11
  class ProviderPactsForVerification < ProviderPacts
12
12
  using PactBroker::HashRefinements
13
13
 
14
+ def content_types_provided
15
+ [["application/hal+json", :to_json]]
16
+ end
17
+
14
18
  def allowed_methods
15
19
  ["GET", "POST", "OPTIONS"]
16
20
  end
17
21
 
18
- def content_types_accepted
19
- [["application/json"]]
22
+ def malformed_request?
23
+ super || ((request.get? || (request.post? && content_type_json?)) && schema_validation_errors?)
20
24
  end
21
25
 
22
- def malformed_request?
23
- if (errors = query_schema.call(query)).any?
24
- set_json_validation_error_messages(errors)
26
+ def process_post
27
+ if content_type_json?
28
+ response.body = to_json
25
29
  true
26
30
  else
27
- false
31
+ 415
28
32
  end
29
33
  end
30
34
 
31
- def process_post
32
- response.body = to_json
33
- true
34
- end
35
-
36
35
  def read_methods
37
36
  super + %w{POST}
38
37
  end
@@ -96,6 +95,15 @@ module PactBroker
96
95
  def nested_query
97
96
  @nested_query ||= Rack::Utils.parse_nested_query(request.uri.query)
98
97
  end
98
+
99
+ def schema_validation_errors?
100
+ if (errors = query_schema.call(query)).any?
101
+ set_json_validation_error_messages(errors)
102
+ true
103
+ else
104
+ false
105
+ end
106
+ end
99
107
  end
100
108
  end
101
109
  end
@@ -11,11 +11,7 @@ module PactBroker
11
11
  include WebhookExecutionMethods
12
12
 
13
13
  def content_types_provided
14
- [["application/hal+json", :to_json]]
15
- end
16
-
17
- def content_types_accepted
18
- [["application/json"]]
14
+ [["application/hal+json"]]
19
15
  end
20
16
 
21
17
  def allowed_methods
@@ -23,20 +19,20 @@ module PactBroker
23
19
  end
24
20
 
25
21
  def malformed_request?
26
- if request.post?
27
- invalid_json? || validation_errors_for_schema?
28
- else
29
- false
30
- end
22
+ super || (request.post? && content_type_json? && validation_errors_for_schema?)
31
23
  end
32
24
 
33
25
  def process_post
34
- if conflict_notices.any?
35
- set_conflict_response
36
- 409
26
+ if content_type_json?
27
+ if conflict_notices.any?
28
+ set_conflict_response
29
+ 409
30
+ else
31
+ publish_contracts
32
+ true
33
+ end
37
34
  else
38
- publish_contracts
39
- true
35
+ 415
40
36
  end
41
37
  end
42
38
 
@@ -8,11 +8,6 @@ module PactBroker
8
8
  class ReleasedVersion < BaseResource
9
9
  include PactBroker::Messages
10
10
 
11
- def initialize
12
- super
13
- @currently_supported_param = params(default: {})[:currentlySupported]
14
- end
15
-
16
11
  def content_types_provided
17
12
  [["application/hal+json", :to_json]]
18
13
  end
@@ -27,6 +22,10 @@ module PactBroker
27
22
  ["GET", "PATCH", "OPTIONS"]
28
23
  end
29
24
 
25
+ def patch_can_create?
26
+ false
27
+ end
28
+
30
29
  def resource_exists?
31
30
  !!released_version
32
31
  end
@@ -63,7 +62,14 @@ module PactBroker
63
62
 
64
63
  private
65
64
 
66
- attr_reader :currently_supported_param
65
+ # can't use ||= with a potentially nil value
66
+ def currently_supported_param
67
+ if defined?(@currently_deployed_param)
68
+ @currently_supported_param
69
+ else
70
+ @currently_supported_param = params(default: {})[:currentlySupported]
71
+ end
72
+ end
67
73
 
68
74
  def process_currently_supported_param
69
75
  if currently_supported_param == false
@@ -5,11 +5,6 @@ module PactBroker
5
5
  module Api
6
6
  module Resources
7
7
  class ReleasedVersionsForVersionAndEnvironment < BaseResource
8
- def initialize
9
- super
10
- @existing_released_version = version && environment && released_version_service.find_released_version_for_version_and_environment(version, environment)
11
- end
12
-
13
8
  def content_types_accepted
14
9
  [["application/json", :from_json]]
15
10
  end
@@ -35,6 +30,7 @@ module PactBroker
35
30
  end
36
31
 
37
32
  def from_json
33
+ existing_released_version # make sure we have this before we update the database
38
34
  @released_version = released_version_service.create_or_update(next_released_version_uuid, version, environment)
39
35
  response.body = decorator_class(:released_version_decorator).new(released_version).to_json(decorator_options)
40
36
  true
@@ -61,7 +57,15 @@ module PactBroker
61
57
 
62
58
  private
63
59
 
64
- attr_reader :released_version, :existing_released_version
60
+ attr_reader :released_version
61
+
62
+ def existing_released_version
63
+ if defined?(@existing_released_version)
64
+ @existing_released_version
65
+ else
66
+ @existing_released_version = version && environment && released_version_service.find_released_version_for_version_and_environment(version, environment)
67
+ end
68
+ end
65
69
 
66
70
  def version
67
71
  @version ||= version_service.find_by_pacticipant_name_and_number(identifier_from_path)
@@ -17,9 +17,13 @@ module PactBroker
17
17
  ["GET","PUT","DELETE", "OPTIONS"]
18
18
  end
19
19
 
20
+ def put_can_create?
21
+ true
22
+ end
23
+
20
24
  def from_json
21
25
  unless tag
22
- @tag = tag_service.create identifier_from_path
26
+ @tag = tag_service.create(identifier_from_path)
23
27
  # Make it return a 201 by setting the Location header
24
28
  response.headers["Location"] = tag_url(base_url, tag)
25
29
  end
@@ -36,11 +40,11 @@ module PactBroker
36
40
  end
37
41
 
38
42
  def tag
39
- @tag ||= tag_service.find identifier_from_path
43
+ @tag ||= tag_service.find(identifier_from_path)
40
44
  end
41
45
 
42
46
  def delete_resource
43
- tag_service.delete identifier_from_path
47
+ tag_service.delete(identifier_from_path)
44
48
  true
45
49
  end
46
50
 
@@ -34,15 +34,7 @@ module PactBroker
34
34
  end
35
35
 
36
36
  def malformed_request?
37
- if request.post?
38
- return true if invalid_json?
39
- errors = verification_service.errors(params)
40
- if !errors.empty?
41
- set_json_validation_error_messages(errors.messages)
42
- return true
43
- end
44
- end
45
- false
37
+ super || (request.post? && any_validation_errors?)
46
38
  end
47
39
 
48
40
  def create_path
@@ -91,6 +83,12 @@ module PactBroker
91
83
  def verification_params
92
84
  params(symbolize_names: false).merge("wip" => wip?, "pending" => pending?)
93
85
  end
86
+
87
+ def any_validation_errors?
88
+ errors = verification_service.errors(params)
89
+ set_json_validation_error_messages(errors.messages) if !errors.empty?
90
+ !errors.empty?
91
+ end
94
92
  end
95
93
  end
96
94
  end
@@ -21,16 +21,16 @@ module PactBroker
21
21
  ["GET", "PUT", "PATCH", "DELETE", "OPTIONS"]
22
22
  end
23
23
 
24
- def resource_exists?
25
- !!version
24
+ def put_can_create?
25
+ true
26
26
  end
27
27
 
28
- def malformed_request?
29
- if request.put? && any_request_body?
30
- invalid_json?
31
- else
32
- false
33
- end
28
+ def patch_can_create?
29
+ true
30
+ end
31
+
32
+ def resource_exists?
33
+ !!version
34
34
  end
35
35
 
36
36
  def from_json