adobe_pdfservices_ruby 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfServices
4
+ module Base
5
+ class Operation
6
+ BASE_ENDPOINT = 'https://pdf-services-ue1.adobe.io/operation/'
7
+ ASSETS_ENDPOINT = 'https://pdf-services-ue1.adobe.io/assets/'
8
+ STATUS = {
9
+ in_progress: 'in progress',
10
+ done: 'done',
11
+ failed: 'failed'
12
+ }.freeze
13
+
14
+ def initialize(api)
15
+ @api = api
16
+ end
17
+
18
+ def upload_asset(asset)
19
+ asset = File.open(asset, 'rb') if asset.is_a?(String)
20
+ Asset.new(@api).upload(asset)
21
+ end
22
+
23
+ def poll_document_result(url, original_asset, &block)
24
+ response = @api.get(url)
25
+ json_response = JSON.parse(response.body.to_s)
26
+ handle_polling_result(url, json_response, original_asset, &block)
27
+ end
28
+
29
+ private
30
+
31
+ def handle_polling_result(url, json_response, original_asset, &block)
32
+ case json_response['status']
33
+ when STATUS[:in_progress]
34
+ handle_in_progress(url, json_response, original_asset, &block)
35
+ when STATUS[:done]
36
+ handle_done(json_response, original_asset, &block)
37
+ when STATUS[:failed]
38
+ handle_failed(json_response, original_asset, &block)
39
+ else
40
+ handle_unexpected_status(json_response, original_asset, &block)
41
+ end
42
+ end
43
+
44
+ def handle_in_progress(url, json_response, original_asset, &block)
45
+ block.call(json_response['status'], nil) if block_given?
46
+ sleep(1) # Consider a more sophisticated retry strategy
47
+ poll_document_result(url, original_asset, &block)
48
+ end
49
+
50
+ def handle_done(json_response, original_asset, &block)
51
+ return block.call(json_response['status'], handle_polling_done(json_response, original_asset)) if block_given?
52
+
53
+ handle_polling_done(json_response, original_asset)
54
+ end
55
+
56
+ def handle_failed(json_response, original_asset, &block)
57
+ block.call(json_response['status'], nil) if block_given?
58
+ handle_polling_failed(json_response, original_asset)
59
+ end
60
+
61
+ def handle_unexpected_status(json_response, original_asset, &block)
62
+ block.call(json_response['status'], nil) if block_given?
63
+ handle_polling_unexpected_status(json_response, original_asset)
64
+ end
65
+
66
+ def handle_polling_done(_json_response, original_asset)
67
+ original_asset.delete
68
+ end
69
+
70
+ def handle_polling_failed(json_response, original_asset)
71
+ original_asset.delete
72
+ raise PollingError, "Document extraction failed: #{json_response['error']}"
73
+ end
74
+
75
+ def handle_polling_unexpected_status(json_response, original_asset)
76
+ original_asset.delete
77
+ raise PollingError, "Unexpected status: #{json_response['status']}"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfServices
4
+ module DocumentGeneration
5
+ class External < Operation
6
+ EXTERNAL_OPTIONS = %i[input output params download_from_external].freeze
7
+ INPUT_KEYS = %i[uri storage].freeze
8
+ OUTPUT_KEYS = %i[uri storage].freeze
9
+ PARAMS_KEYS = %i[output_format json_data_for_merge fragments].freeze
10
+ STORAGE_OPTIONS = %i[S3 SHAREPOINT DROPBOX BLOB].freeze
11
+
12
+ def initialize(api)
13
+ super
14
+ @download_from_external = false
15
+ @download_uri = nil
16
+ end
17
+
18
+ def execute(template_path, options = {})
19
+ validate_options(options)
20
+ asset = upload_asset(template_path)
21
+
22
+ @download_from_external = options[:download_from_external] || false
23
+
24
+ @download_uri = options[:output][:uri] if @download_from_external
25
+
26
+ response = @api.post(OPERATION_ENDPOINT,
27
+ body: request_body(asset.id, options),
28
+ headers: request_headers)
29
+ handle_response(response, asset.id)
30
+ end
31
+
32
+ private
33
+
34
+ def request_body(asset_id, options)
35
+ {
36
+ assetID: asset_id,
37
+ input: camelize_keys(options[:input]),
38
+ output: camelize_keys(options[:output]),
39
+ params: camelize_keys(options[:params])
40
+ }
41
+ end
42
+
43
+ def handle_polling_done(response, original_asset)
44
+ file = @api.get(@download_uri).body if @download_from_external
45
+ super
46
+ file || true
47
+ end
48
+
49
+ def validate_options(options)
50
+ raise ArgumentError, 'Options must be a hash' unless options.is_a?(Hash)
51
+
52
+ validate_required_keys(options)
53
+ validate_input_options(options[:input])
54
+ validate_output_options(options[:output])
55
+ validate_params_options(options[:params])
56
+ end
57
+
58
+ def validate_required_keys(options)
59
+ required_keys = EXTERNAL_OPTIONS - %i[download_from_external]
60
+ required_keys.each do |key|
61
+ raise ArgumentError, "Missing required key: #{key}" unless options.key?(key)
62
+ end
63
+ end
64
+
65
+ def validate_input_options(input_options)
66
+ raise ArgumentError, 'Input options must be a hash' unless input_options.is_a?(Hash)
67
+
68
+ required_input_keys = INPUT_KEYS
69
+ required_input_keys.each do |key|
70
+ raise ArgumentError, "Missing required input key: #{key}" unless input_options.key?(key)
71
+ end
72
+ validate_storage_options(input_options[:storage])
73
+ end
74
+
75
+ def validate_output_options(output_options)
76
+ raise ArgumentError, 'Output options must be a hash' unless output_options.is_a?(Hash)
77
+
78
+ required_output_keys = OUTPUT_KEYS - %i[uri]
79
+ required_output_keys.each do |key|
80
+ raise ArgumentError, "Missing required output key: #{key}" unless output_options.key?(key)
81
+ end
82
+ validate_storage_options(output_options[:storage])
83
+ end
84
+
85
+ def validate_storage_option(storage_option)
86
+ raise ArgumentError, "Invalid storage option: #{storage_option}" unless STORAGE_OPTIONS.include?(storage_option)
87
+ end
88
+
89
+ def validate_params_options(params_options)
90
+ raise ArgumentError, 'Params options must be a hash' unless params_options.is_a?(Hash)
91
+
92
+ required_params_keys = PARAMS_KEYS - %i[output_format fragments]
93
+ required_params_keys.each do |key|
94
+ raise ArgumentError, "Missing required params key: #{key}" unless params_options.key?(key)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfServices
4
+ module DocumentGeneration
5
+ class Internal < Operation
6
+ INTERNAL_OPTIONS = %i[output_format json_data_for_merge fragments notifiers].freeze
7
+
8
+ def execute(template_path, options = {})
9
+ validate_options(options)
10
+ asset = upload_asset(template_path)
11
+
12
+ response = @api.post(OPERATION_ENDPOINT,
13
+ body: request_body(asset.id, options),
14
+ headers: request_headers)
15
+
16
+ handle_response(response, asset.id)
17
+ end
18
+
19
+ private
20
+
21
+ def request_body(asset_id, options)
22
+ body = {
23
+ assetID: asset_id,
24
+ outputFormat: options.fetch(:output_format, 'pdf'),
25
+ jsonDataForMerge: options.fetch(:json_data_for_merge, {})
26
+ }
27
+ body[:fragments] = options[:fragments] if options[:fragments]
28
+ body[:notifiers] = options[:notifiers] if options[:notifiers]
29
+ body
30
+ end
31
+
32
+ def validate_options(options)
33
+ raise ArgumentError, 'Invalid options' unless options.is_a?(Hash)
34
+
35
+ validate_output_format(options[:output_format])
36
+ validate_fragments(options[:fragments])
37
+ validate_notifiers(options[:notifiers])
38
+ validate_keys(options)
39
+ validate_json_data_for_merge(options[:json_data_for_merge])
40
+ end
41
+
42
+ def validate_output_format(output_format)
43
+ raise ArgumentError, "Invalid output format: #{output_format}" unless %w[pdf docx].include?(output_format)
44
+ end
45
+
46
+ def validate_fragments(fragments)
47
+ raise ArgumentError, 'Invalid fragments, must be a hash' unless fragments.is_a?(Hash) || fragments.nil?
48
+ end
49
+
50
+ def validate_notifiers(notifiers)
51
+ return if notifiers.is_a?(Array) || notifiers.nil?
52
+
53
+ raise ArgumentError,
54
+ 'Invalid notifiers, must be an array of hashes'
55
+ end
56
+
57
+ def validate_json_data_for_merge(json_data_for_merge)
58
+ raise ArgumentError, 'Invalid json_data_for_merge, must be a hash' unless json_data_for_merge.is_a?(Hash)
59
+ end
60
+
61
+ def validate_keys(options)
62
+ valid_keys = INTERNAL_OPTIONS
63
+ invalid_keys = options.keys - valid_keys
64
+ raise ArgumentError, "Invalid options: #{invalid_keys}" unless invalid_keys.empty?
65
+ end
66
+
67
+ def handle_polling_done(json_response, original_asset)
68
+ asset_id = json_response['asset']['assetID']
69
+ file = Asset.new(@api).download(asset_id).body
70
+ super
71
+ file
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfServices
4
+ module DocumentGeneration
5
+ class Operation < InternalExternalOperation::Operation
6
+ OPERATION_ENDPOINT = "#{BASE_ENDPOINT}documentgeneration".freeze
7
+
8
+ def request_headers
9
+ { 'Content-Type' => 'application/json' }
10
+ end
11
+
12
+ def handle_response(response, asset_id)
13
+ unless response.status == 201
14
+ raise "Unexpected response status from document merge endpoint: #{response.status}, asset_id: #{asset_id}"
15
+ end
16
+
17
+ document_url = response.headers['location']
18
+ poll_document_result document_url, asset_id
19
+ end
20
+
21
+ def internal_class
22
+ Internal
23
+ end
24
+
25
+ def external_class
26
+ External
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfServices
4
+ module ExtractPdf
5
+ class Operation < Base::Operation
6
+ OPERATION_ENDPOINT = "#{BASE_ENDPOINT}extractpdf".freeze
7
+ VALID_EXTRACT_ELEMENTS = %w[tables text].freeze
8
+ TABLE_OUTPUT_FORMATS = %w[csv xlsx].freeze
9
+ RENDITIONS_EXTRACTS = %w[tables figures].freeze
10
+
11
+ def execute(source_pdf_path = nil, options = {})
12
+ validate_options(options)
13
+ @download_zip = options.delete(:download_zip) || false
14
+ asset = upload_asset(source_pdf_path)
15
+
16
+ response = @api.post(OPERATION_ENDPOINT,
17
+ body: extract_pdf_request_body(asset.id, options),
18
+ headers: extract_pdf_request_headers)
19
+ handle_extract_pdf_response(response, asset)
20
+ end
21
+
22
+ private
23
+
24
+ def extract_pdf_request_body(asset_id, options)
25
+ {
26
+ assetID: asset_id,
27
+ includeStyling: options.fetch(:include_styling, false),
28
+ getCharBounds: options.fetch(:get_char_bounds, false),
29
+ renditionsToExtract: options.fetch(:renditions_to_extract, RENDITIONS_EXTRACTS),
30
+ tableOutputFormat: options.fetch(:table_output_format, TABLE_OUTPUT_FORMATS.first),
31
+ elementsToExtract: options.fetch(:extract_elements, VALID_EXTRACT_ELEMENTS)
32
+ }
33
+ end
34
+
35
+ def extract_pdf_request_headers
36
+ { 'Content-Type' => 'application/json' }
37
+ end
38
+
39
+ def handle_extract_pdf_response(response, asset)
40
+ raise OperationError, "Extract PDF operation failed: #{response.body}" unless response.status == 201
41
+
42
+ polling_url = response.headers['location']
43
+ poll_document_result polling_url, asset
44
+ end
45
+
46
+ def handle_polling_done(json_response, original_asset)
47
+ file_key = @download_zip ? 'resource' : 'content'
48
+ asset_id = json_response[file_key]['assetID']
49
+ file = Asset.new(@api).download(asset_id).body
50
+ super
51
+ file
52
+ end
53
+
54
+ def validate_options(options)
55
+ validate_renditions_to_extract(options[:renditions_to_extract])
56
+ validate_table_output_format(options[:table_output_format])
57
+ validate_extract_elements(options[:extract_elements])
58
+ end
59
+
60
+ def validate_renditions_to_extract(renditions_to_extract)
61
+ renditions_to_extract ||= RENDITIONS_EXTRACTS
62
+ return if (renditions_to_extract - RENDITIONS_EXTRACTS).empty?
63
+
64
+ raise ArgumentError, "Invalid renditions_to_extract: #{renditions_to_extract}"
65
+ end
66
+
67
+ def validate_table_output_format(table_output_format)
68
+ table_output_format ||= TABLE_OUTPUT_FORMATS.first
69
+ return if TABLE_OUTPUT_FORMATS.include?(table_output_format)
70
+
71
+ raise ArgumentError, "Invalid table_output_format: #{table_output_format}"
72
+ end
73
+
74
+ def validate_extract_elements(extract_elements)
75
+ extract_elements ||= VALID_EXTRACT_ELEMENTS
76
+ invalid_elements = extract_elements - VALID_EXTRACT_ELEMENTS
77
+ return if invalid_elements.empty?
78
+
79
+ raise ArgumentError, "Invalid extract_elements: #{invalid_elements}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfServices
4
+ module HtmlToPdf
5
+ class External < Operation
6
+ EXTERNAL_OPTIONS = %i[input output params download_from_external].freeze
7
+ INPUT_KEYS = %i[uri storage].freeze
8
+ OUTPUT_KEYS = %i[uri storage].freeze
9
+ PARAMS_KEYS = %i[include_header_footer json page_layout].freeze
10
+ STORAGE_OPTIONS = %i[S3 SHAREPOINT DROPBOX BLOB].freeze
11
+
12
+ def initialize(api)
13
+ super
14
+ @download_from_external = false
15
+ @download_uri = nil
16
+ end
17
+
18
+ def execute(url, options = {})
19
+ @download_from_external = options[:download_from_external] || false
20
+ options[:input][:uri] = options[:input][:uri] || url
21
+ validate_options(options)
22
+
23
+ @download_uri = options[:output][:uri] if @download_from_external
24
+
25
+ response = @api.post(OPERATION_ENDPOINT,
26
+ body: request_body(asset.id, options),
27
+ headers: request_headers)
28
+ handle_response(response, asset.id)
29
+ end
30
+
31
+ private
32
+
33
+ def request_body(asset_id, options)
34
+ {
35
+ assetID: asset_id,
36
+ input: camelize_keys(options[:input]),
37
+ output: camelize_keys(options[:output]),
38
+ params: camelize_keys(options[:params])
39
+ }
40
+ end
41
+
42
+ def handle_polling_done(_json_response, original_asset)
43
+ file = @api.get(@download_uri).body if @download_from_external
44
+ super
45
+ file || true
46
+ end
47
+
48
+ def validate_options(options)
49
+ raise ArgumentError, 'Options must be a hash' unless options.is_a?(Hash)
50
+
51
+ validate_required_keys(options)
52
+ validate_input_options(options[:input])
53
+ validate_output_options(options[:output])
54
+ end
55
+
56
+ def validate_required_keys(options)
57
+ required_keys = EXTERNAL_OPTIONS - %i[download_from_external]
58
+ required_keys.each do |key|
59
+ raise ArgumentError, "Missing required key: #{key}" unless options.key?(key)
60
+ end
61
+ end
62
+
63
+ def validate_input_options(input_options)
64
+ raise ArgumentError, 'Input options must be a hash' unless input_options.is_a?(Hash)
65
+
66
+ required_input_keys = INPUT_KEYS
67
+ required_input_keys.each do |key|
68
+ raise ArgumentError, "Missing required input key: #{key}" unless input_options.key?(key)
69
+ end
70
+ validate_storage_options(input_options[:storage])
71
+ end
72
+
73
+ def validate_output_options(output_options)
74
+ raise ArgumentError, 'Output options must be a hash' unless output_options.is_a?(Hash)
75
+
76
+ unless output_options.key?(:uri) && @download_from_external
77
+ raise ArgumentError,
78
+ 'Output options must contain uri when downloading from external storage'
79
+ end
80
+
81
+ required_output_keys = OUTPUT_KEYS - %i[uri]
82
+ required_output_keys.each do |key|
83
+ raise ArgumentError, "Missing required output key: #{key}" unless output_options.key?(key)
84
+ end
85
+ validate_storage_options(output_options[:storage])
86
+ end
87
+
88
+ def validate_storage_option(storage_option)
89
+ return if STORAGE_OPTIONS.include?(storage_option)
90
+
91
+ raise ArgumentError,
92
+ "Invalid storage option: #{storage_option}"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfServices
4
+ module HtmlToPdf
5
+ class Internal < Operation
6
+ INTERNAL_OPTIONS = %i[input_url json include_header_footer page_layout notifiers].freeze
7
+ PAGE_LAYOUT_OPTIONS = %i[page_width page_height].freeze
8
+
9
+ def execute(html_file_path, options = {})
10
+ validate_options(options)
11
+ asset = upload_asset(html_file_path)
12
+
13
+ response = @api.post(OPERATION_ENDPOINT,
14
+ body: request_body(asset.id, options),
15
+ headers: request_headers)
16
+
17
+ handle_response(response, asset.id)
18
+ end
19
+
20
+ private
21
+
22
+ def handle_polling_done(json_response, _original_asset_id)
23
+ asset_id = json_response['asset']['assetID']
24
+ Asset.new(@api).download(asset_id).body
25
+ end
26
+
27
+ def request_body(asset_id, options)
28
+ body = {
29
+ assetID: asset_id,
30
+ inputUrl: options.fetch(:input_url, ''),
31
+ pageLayout: camelize_keys(options.fetch(:page_layout, {})),
32
+ json: transform_json(options.fetch(:json, ''))
33
+ }
34
+ body[:notifiers] = options[:notifiers] if options[:notifiers]
35
+ body
36
+ end
37
+
38
+ def transform_json(json)
39
+ json.is_a?(String) ? json : json.to_json
40
+ end
41
+
42
+ def validate_options(options)
43
+ raise ArgumentError, 'Invalid options' unless options.is_a?(Hash)
44
+
45
+ options.each_key do |key|
46
+ raise ArgumentError, "Invalid option: #{key}" unless INTERNAL_OPTIONS.include?(key)
47
+ end
48
+
49
+ validate_required_keys(options)
50
+ validate_page_layout_options(options[:page_layout]) if options[:page_layout]
51
+ end
52
+
53
+ def validate_required_keys(options)
54
+ required_keys = INTERNAL_OPTIONS - %i[page_layout notifiers]
55
+ required_keys.each do |key|
56
+ raise ArgumentError, "Missing required option: #{key}" unless options.key?(key)
57
+ end
58
+ end
59
+
60
+ def validate_page_layout_options(options)
61
+ raise ArgumentError, 'Invalid page layout options' unless options.is_a?(Hash)
62
+
63
+ options.each_key do |key|
64
+ raise ArgumentError, "Invalid page layout option: #{key}" unless PAGE_LAYOUT_OPTIONS.include?(key)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfServices
4
+ module HtmlToPdf
5
+ class Operation < InternalExternalOperation::Operation
6
+ OPERATION_ENDPOINT = "#{BASE_ENDPOINT}htmltopdf".freeze
7
+
8
+ def request_headers
9
+ { 'Content-Type' => 'application/json' }
10
+ end
11
+
12
+ def handle_response(response, asset_id)
13
+ unless response.status == 201
14
+ raise "Unexpected response status from document merge endpoint: #{response.status}, asset_id: #{asset_id}"
15
+ end
16
+
17
+ document_url = response.headers['location']
18
+ poll_document_result document_url, asset_id
19
+ end
20
+
21
+ def internal_class
22
+ Internal
23
+ end
24
+
25
+ def external_class
26
+ External
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ module PdfServices
2
+ module InternalExternalOperation
3
+ class Operation < Base::Operation
4
+ def execute(source_file_path, options = {})
5
+ operation_class = switch_on_type(options)
6
+ operation_class.new(@api).execute(source_file_path, options)
7
+ end
8
+
9
+ private
10
+
11
+ def switch_on_type(options)
12
+ type = options[:type] || :internal
13
+ case type
14
+ when :internal
15
+ internal_class
16
+ when :external
17
+ external_class
18
+ end
19
+ end
20
+
21
+ def internal_class
22
+ raise NotImplementedError, 'Subclasses must implement this method'
23
+ end
24
+
25
+ def external_class
26
+ raise NotImplementedError, 'Subclasses must implement this method'
27
+ end
28
+
29
+ def camelize_keys(hash)
30
+ hash.transform_keys { |key| camelize(key.to_s) }
31
+ end
32
+
33
+ def camelize(str)
34
+ caps = str.split('_').map(&:capitalize)
35
+ caps[0].downcase!
36
+ caps.join
37
+ end
38
+ end
39
+ end
40
+ end