adobe_doc_api 0.2.1 → 0.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -1
- data/README.md +5 -15
- data/lib/adobe_doc_api/client.rb +144 -98
- data/lib/adobe_doc_api/configuration.rb +3 -5
- data/lib/adobe_doc_api/version.rb +1 -1
- metadata +7 -50
- data/Gemfile.lock +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88a17a7824cbc56df0cc3b1a4fe8ce43e51ac80d2f49c8aeb2bf3bd3d3afc4b6
|
4
|
+
data.tar.gz: 4ab99d5fd436027b5a8dfa58610969ec6955cd592474e55ac0285def3c4f62ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0298bd962026cc2c072239d0a8dda4fbca883b94389f40daa5efc843a2256b239a52bb3c21b4900c3098338b10f78e47d22c52745527b1df739ea80c46bcd3e
|
7
|
+
data.tar.gz: e8c44532b30fe23d98076fa67f5ce0b49d9def18f5593142fbed19f75410025d5461d7f87604a48f07f0b1a458f19b4f64283b49bfb77f666b288ba2ae6e5122
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
## [0.3.0] - 2023-06-20
|
2
|
+
- Migrated authentication from JWT to OAuth server-to-server
|
3
|
+
- Updated configuration to accept OAuth scopes
|
4
|
+
|
1
5
|
## [0.2.1] - 2023-06-20
|
2
6
|
- Updates to Adobe PDF Services API
|
3
|
-
- Removed JWT token authentication
|
4
7
|
|
5
8
|
## [0.1.4] - 2021-12-30
|
6
9
|
- Fix issue with parsing file boundary that included \r\n
|
data/README.md
CHANGED
@@ -23,9 +23,7 @@ Or install it yourself as:
|
|
23
23
|
AdobeDocApi.configure do |config|
|
24
24
|
config.client_id = nil
|
25
25
|
config.client_secret = nil
|
26
|
-
config.
|
27
|
-
config.tech_account_id = nil
|
28
|
-
config.private_key_path = nil
|
26
|
+
config.scopes = nil
|
29
27
|
end
|
30
28
|
```
|
31
29
|
### Recommended configuration if using Rails 6+
|
@@ -33,9 +31,7 @@ end
|
|
33
31
|
AdobeDocApi.configure do |config|
|
34
32
|
config.client_id = Rails.application.credentials.dig(:adobe_doc, :client_id)
|
35
33
|
config.client_secret = Rails.application.credentials.dig(:adobe_doc, :client_secret)
|
36
|
-
config.
|
37
|
-
config.tech_account_id = Rails.application.credentials.dig(:adobe_doc, :tech_account_id)
|
38
|
-
config.private_key_path = Rails.application.credentials.dig(:adobe_doc, :private_key_path)
|
34
|
+
config.scopes = Rails.application.credentials.dig(:adobe_doc, :scopes)
|
39
35
|
end
|
40
36
|
```
|
41
37
|
## Usage
|
@@ -51,16 +47,10 @@ client.submit(json: json_data, template: template_path, output: output_path)
|
|
51
47
|
```
|
52
48
|
### Usage without configuration
|
53
49
|
```ruby
|
54
|
-
client = AdobeDocApi::Client.new(
|
55
|
-
|
56
|
-
|
57
|
-
org_id: adobe_org_id,
|
58
|
-
tech_account_id: adobe_tech_account_id,
|
59
|
-
access_token: nil)
|
50
|
+
client = AdobeDocApi::Client.new(client_id: adobe_client_id,
|
51
|
+
client_secret: adobe_client_secret,
|
52
|
+
scopes: adobe_scopes)
|
60
53
|
```
|
61
|
-
## Todo
|
62
|
-
- [x] Add multipart parsing to improve saving the file from the response
|
63
|
-
- [ ] Add documentation
|
64
54
|
|
65
55
|
## Contributing
|
66
56
|
|
data/lib/adobe_doc_api/client.rb
CHANGED
@@ -1,130 +1,176 @@
|
|
1
|
-
require "
|
2
|
-
require "
|
3
|
-
require "jwt"
|
4
|
-
require "openssl"
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
5
3
|
|
6
4
|
module AdobeDocApi
|
7
5
|
class Client
|
8
|
-
|
9
|
-
API_ENDPOINT_URL = "https://
|
6
|
+
OAUTH_URL = "https://ims-na1.adobelogin.com/ims/token/v3".freeze
|
7
|
+
API_ENDPOINT_URL = "https://pdf-services-ue1.adobe.io/operation/documentgeneration"
|
8
|
+
attr_reader :location_url, :raw_response, :client_id, :client_secret, :scopes
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
def initialize(private_key: nil, client_id: nil, client_secret: nil, org_id: nil, tech_account_id: nil, access_token: nil)
|
14
|
-
# TODO Need to validate if any params are missing and return error
|
10
|
+
def initialize(client_id: nil, client_secret: nil, scopes: nil)
|
15
11
|
@client_id = client_id || AdobeDocApi.configuration.client_id
|
16
12
|
@client_secret = client_secret || AdobeDocApi.configuration.client_secret
|
17
|
-
@
|
18
|
-
@tech_account_id = tech_account_id || AdobeDocApi.configuration.tech_account_id
|
19
|
-
@private_key_path = private_key || AdobeDocApi.configuration.private_key_path
|
13
|
+
@scopes = scopes || AdobeDocApi.configuration.scopes
|
20
14
|
@location_url = nil
|
21
15
|
@output_file_path = nil
|
22
16
|
@raw_response = nil
|
23
|
-
@access_token =
|
17
|
+
@access_token = get_access_token
|
24
18
|
end
|
25
19
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
# "https://ims-na1.adobelogin.com/s/ent_documentcloud_sdk" => true,
|
32
|
-
# "aud" => "https://ims-na1.adobelogin.com/c/#{@client_id}",
|
33
|
-
# "exp" => (Time.now.utc + 60).to_i
|
34
|
-
# }
|
35
|
-
#
|
36
|
-
# rsa_private = OpenSSL::PKey::RSA.new File.read(private_key)
|
37
|
-
#
|
38
|
-
# jwt_token = JWT.encode jwt_payload, rsa_private, "RS256"
|
39
|
-
#
|
40
|
-
connection = Faraday.new do |conn|
|
41
|
-
conn.response :json, content_type: "application/json"
|
42
|
-
end
|
43
|
-
# response = connection.post JWT_URL do |req|
|
44
|
-
# req.params["client_id"] = @client_id
|
45
|
-
# req.params["client_secret"] = @client_secret
|
46
|
-
# req.params["jwt_token"] = jwt_token
|
47
|
-
# end
|
48
|
-
scopes = "openid, DCAPI, AdobeID"
|
49
|
-
|
50
|
-
response = connection.post "https://ims-na1.adobelogin.com/ims/token/v3" do |req|
|
51
|
-
req.params["client_id"] = @client_id
|
52
|
-
req.body = "client_secret=#{@client_secret}&grant_type=client_credentials&scope=#{scopes}"
|
53
|
-
end
|
54
|
-
return response.body["access_token"]
|
20
|
+
def submit(json:, template:, output:)
|
21
|
+
@output = output
|
22
|
+
@asset_id, upload_uri = upload_presigned_uri
|
23
|
+
upload_asset(upload_uri, template: template)
|
24
|
+
document_generation(json: json)
|
55
25
|
end
|
56
26
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
27
|
+
private
|
28
|
+
|
29
|
+
def get_access_token
|
30
|
+
url = URI(OAUTH_URL)
|
31
|
+
https = Net::HTTP.new(url.host, url.port)
|
32
|
+
https.use_ssl = true
|
33
|
+
request = Net::HTTP::Post.new(url)
|
34
|
+
request["Content-Type"] = "application/x-www-form-urlencoded"
|
35
|
+
request.body = "grant_type=client_credentials&client_id=#{@client_id}&client_secret=#{@client_secret}&scope=#{@scopes}"
|
36
|
+
response = https.request(request)
|
37
|
+
if response.code.to_i != 200
|
38
|
+
raise Error.new(status_code: response.code, msg: "Failed to get access token: #{response.body}")
|
39
|
+
else
|
40
|
+
puts "Access token retrieved successfully"
|
41
|
+
JSON.parse(response.body)["access_token"]
|
68
42
|
end
|
69
|
-
# Return pre-signed uploadUri and assedID
|
70
|
-
return response.body["assetID"], response.body["uploadUri"]
|
71
43
|
end
|
72
44
|
|
73
|
-
def
|
74
|
-
asset_id, upload_uri = get_asset_id
|
45
|
+
def upload_presigned_uri
|
75
46
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
47
|
+
url = URI("https://pdf-services-ue1.adobe.io/assets")
|
48
|
+
https = Net::HTTP.new(url.host, url.port)
|
49
|
+
https.use_ssl = true
|
50
|
+
request = Net::HTTP::Post.new(url)
|
51
|
+
request["Content-Type"] = "application/json"
|
52
|
+
request["X-API-Key"] = @client_id
|
53
|
+
request["Authorization"] = "bearer #{@access_token}"
|
54
|
+
request.body ='{"mediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}'
|
55
|
+
response = https.request(request)
|
82
56
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
conn.request :authorization, "Bearer", @access_token
|
92
|
-
conn.headers["x-api-key"] = @client_id
|
57
|
+
if response.code.to_i != 200
|
58
|
+
raise Error.new(status_code: response.code, msg: "Failed to create asset: #{response.body}")
|
59
|
+
else
|
60
|
+
puts "Asset created successfully"
|
61
|
+
response_body = JSON.parse(response.body)
|
62
|
+
asset_id = response_body["assetID"]
|
63
|
+
upload_uri = response_body["uploadUri"]
|
64
|
+
return asset_id, upload_uri
|
93
65
|
end
|
94
|
-
|
95
|
-
|
96
|
-
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
def upload_asset(upload_uri, template:)
|
70
|
+
|
71
|
+
# Upload the template to the presigned URI
|
72
|
+
url = URI(upload_uri)
|
73
|
+
https = Net::HTTP.new(url.host, url.port)
|
74
|
+
https.use_ssl = true
|
75
|
+
request = Net::HTTP::Put.new(url)
|
76
|
+
request["Content-Type"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
77
|
+
request.body = File.read(template)
|
78
|
+
response = https.request(request)
|
79
|
+
if response.code.to_i != 200
|
80
|
+
raise Error.new(status_code: response.code, msg: "Failed to upload template: #{response.body}")
|
81
|
+
else
|
82
|
+
puts "Template uploaded successfully"
|
97
83
|
end
|
98
|
-
|
99
|
-
# Begin polling for status of file
|
100
|
-
poll_for_file(res.headers["location"], output)
|
84
|
+
|
101
85
|
end
|
102
86
|
|
103
|
-
|
87
|
+
def document_generation(json:)
|
88
|
+
# Document Generation
|
89
|
+
url = URI("https://pdf-services-ue1.adobe.io/operation/documentgeneration")
|
90
|
+
https = Net::HTTP.new(url.host, url.port)
|
91
|
+
https.use_ssl = true
|
92
|
+
request = Net::HTTP::Post.new(url)
|
93
|
+
request["Content-Type"] = "application/json"
|
94
|
+
request["X-API-Key"] = @client_id
|
95
|
+
request["Authorization"] = "Bearer #{@access_token}"
|
104
96
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
97
|
+
request.body = {"assetID": @asset_id,
|
98
|
+
"outputFormat": "docx",
|
99
|
+
"jsonDataForMerge": json
|
100
|
+
}.to_json
|
101
|
+
|
102
|
+
response = https.request(request)
|
103
|
+
|
104
|
+
if response.code.to_i != 201
|
105
|
+
raise Error.new(status_code: response.code, msg: "Failed to submit document generation request: #{response.body}")
|
106
|
+
else
|
107
|
+
status_url = response.header["location"]
|
108
|
+
puts "Document Generation submitted successfully"
|
109
109
|
end
|
110
|
-
|
110
|
+
|
111
|
+
# Start polling for the status of the document generation
|
112
|
+
poll_status(status_url)
|
113
|
+
end
|
114
|
+
|
115
|
+
def poll_status(status_url, timeout: 30)
|
116
|
+
# Poll for the generated document
|
117
|
+
download_uri = nil
|
111
118
|
loop do
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
119
|
+
timeout -= 1
|
120
|
+
break if timeout <= 0
|
121
|
+
|
122
|
+
# Wait for 5 seconds before checking the status
|
123
|
+
url = URI(status_url)
|
124
|
+
https = Net::HTTP.new(url.host, url.port)
|
125
|
+
https.use_ssl = true
|
126
|
+
request = Net::HTTP::Get.new(url)
|
127
|
+
request["Content-Type"] = "application/json"
|
128
|
+
request["X-API-Key"] = @client_id
|
129
|
+
request["Authorization"] = "Bearer #{@access_token}"
|
130
|
+
response = https.request(request)
|
131
|
+
|
132
|
+
if response.code.to_i != 200
|
133
|
+
raise Error.new(status_code: response.code, msg: "Failed to check document generation status: #{response.body}")
|
134
|
+
end
|
135
|
+
|
136
|
+
response_body = JSON.parse(response.body)
|
137
|
+
puts "Current status: #{response_body['status']}"
|
138
|
+
if response_body["status"] == "done"
|
139
|
+
download_uri = response_body["asset"]["downloadUri"]
|
140
|
+
break
|
141
|
+
elsif response_body["status"] == "failed"
|
142
|
+
raise Error.new(status_code: response.code, msg: "Document generation failed: #{response.body}")
|
118
143
|
else
|
119
|
-
|
120
|
-
raise Error.new(status_code: status["status"], msg: status) if status["status"] != 202
|
144
|
+
puts "Document generation in progress..."
|
121
145
|
end
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
146
|
+
sleep 1
|
147
|
+
end
|
148
|
+
|
149
|
+
# If the download URI is available, proceed to download the document
|
150
|
+
if download_uri
|
151
|
+
download_output(download_uri: download_uri)
|
152
|
+
else
|
153
|
+
raise Error.new(status_code: response.code, msg: "Document generation not completed: #{response.body}")
|
126
154
|
end
|
155
|
+
|
127
156
|
end
|
128
157
|
|
158
|
+
def download_output(download_uri:)
|
159
|
+
# Finally, download the generated document
|
160
|
+
url = URI(download_uri)
|
161
|
+
https = Net::HTTP.new(url.host, url.port)
|
162
|
+
https.use_ssl = true
|
163
|
+
request = Net::HTTP::Get.new(url)
|
164
|
+
response = https.request(request)
|
165
|
+
if response.code.to_i != 200
|
166
|
+
raise Error.new(status_code: response.code, msg: "Failed to download document: #{response.body}")
|
167
|
+
else
|
168
|
+
if File.open(@output, "wb") { |f| f.write response.body }
|
169
|
+
puts "Document saved successfully to #{@output}"
|
170
|
+
return true
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
129
174
|
end
|
175
|
+
|
130
176
|
end
|
@@ -1,13 +1,11 @@
|
|
1
1
|
module AdobeDocApi
|
2
2
|
class Configuration
|
3
|
-
attr_accessor :client_id, :client_secret, :
|
3
|
+
attr_accessor :client_id, :client_secret, :scopes
|
4
4
|
|
5
5
|
def initialize
|
6
6
|
@client_id = nil
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@tech_account_id = nil
|
10
|
-
@private_key_path = nil
|
7
|
+
@client_secret = nil
|
8
|
+
@scopes = nil
|
11
9
|
end
|
12
10
|
end
|
13
11
|
end
|
metadata
CHANGED
@@ -1,71 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: adobe_doc_api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Sonnier
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-06-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: json
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: faraday_middleware
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1.2'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.2'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: jwt
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 2.3.0
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 2.3.0
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: openssl
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 2.2.1
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 2.2.1
|
26
|
+
version: '2.0'
|
69
27
|
description: Ruby interface for Adobe PDF Services API Document Generation
|
70
28
|
email:
|
71
29
|
- christopher.sonnier@gmail.com
|
@@ -76,7 +34,6 @@ files:
|
|
76
34
|
- CHANGELOG.md
|
77
35
|
- CODE_OF_CONDUCT.md
|
78
36
|
- Gemfile
|
79
|
-
- Gemfile.lock
|
80
37
|
- LICENSE.txt
|
81
38
|
- README.md
|
82
39
|
- Rakefile
|
@@ -102,14 +59,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
59
|
requirements:
|
103
60
|
- - ">="
|
104
61
|
- !ruby/object:Gem::Version
|
105
|
-
version:
|
62
|
+
version: 3.0.0
|
106
63
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
64
|
requirements:
|
108
65
|
- - ">="
|
109
66
|
- !ruby/object:Gem::Version
|
110
67
|
version: '0'
|
111
68
|
requirements: []
|
112
|
-
rubygems_version: 3.
|
69
|
+
rubygems_version: 3.4.10
|
113
70
|
signing_key:
|
114
71
|
specification_version: 4
|
115
72
|
summary: Ruby interface for Adobe PDF Services API Document Generation
|
data/Gemfile.lock
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
adobe_doc_api (0.2.0)
|
5
|
-
faraday (~> 1.8)
|
6
|
-
faraday_middleware (~> 1.2)
|
7
|
-
jwt (~> 2.3.0)
|
8
|
-
openssl (~> 2.2.1)
|
9
|
-
|
10
|
-
GEM
|
11
|
-
remote: https://rubygems.org/
|
12
|
-
specs:
|
13
|
-
faraday (1.10.3)
|
14
|
-
faraday-em_http (~> 1.0)
|
15
|
-
faraday-em_synchrony (~> 1.0)
|
16
|
-
faraday-excon (~> 1.1)
|
17
|
-
faraday-httpclient (~> 1.0)
|
18
|
-
faraday-multipart (~> 1.0)
|
19
|
-
faraday-net_http (~> 1.0)
|
20
|
-
faraday-net_http_persistent (~> 1.0)
|
21
|
-
faraday-patron (~> 1.0)
|
22
|
-
faraday-rack (~> 1.0)
|
23
|
-
faraday-retry (~> 1.0)
|
24
|
-
ruby2_keywords (>= 0.0.4)
|
25
|
-
faraday-em_http (1.0.0)
|
26
|
-
faraday-em_synchrony (1.0.0)
|
27
|
-
faraday-excon (1.1.0)
|
28
|
-
faraday-httpclient (1.0.1)
|
29
|
-
faraday-multipart (1.0.4)
|
30
|
-
multipart-post (~> 2)
|
31
|
-
faraday-net_http (1.0.1)
|
32
|
-
faraday-net_http_persistent (1.2.0)
|
33
|
-
faraday-patron (1.0.0)
|
34
|
-
faraday-rack (1.0.0)
|
35
|
-
faraday-retry (1.0.3)
|
36
|
-
faraday_middleware (1.2.0)
|
37
|
-
faraday (~> 1.0)
|
38
|
-
ipaddr (1.2.5)
|
39
|
-
jwt (2.3.0)
|
40
|
-
minitest (5.15.0)
|
41
|
-
multipart-post (2.3.0)
|
42
|
-
openssl (2.2.3)
|
43
|
-
ipaddr
|
44
|
-
rake (13.0.6)
|
45
|
-
ruby2_keywords (0.0.5)
|
46
|
-
|
47
|
-
PLATFORMS
|
48
|
-
x86_64-darwin-19
|
49
|
-
x86_64-linux
|
50
|
-
|
51
|
-
DEPENDENCIES
|
52
|
-
adobe_doc_api!
|
53
|
-
minitest (~> 5.0)
|
54
|
-
rake (~> 13.0)
|
55
|
-
|
56
|
-
BUNDLED WITH
|
57
|
-
2.2.32
|