vault_ruby_client 0.18.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +287 -0
  3. data/LICENSE +364 -0
  4. data/README.md +223 -0
  5. data/lib/vault/api/approle.rb +221 -0
  6. data/lib/vault/api/auth.rb +324 -0
  7. data/lib/vault/api/auth_tls.rb +95 -0
  8. data/lib/vault/api/auth_token.rb +245 -0
  9. data/lib/vault/api/help.rb +36 -0
  10. data/lib/vault/api/kv.rb +230 -0
  11. data/lib/vault/api/logical.rb +153 -0
  12. data/lib/vault/api/secret.rb +171 -0
  13. data/lib/vault/api/sys/audit.rb +94 -0
  14. data/lib/vault/api/sys/auth.rb +119 -0
  15. data/lib/vault/api/sys/health.rb +66 -0
  16. data/lib/vault/api/sys/init.rb +86 -0
  17. data/lib/vault/api/sys/leader.rb +51 -0
  18. data/lib/vault/api/sys/lease.rb +52 -0
  19. data/lib/vault/api/sys/mount.rb +165 -0
  20. data/lib/vault/api/sys/namespace.rb +86 -0
  21. data/lib/vault/api/sys/policy.rb +95 -0
  22. data/lib/vault/api/sys/quota.rb +110 -0
  23. data/lib/vault/api/sys/seal.rb +84 -0
  24. data/lib/vault/api/sys.rb +30 -0
  25. data/lib/vault/api/transform/alphabet.rb +46 -0
  26. data/lib/vault/api/transform/role.rb +45 -0
  27. data/lib/vault/api/transform/template.rb +57 -0
  28. data/lib/vault/api/transform/transformation.rb +64 -0
  29. data/lib/vault/api/transform.rb +32 -0
  30. data/lib/vault/api.rb +17 -0
  31. data/lib/vault/client.rb +460 -0
  32. data/lib/vault/configurable.rb +53 -0
  33. data/lib/vault/defaults.rb +218 -0
  34. data/lib/vault/encode.rb +22 -0
  35. data/lib/vault/errors.rb +87 -0
  36. data/lib/vault/persistent/connection.rb +45 -0
  37. data/lib/vault/persistent/pool.rb +51 -0
  38. data/lib/vault/persistent/timed_stack_multi.rb +73 -0
  39. data/lib/vault/persistent.rb +1161 -0
  40. data/lib/vault/request.rb +47 -0
  41. data/lib/vault/response.rb +92 -0
  42. data/lib/vault/vendor/connection_pool/timed_stack.rb +181 -0
  43. data/lib/vault/vendor/connection_pool/version.rb +8 -0
  44. data/lib/vault/vendor/connection_pool.rb +153 -0
  45. data/lib/vault/version.rb +6 -0
  46. data/lib/vault_ruby_client.rb +53 -0
  47. metadata +158 -0
@@ -0,0 +1,119 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ require "json"
5
+
6
+ module Vault
7
+ class Auth < Response
8
+ # @!attribute [r] description
9
+ # Description of the auth backend.
10
+ # @return [String]
11
+ field :description
12
+
13
+ # @!attribute [r] type
14
+ # Name of the auth backend.
15
+ # @return [String]
16
+ field :type
17
+ end
18
+
19
+ class AuthConfig < Response
20
+ # @!attribute [r] default_lease_ttl
21
+ # The default time-to-live.
22
+ # @return [String]
23
+ field :default_lease_ttl
24
+
25
+ # @!attribute [r] max_lease_ttl
26
+ # The maximum time-to-live.
27
+ # @return [String]
28
+ field :max_lease_ttl
29
+ end
30
+
31
+ class Sys
32
+ # List all auths in Vault.
33
+ #
34
+ # @example
35
+ # Vault.sys.auths #=> {:token => #<Vault::Auth type="token", description="token based credentials">}
36
+ #
37
+ # @return [Hash<Symbol, Auth>]
38
+ def auths
39
+ json = client.get("/v1/sys/auth")
40
+ json = json[:data] if json[:data]
41
+ return Hash[*json.map do |k,v|
42
+ [k.to_s.chomp("/").to_sym, Auth.decode(v)]
43
+ end.flatten]
44
+ end
45
+
46
+ # Enable a particular authentication at the given path.
47
+ #
48
+ # @example
49
+ # Vault.sys.enable_auth("github", "github") #=> true
50
+ #
51
+ # @param [String] path
52
+ # the path to mount the auth
53
+ # @param [String] type
54
+ # the type of authentication
55
+ # @param [String] description
56
+ # a human-friendly description (optional)
57
+ #
58
+ # @return [true]
59
+ def enable_auth(path, type, description = nil)
60
+ payload = { type: type }
61
+ payload[:description] = description if !description.nil?
62
+
63
+ client.post("/v1/sys/auth/#{encode_path(path)}", JSON.fast_generate(payload))
64
+ return true
65
+ end
66
+
67
+ # Disable a particular authentication at the given path. If not auth
68
+ # exists at that path, an error will be raised.
69
+ #
70
+ # @example
71
+ # Vault.sys.disable_auth("github") #=> true
72
+ #
73
+ # @param [String] path
74
+ # the path to disable
75
+ #
76
+ # @return [true]
77
+ def disable_auth(path)
78
+ client.delete("/v1/sys/auth/#{encode_path(path)}")
79
+ return true
80
+ end
81
+
82
+ # Read the given auth path's configuration.
83
+ #
84
+ # @example
85
+ # Vault.sys.auth_tune("github") #=> #<Vault::AuthConfig "default_lease_ttl"=3600, "max_lease_ttl"=7200>
86
+ #
87
+ # @param [String] path
88
+ # the path to retrieve configuration for
89
+ #
90
+ # @return [AuthConfig]
91
+ # configuration of the given auth path
92
+ def auth_tune(path)
93
+ json = client.get("/v1/sys/auth/#{encode_path(path)}/tune")
94
+ return AuthConfig.decode(json)
95
+ rescue HTTPError => e
96
+ return nil if e.code == 404
97
+ raise
98
+ end
99
+
100
+ # Write the given auth path's configuration.
101
+ #
102
+ # @example
103
+ # Vault.sys.auth_tune("github", "default_lease_ttl" => 600, "max_lease_ttl" => 1200 ) #=> true
104
+ #
105
+ # @param [String] path
106
+ # the path to retrieve configuration for
107
+ #
108
+ # @return [AuthConfig]
109
+ # configuration of the given auth path
110
+ def put_auth_tune(path, config = {})
111
+ json = client.put("/v1/sys/auth/#{encode_path(path)}/tune", JSON.fast_generate(config))
112
+ if json.nil?
113
+ return true
114
+ else
115
+ return Secret.decode(json)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,66 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ require "json"
5
+
6
+ module Vault
7
+ class HealthStatus < Response
8
+ # @!attribute [r] initialized
9
+ # Whether the Vault server is Initialized.
10
+ # @return [Boolean]
11
+ field :initialized, as: :initialized?
12
+
13
+ # @!attribute [r] sealed
14
+ # Whether the Vault server is Sealed.
15
+ # @return [Boolean]
16
+ field :sealed, as: :sealed?
17
+
18
+ # @!attribute [r] standby
19
+ # Whether the Vault server is in Standby mode.
20
+ # @return [Boolean]
21
+ field :standby, as: :standby?
22
+
23
+ # @!attribute [r] replication_performance_mode
24
+ # Verbose description of DR mode (added in 0.9.2)
25
+ # @return [String]
26
+ field :replication_performance_mode
27
+
28
+ # @!attribute [r] replication_dr_mode
29
+ # Verbose description of DR mode (added in 0.9.2)
30
+ # @return [String]
31
+ field :replication_dr_mode
32
+
33
+ # @!attribute [r] server_time_utc
34
+ # Server time in Unix seconds, UTC
35
+ # @return [Fixnum]
36
+ field :server_time_utc
37
+
38
+ # @!attribute [r] version
39
+ # Server Vault version string (added in 0.6.1)
40
+ # @return [String]
41
+ field :version
42
+
43
+ # @!attribute [r] cluster_name
44
+ # Server cluster name
45
+ # @return [String]
46
+ field :cluster_name
47
+
48
+ # @!attribute [r] cluster_id
49
+ # Server cluster UUID
50
+ # @return [String]
51
+ field :cluster_id
52
+ end
53
+
54
+ class Sys
55
+ # Show the health status for this vault.
56
+ #
57
+ # @example
58
+ # Vault.sys.health_status #=> #Vault::HealthStatus @initialized=true, @sealed=false, @standby=false, @replication_performance_mode="disabled", @replication_dr_mode="disabled", @server_time_utc=1519776728, @version="0.9.3", @cluster_name="vault-cluster-997f514e", @cluster_id="c2dad70a-6d88-a06d-69f6-9ae7f5485998">
59
+ #
60
+ # @return [HealthStatus]
61
+ def health_status
62
+ json = client.get("/v1/sys/health", {:sealedcode => 200, :uninitcode => 200, :standbycode => 200})
63
+ return HealthStatus.decode(json)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,86 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ require "json"
5
+
6
+ module Vault
7
+ class InitResponse < Response
8
+ # @!attribute [r] keys
9
+ # List of unseal keys.
10
+ # @return [Array<String>]
11
+ field :keys
12
+
13
+ # @!attribute [r] keys_base64
14
+ # List of unseal keys, base64-encoded
15
+ # @return [Array<String>]
16
+ field :keys_base64
17
+
18
+ # @!attribute [r] root_token
19
+ # Initial root token.
20
+ # @return [String]
21
+ field :root_token
22
+ end
23
+
24
+ class InitStatus < Response
25
+ # @!method initialized?
26
+ # Returns whether the Vault server is initialized.
27
+ # @return [Boolean]
28
+ field :initialized, as: :initialized?
29
+ end
30
+
31
+ class Sys
32
+ # Show the initialization status for this vault.
33
+ #
34
+ # @example
35
+ # Vault.sys.init_status #=> #<Vault::InitStatus initialized=true>
36
+ #
37
+ # @return [InitStatus]
38
+ def init_status
39
+ json = client.get("/v1/sys/init")
40
+ return InitStatus.decode(json)
41
+ end
42
+
43
+ # Initialize a new vault.
44
+ #
45
+ # @example
46
+ # Vault.sys.init #=> #<Vault::InitResponse keys=["..."] root_token="...">
47
+ #
48
+ # @param [Hash] options
49
+ # the list of init options
50
+ #
51
+ # @option options [String] :root_token_pgp_key
52
+ # optional base64-encoded PGP public key used to encrypt the initial root
53
+ # token.
54
+ # @option options [Fixnum] :secret_shares
55
+ # the number of shares
56
+ # @option options [Fixnum] :secret_threshold
57
+ # the number of keys needed to unlock
58
+ # @option options [Array<String>] :pgp_keys
59
+ # an optional Array of base64-encoded PGP public keys to encrypt sharees
60
+ # @option options [Fixnum] :stored_shares
61
+ # the number of shares that should be encrypted by the HSM for
62
+ # auto-unsealing
63
+ # @option options [Fixnum] :recovery_shares
64
+ # the number of shares to split the recovery key into
65
+ # @option options [Fixnum] :recovery_threshold
66
+ # the number of shares required to reconstruct the recovery key
67
+ # @option options [Array<String>] :recovery_pgp_keys
68
+ # an array of PGP public keys used to encrypt the output for the recovery
69
+ # keys
70
+ #
71
+ # @return [InitResponse]
72
+ def init(options = {})
73
+ json = client.put("/v1/sys/init", JSON.fast_generate(
74
+ root_token_pgp_key: options.fetch(:root_token_pgp_key, nil),
75
+ secret_shares: options.fetch(:secret_shares, options.fetch(:shares, 5)),
76
+ secret_threshold: options.fetch(:secret_threshold, options.fetch(:threshold, 3)),
77
+ pgp_keys: options.fetch(:pgp_keys, nil),
78
+ stored_shares: options.fetch(:stored_shares, nil),
79
+ recovery_shares: options.fetch(:recovery_shares, nil),
80
+ recovery_threshold: options.fetch(:recovery_threshold, nil),
81
+ recovery_pgp_keys: options.fetch(:recovery_pgp_keys, nil),
82
+ ))
83
+ return InitResponse.decode(json)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,51 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ module Vault
5
+ class LeaderStatus < Response
6
+ # @!method ha_enabled?
7
+ # Returns whether the high-availability mode is enabled.
8
+ # @return [Boolean]
9
+ field :ha_enabled, as: :ha_enabled?
10
+
11
+ # @!method leader?
12
+ # Returns whether the Vault server queried is the leader.
13
+ # @return [Boolean]
14
+ field :is_self, as: :leader?
15
+
16
+ # @!attribute [r] address
17
+ # URL where the server is running.
18
+ # @return [String]
19
+ field :leader_address, as: :address
20
+
21
+ # @deprecated Use {#ha_enabled?} instead
22
+ def ha?; ha_enabled?; end
23
+
24
+ # @deprecated Use {#leader?} instead
25
+ def is_leader?; leader?; end
26
+
27
+ # @deprecated Use {#leader?} instead
28
+ def is_self?; leader?; end
29
+
30
+ # @deprecated Use {#leader?} instead
31
+ def self?; leader?; end
32
+ end
33
+
34
+ class Sys
35
+ # Determine the leader status for this vault.
36
+ #
37
+ # @example
38
+ # Vault.sys.leader #=> #<Vault::LeaderStatus ha_enabled=false, is_self=false, leader_address="">
39
+ #
40
+ # @return [LeaderStatus]
41
+ def leader
42
+ json = client.get("/v1/sys/leader")
43
+ return LeaderStatus.decode(json)
44
+ end
45
+
46
+ def step_down
47
+ client.put("/v1/sys/step-down", nil)
48
+ return true
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ module Vault
5
+ class Sys
6
+ # Renew a lease with the given ID.
7
+ #
8
+ # @example
9
+ # Vault.sys.renew("aws/username") #=> #<Vault::Secret ...>
10
+ #
11
+ # @param [String] id
12
+ # the lease ID
13
+ # @param [Fixnum] increment
14
+ #
15
+ # @return [Secret]
16
+ def renew(id, increment = 0)
17
+ json = client.put("/v1/sys/renew/#{id}", JSON.fast_generate(
18
+ increment: increment,
19
+ ))
20
+ return Secret.decode(json)
21
+ end
22
+
23
+ # Revoke the secret at the given id. If the secret does not exist, an error
24
+ # will be raised.
25
+ #
26
+ # @example
27
+ # Vault.sys.revoke("aws/username") #=> true
28
+ #
29
+ # @param [String] id
30
+ # the lease ID
31
+ #
32
+ # @return [true]
33
+ def revoke(id)
34
+ client.put("/v1/sys/revoke/#{id}", nil)
35
+ return true
36
+ end
37
+
38
+ # Revoke all secrets under the given prefix.
39
+ #
40
+ # @example
41
+ # Vault.sys.revoke_prefix("aws") #=> true
42
+ #
43
+ # @param [String] id
44
+ # the lease ID
45
+ #
46
+ # @return [true]
47
+ def revoke_prefix(id)
48
+ client.put("/v1/sys/revoke-prefix/#{id}", nil)
49
+ return true
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,165 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ require "json"
5
+
6
+ module Vault
7
+ class Mount < Response
8
+ # @!attribute [r] config
9
+ # Arbitrary configuration for the backend.
10
+ # @return [Hash<Symbol, Object>]
11
+ field :config
12
+
13
+ # @!attribute [r] description
14
+ # Description of the mount.
15
+ # @return [String]
16
+ field :description
17
+
18
+ # @!attribute [r] type
19
+ # Type of the mount.
20
+ # @return [String]
21
+ field :type
22
+
23
+ # @!attribute [r] type
24
+ # Options given to the mount.
25
+ # @return [Hash<Symbol, Object>]
26
+ field :options
27
+ end
28
+
29
+ class MountTune < Response
30
+ # @!attribute [r] description
31
+ # Specifies the description of the mount.
32
+ # @return [String]
33
+ field :description
34
+
35
+ # @!attribute [r] default_lease_ttl
36
+ # Specifies the default time-to-live.
37
+ # @return [Fixnum]
38
+ field :default_lease_ttl
39
+
40
+ # @!attribute [r] max_lease_ttl
41
+ # Specifies the maximum time-to-live.
42
+ # @return [Fixnum]
43
+ field :max_lease_ttl
44
+
45
+ # @!attribute [r] audit_non_hmac_request_keys
46
+ # Specifies the comma-separated list of keys that will not be HMAC'd by audit devices in the request data object.
47
+ # @return [Array<String>]
48
+ field :audit_non_hmac_request_keys
49
+
50
+ # @!attribute [r] audit_non_hmac_response_keys
51
+ # Specifies the comma-separated list of keys that will not be HMAC'd by audit devices in the response data object.
52
+ # @return [Array<String>]
53
+ field :audit_non_hmac_response_keys
54
+
55
+ # @!attribute [r] listing_visibility
56
+ # Specifies whether to show this mount in the UI-specific listing endpoint.
57
+ # @return [String]
58
+ field :listing_visibility
59
+
60
+ # @!attribute [r] passthrough_request_headers
61
+ # Comma-separated list of headers to whitelist and pass from the request to the plugin.
62
+ # @return [Array<String>]
63
+ field :passthrough_request_headers
64
+
65
+ # @!attribute [r] allowed_response_headers
66
+ # Comma-separated list of headers to whitelist, allowing a plugin to include them in the response.
67
+ # @return [Array<String>]
68
+ field :allowed_response_headers
69
+ end
70
+
71
+ class Sys < Request
72
+ # List all mounts in the vault.
73
+ #
74
+ # @example
75
+ # Vault.sys.mounts #=> { :secret => #<struct Vault::Mount type="generic", description="generic secret storage"> }
76
+ #
77
+ # @return [Hash<Symbol, Mount>]
78
+ def mounts
79
+ json = client.get("/v1/sys/mounts")
80
+ json = json[:data] if json[:data]
81
+ return Hash[*json.map do |k,v|
82
+ [k.to_s.chomp("/").to_sym, Mount.decode(v)]
83
+ end.flatten]
84
+ end
85
+
86
+ # Create a mount at the given path.
87
+ #
88
+ # @example
89
+ # Vault.sys.mount("pg", "postgresql", "Postgres user management") #=> true
90
+ #
91
+ # @param [String] path
92
+ # the path to mount at
93
+ # @param [String] type
94
+ # the type of mount
95
+ # @param [String] description
96
+ # a human-friendly description (optional)
97
+ def mount(path, type, description = nil, options = {})
98
+ payload = options.merge type: type
99
+ payload[:description] = description if !description.nil?
100
+
101
+ client.post("/v1/sys/mounts/#{encode_path(path)}", JSON.fast_generate(payload))
102
+ return true
103
+ end
104
+
105
+ # Get the mount tunings at a given path.
106
+ #
107
+ # @example
108
+ # Vault.sys.get_mount_tune("pki") #=> { :pki => #<struct Vault::MountTune default_lease_ttl=2764800> }
109
+ #
110
+ # @return [MountTune]
111
+ def get_mount_tune(path)
112
+ json = client.get("/v1/sys/mounts/#{encode_path(path)}/tune")
113
+ json = json[:data] if json[:data]
114
+ return MountTune.decode(json)
115
+ end
116
+
117
+ # Tune a mount at the given path.
118
+ #
119
+ # @example
120
+ # Vault.sys.mount_tune("pki", max_lease_ttl: '87600h') #=> true
121
+ #
122
+ # @param [String] path
123
+ # the path to write
124
+ # @param [Hash] data
125
+ # the data to write
126
+ def mount_tune(path, data = {})
127
+ json = client.post("/v1/sys/mounts/#{encode_path(path)}/tune", JSON.fast_generate(data))
128
+ return true
129
+ end
130
+
131
+ # Unmount the thing at the given path. If the mount does not exist, an error
132
+ # will be raised.
133
+ #
134
+ # @example
135
+ # Vault.sys.unmount("pg") #=> true
136
+ #
137
+ # @param [String] path
138
+ # the path to unmount
139
+ #
140
+ # @return [true]
141
+ def unmount(path)
142
+ client.delete("/v1/sys/mounts/#{encode_path(path)}")
143
+ return true
144
+ end
145
+
146
+ # Change the name of the mount
147
+ #
148
+ # @example
149
+ # Vault.sys.remount("pg", "postgres") #=> true
150
+ #
151
+ # @param [String] from
152
+ # the origin mount path
153
+ # @param [String] to
154
+ # the new mount path
155
+ #
156
+ # @return [true]
157
+ def remount(from, to)
158
+ client.post("/v1/sys/remount", JSON.fast_generate(
159
+ from: from,
160
+ to: to,
161
+ ))
162
+ return true
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,86 @@
1
+ # Copyright (c) HashiCorp, Inc.
2
+ # SPDX-License-Identifier: MPL-2.0
3
+
4
+ module Vault
5
+ class Namespace < Response
6
+ # @!attribute [r] id
7
+ # ID of the namespace
8
+ # @return [String]
9
+ field :id
10
+
11
+ # @!attribute [r] path
12
+ # Path of the namespace, includes parent paths if nested.
13
+ # @return [String]
14
+ field :path
15
+ end
16
+
17
+ class Sys
18
+ # List all namespaces in a given scope. Ignores nested namespaces.
19
+ #
20
+ # @example
21
+ # Vault.sys.namespaces #=> { :foo => #<struct Vault::Namespace id="xxxx1", path="foo/" }
22
+ #
23
+ # @return [Hash<Symbol, Namespace>]
24
+ def namespaces(scoped=nil)
25
+ path = ["v1", scoped, "sys", "namespaces"].compact
26
+ json = client.list(path.join("/"))
27
+ json = json[:data] if json[:data]
28
+ if json[:key_info]
29
+ json = json[:key_info]
30
+ hash = {}
31
+ json.each do |k,v|
32
+ hash[k.to_s.chomp("/").to_sym] = Namespace.decode(v)
33
+ end
34
+ hash
35
+ else
36
+ json
37
+ end
38
+ end
39
+
40
+ # Create a namespace. Nests the namespace if a namespace header is provided.
41
+ #
42
+ # @example
43
+ # Vault.sys.create_namespace("foo")
44
+ #
45
+ # @param [String] namespace
46
+ # the potential path of the namespace, without any parent path provided
47
+ #
48
+ # @return [true]
49
+ def create_namespace(namespace)
50
+ client.put("/v1/sys/namespaces/#{namespace}", {})
51
+ return true
52
+ end
53
+
54
+ # Delete a namespace. Raises an error if the namespace provided is not empty.
55
+ #
56
+ # @example
57
+ # Vault.sys.delete_namespace("foo")
58
+ #
59
+ # @param [String] namespace
60
+ # the path of the namespace to be deleted
61
+ #
62
+ # @return [true]
63
+ def delete_namespace(namespace)
64
+ client.delete("/v1/sys/namespaces/#{namespace}")
65
+ return true
66
+ end
67
+
68
+ # Retrieve a namespace by path.
69
+ #
70
+ # @example
71
+ # Vault.sys.get_namespace("foo")
72
+ #
73
+ # @param [String] namespace
74
+ # the path of the namespace ot be retrieved
75
+ #
76
+ # @return [Namespace]
77
+ def get_namespace(namespace)
78
+ json = client.get("/v1/sys/namespaces/#{namespace}")
79
+ if data = json.dig(:data)
80
+ Namespace.decode(data)
81
+ else
82
+ json
83
+ end
84
+ end
85
+ end
86
+ end