├── .gitattributes
├── .hound.yml
├── lib
├── breezy_pdf_lite
│ ├── version.rb
│ ├── intercept
│ │ ├── base.rb
│ │ └── html.rb
│ ├── intercept.rb
│ ├── client.rb
│ ├── middleware.rb
│ ├── render_request.rb
│ ├── response.rb
│ ├── util.rb
│ └── interceptor.rb
└── breezy_pdf_lite.rb
├── .gitignore
├── bin
├── setup
├── console
└── release
├── .travis.yml
├── Gemfile
├── test
├── breezy_pdf_lite_test.rb
├── middleware_test.rb
├── client_test.rb
├── render_request_test.rb
├── intercept
│ └── html_test.rb
├── test_helper.rb
└── interceptor_test.rb
├── Rakefile
├── .github
├── dependabot.yml
└── workflows
│ ├── lint.yml
│ └── test.yml
├── example
├── config.ru
├── pragmatic.rb
└── ex.html
├── breezy_pdf_lite.gemspec
├── Gemfile.lock
├── README.md
└── LICENSE.txt
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.html linguist-detectable=false
2 |
--------------------------------------------------------------------------------
/.hound.yml:
--------------------------------------------------------------------------------
1 | rubocop:
2 | config_file: .rubocop.yml
3 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite
4 | VERSION = "0.1.1"
5 | end
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | example/example.pdf
10 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: ruby
3 | rvm:
4 | - 2.7
5 | - 2.6
6 | - 2.5
7 | - jruby
8 | before_install: gem install bundler
9 | install: bundle install --jobs=3 --retry=3
10 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6 |
7 | # Specify your gem's dependencies in breezy_pdf_lite.gemspec
8 | gemspec
9 |
--------------------------------------------------------------------------------
/test/breezy_pdf_lite_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class BreezyPDFLiteTest < Minitest::Test
6 | def test_that_it_has_a_version_number
7 | refute_nil ::BreezyPDFLite::VERSION
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/intercept/base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite::Intercept
4 | # :nodoc
5 | class Base
6 | attr_reader :body
7 |
8 | def initialize(body)
9 | @body = body
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rake/testtask"
5 |
6 | Rake::TestTask.new(:test) do |t|
7 | t.libs << "test"
8 | t.libs << "lib"
9 | t.test_files = FileList["test/**/*_test.rb"]
10 | end
11 |
12 | task default: :test
13 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/intercept.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite
4 | # :nodoc
5 | module Intercept
6 | UnRenderable = Class.new(BreezyPDFLite::Error)
7 | autoload :Base, "breezy_pdf_lite/intercept/base"
8 | autoload :HTML, "breezy_pdf_lite/intercept/html"
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: bundler
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "12:00"
8 | open-pull-requests-limit: 10
9 | ignore:
10 | - dependency-name: rubocop
11 | versions:
12 | - 1.10.0
13 | - 1.11.0
14 | - 1.12.0
15 | - 1.12.1
16 | - 1.9.0
17 | - 1.9.1
18 | - dependency-name: minitest
19 | versions:
20 | - 5.14.3
21 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "breezy_pdf_lite"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require "irb"
15 | IRB.start(__FILE__)
16 |
--------------------------------------------------------------------------------
/example/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4 | require "breezy_pdf_lite"
5 |
6 | html = File.read(File.expand_path("ex.html", __dir__))
7 |
8 | BreezyPDFLite.setup do |config|
9 | config.secret_api_key = ENV["BREEZYPDF_SECRET_API_KEY"]
10 | config.base_url = ENV.fetch("BREEZYPDF_BASE_URL", "http://localhost:5001")
11 | config.middleware_path_matchers = [/as-pdf.pdf/]
12 | end
13 |
14 | use BreezyPDFLite::Middleware
15 | run(proc { |_env| ["200", {"Content-Type" => "text/html"}, [html]] })
16 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | lint:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Set up Ruby
20 | uses: ruby/setup-ruby@v1
21 | with:
22 | ruby-version: 3.1
23 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
24 | - name: Run style check
25 | run: bundle exec standardrb
26 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | test:
14 |
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | ruby-version: ['2.7', '3.0', '3.1']
19 |
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Set up Ruby
23 | uses: ruby/setup-ruby@v1
24 | with:
25 | ruby-version: ${{ matrix.ruby-version }}
26 | bundler-cache: true
27 | - name: Run tests
28 | run: bundle exec rake
29 |
--------------------------------------------------------------------------------
/example/pragmatic.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4 | require "breezy_pdf_lite"
5 | require "fileutils"
6 |
7 | html = File.read(File.expand_path("ex.html", __dir__))
8 |
9 | BreezyPDFLite.setup do |config|
10 | config.secret_api_key = ENV["BREEZYPDF_SECRET_API_KEY"]
11 | config.base_url = ENV.fetch("BREEZYPDF_BASE_URL", "http://localhost:5001")
12 | config.middleware_path_matchers = [/as-pdf.pdf/]
13 | end
14 |
15 | render_request = BreezyPDFLite::RenderRequest.new(html)
16 |
17 | begin
18 | tempfile = render_request.to_file
19 | puts tempfile.path
20 | puts "Enter to remove..."
21 | gets
22 | rescue BreezyPDFLite::BreezyPDFLiteError => e
23 | puts "Unable to render PDF: #{e.message}"
24 | end
25 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite
4 | # HTTP Client for BreezyPDFLite API
5 | class Client
6 | def post(path, body)
7 | uri = URI.parse(BreezyPDFLite.base_url + path)
8 |
9 | http = Net::HTTP.new(uri.host, uri.port)
10 | http.use_ssl = uri.scheme == "https"
11 | http.open_timeout = BreezyPDFLite.open_timeout
12 | http.read_timeout = BreezyPDFLite.read_timeout
13 |
14 | request = Net::HTTP::Post.new(uri.request_uri, headers)
15 |
16 | request.body = body
17 |
18 | http.request(request)
19 | end
20 |
21 | private
22 |
23 | def headers
24 | {
25 | Authorization: "Bearer #{BreezyPDFLite.secret_api_key}"
26 | }
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/middleware.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite
4 | # Rack Middleware for BreezyPDFLite
5 | # Determines if the request should be intercepted or not
6 | class Middleware
7 | def initialize(app, _options = {})
8 | @app = app
9 | end
10 |
11 | def call(env)
12 | if intercept?(env)
13 | Interceptor.new(@app, env).call
14 | else
15 | @app.call(env)
16 | end
17 | end
18 |
19 | private
20 |
21 | # Is this request applicable?
22 | def intercept?(env)
23 | env["REQUEST_METHOD"].match?(/get/i) && matching_uri?(env)
24 | end
25 |
26 | def matching_uri?(env)
27 | BreezyPDFLite.middleware_path_matchers.any? do |regex|
28 | env["REQUEST_URI"].match?(regex)
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/render_request.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite
4 | # Request conversion of a HTML string to PDF
5 | # If the response isn't a 201, raise an error
6 | class RenderRequest
7 | def initialize(body)
8 | @body = body
9 | end
10 |
11 | def response
12 | @response ||= submit.tap do |resp|
13 | raise RenderError, "#{resp.code}: #{resp.body}" if resp.code != "201"
14 | end
15 | end
16 |
17 | def to_file
18 | @to_file ||= Tempfile.new(%w[response .pdf]).tap do |file|
19 | file.binmode
20 | file.write response.body
21 | file.flush
22 | file.rewind
23 | end
24 | end
25 |
26 | private
27 |
28 | def submit
29 | client.post("/render/html", @body)
30 | end
31 |
32 | def client
33 | @client ||= Client.new
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/response.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite
4 | # RenderRequest -> Net::HTTPResponse wrapper that is streaming compatable
5 | class Response
6 | def initialize(response)
7 | @response = response
8 | end
9 |
10 | def status
11 | @status ||= @response.code.to_i
12 | end
13 |
14 | def headers
15 | {
16 | "Content-Type" => "application/pdf",
17 | "Content-Length" => @response.header["Content-Length"],
18 | "Content-Disposition" => @response.header["Content-Disposition"]
19 | }
20 | end
21 |
22 | def each(&blk)
23 | @response.read_body(&blk)
24 | end
25 |
26 | def body
27 | io = StringIO.new
28 | io.binmode
29 |
30 | each do |chunk|
31 | io.write chunk
32 | end
33 |
34 | io.flush
35 | io.rewind
36 |
37 | io
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/middleware_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class BreezyPDFLite::MiddlewareTest < BreezyTest
6 | def test_a_post_request
7 | app = mock
8 | env = {"REQUEST_METHOD" => "post"}
9 |
10 | app.expects(:call).with(env)
11 | tested_class.new(app).call(env)
12 | end
13 |
14 | def test_a_get_request_with_invalid_path
15 | app = mock
16 | env = {"REQUEST_METHOD" => "get", "REQUEST_URI" => "/foo"}
17 |
18 | app.expects(:call).with(env)
19 | tested_class.new(app).call(env)
20 | end
21 |
22 | def test_a_get_request_with_valid_path
23 | app = proc { true }
24 | env = {"REQUEST_METHOD" => "get", "REQUEST_URI" => "/foo.pdf"}
25 |
26 | interceptor_mock = mock
27 | interceptor_mock.expects(:call)
28 |
29 | BreezyPDFLite::Interceptor.expects(:new).with(app, env).returns(interceptor_mock)
30 | tested_class.new(app).call(env)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/client_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class BreezyPDFLite::ClientTest < BreezyTest
6 | def test_post
7 | body = "bob"
8 | path = "/foo"
9 | uri = URI.parse(BreezyPDFLite.base_url + path)
10 | headers = {
11 | Authorization: "Bearer #{BreezyPDFLite.secret_api_key}"
12 | }
13 |
14 | request_mock = mock("Request")
15 | request_mock.expects(:body=).with(body)
16 |
17 | http_mock = mock("HTTP")
18 | http_mock.expects(:use_ssl=).with(true)
19 | http_mock.expects(:open_timeout=).with(BreezyPDFLite.open_timeout)
20 | http_mock.expects(:read_timeout=).with(BreezyPDFLite.read_timeout)
21 | http_mock.expects(:request).with(request_mock)
22 |
23 | Net::HTTP.expects(:new).returns(http_mock)
24 | Net::HTTP::Post.expects(:new).with(uri.request_uri, headers).returns(request_mock)
25 |
26 | tested_class.new.post(path, body)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/intercept/html.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rack/request"
4 | require "rack/file"
5 |
6 | module BreezyPDFLite::Intercept
7 | # Takes the App's response body, and submits it to the breezypdf lite endpoint
8 | # resulting in a file. File is then served with Rack::File
9 | class HTML < Base
10 | def call
11 | request = Rack::Request.new({})
12 | path = render_request_file.path
13 |
14 | Rack::File.new(path, response_headers).serving(request, path)
15 | end
16 |
17 | private
18 |
19 | def render_request
20 | @render_request ||= BreezyPDFLite::RenderRequest.new(body)
21 | end
22 |
23 | def render_request_file
24 | @render_request_file ||= render_request.to_file
25 | end
26 |
27 | def response_headers
28 | @response_headers ||= {
29 | "Content-Type" => "application/pdf",
30 | "Content-Disposition" => render_request.response.header["Content-Disposition"]
31 | }
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/bin/release:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | release_version = case ARGV[0]
5 | when 'major'
6 | 'major'
7 | when 'minor'
8 | 'minor'
9 | when 'pre'
10 | 'pre'
11 | when /\d+\.\d+\.\d+$/
12 | ARGV[0]
13 | else
14 | 'patch'
15 | end
16 |
17 | version = `gem bump --no-commit --version #{release_version} | awk '{ print $4 }' | head -n 1`.strip
18 |
19 | system('bundle')
20 |
21 | system('git add lib/breezy_pdf_lite/version.rb')
22 | system('git add Gemfile.lock')
23 |
24 | system("git commit -m \"Bump breezy_pdf_lite to #{version}\"")
25 |
26 | system("git tag v#{version}")
27 | system('git push')
28 | system('git push --tags')
29 |
30 | system('gem build breezy_pdf_lite')
31 |
32 | puts 'OTP Code:'
33 | code = gets.strip
34 | system("gem push --otp #{code} breezy_pdf_lite-#{version}.gem")
35 |
36 | system("rm breezy_pdf_lite-#{version}.gem")
37 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "uri"
4 | require "net/http"
5 | require "tempfile"
6 |
7 | require "breezy_pdf_lite/version"
8 | require "breezy_pdf_lite/util"
9 |
10 | # :nodoc
11 | module BreezyPDFLite
12 | extend BreezyPDFLite::Util
13 |
14 | autoload :Client, "breezy_pdf_lite/client"
15 | autoload :Intercept, "breezy_pdf_lite/intercept"
16 | autoload :Interceptor, "breezy_pdf_lite/interceptor"
17 | autoload :Middleware, "breezy_pdf_lite/middleware"
18 | autoload :RenderRequest, "breezy_pdf_lite/render_request"
19 |
20 | Error = Class.new(StandardError)
21 | RenderError = Class.new(Error)
22 |
23 | mattr_accessor :secret_api_key
24 | @@secret_api_key = nil
25 |
26 | mattr_accessor :base_url
27 | @@base_url = "https://localhost:5001/"
28 |
29 | mattr_accessor :middleware_path_matchers
30 | @@middleware_path_matchers = [/\.pdf/]
31 |
32 | mattr_accessor :open_timeout
33 | @@open_timeout = 30
34 |
35 | mattr_accessor :read_timeout
36 | @@read_timeout = 30
37 |
38 | def self.setup
39 | yield self
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/breezy_pdf_lite.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path("lib", __dir__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require "breezy_pdf_lite/version"
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = "breezy_pdf_lite"
9 | spec.version = BreezyPDFLite::VERSION
10 | spec.authors = ["Daniel Westendorf"]
11 | spec.email = ["daniel@prowestech.com"]
12 |
13 | spec.summary = "Ruby/rack middleware for BreezyPDF Lite. HTML to PDF."
14 | spec.license = "GPL-3.0"
15 |
16 | spec.files = `git ls-files -z`.split("\x0").reject do |f|
17 | f.match(%r{^(test|spec|features)/})
18 | end
19 | spec.bindir = "exe"
20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21 | spec.require_paths = ["lib"]
22 | spec.required_ruby_version = "> 2"
23 |
24 | spec.add_development_dependency "bundler", "~> 2.0"
25 | spec.add_development_dependency "minitest", "~> 5.0"
26 | spec.add_development_dependency "mocha", "2.0.2"
27 | spec.add_development_dependency "rack", "~> 3.0"
28 | spec.add_development_dependency "rake", "~> 13.0"
29 | spec.add_development_dependency "standardrb"
30 | end
31 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/util.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite
4 | # Utility methods
5 | module Util
6 | def mattr_reader(*syms)
7 | syms.each do |sym|
8 | raise NameError, "invalid attribute name: #{sym}" unless /\A[_A-Za-z]\w*\z/.match?(sym)
9 |
10 | class_eval(<<-EOS, __FILE__, __LINE__ + 1)
11 | @@#{sym} = nil unless defined? @@#{sym}
12 | def self.#{sym}
13 | @@#{sym}
14 | end
15 | EOS
16 |
17 | class_eval(<<-EOS, __FILE__, __LINE__ + 1)
18 | def #{sym}
19 | @@#{sym}
20 | end
21 | EOS
22 | class_variable_set("@@#{sym}", yield) if block_given?
23 | end
24 | end
25 |
26 | def mattr_writer(*syms)
27 | syms.each do |sym|
28 | raise NameError, "invalid attribute name: #{sym}" unless /\A[_A-Za-z]\w*\z/.match?(sym)
29 |
30 | class_eval(<<-EOS, __FILE__, __LINE__ + 1)
31 | @@#{sym} = nil unless defined? @@#{sym}
32 | def self.#{sym}=(obj)
33 | @@#{sym} = obj
34 | end
35 | EOS
36 |
37 | class_eval(<<-EOS, __FILE__, __LINE__ + 1)
38 | def #{sym}=(obj)
39 | @@#{sym} = obj
40 | end
41 | EOS
42 | send("#{sym}=", yield) if block_given?
43 | end
44 | end
45 |
46 | def mattr_accessor(*syms, &blk)
47 | mattr_reader(*syms, &blk)
48 | mattr_writer(*syms)
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/render_request_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class BreezyPDFLite::RenderRequestTest < BreezyTest
6 | def client_mock
7 | @client_mock ||= mock("Client")
8 | end
9 |
10 | def response_mock
11 | @response_mock ||= mock("Client response")
12 | end
13 |
14 | def test_response_successful
15 | body = "foo"
16 |
17 | response_mock.expects(:code).returns("201")
18 | client_mock.expects(:post).with("/render/html", body).returns(response_mock)
19 |
20 | BreezyPDFLite::Client.expects(:new).returns(client_mock)
21 |
22 | tested_class.new(body).response
23 | end
24 |
25 | def test_response_unsuccessful
26 | body = "foo"
27 |
28 | response_mock.expects(:code).returns("500").twice
29 | response_mock.expects(:body).returns("error")
30 | client_mock.expects(:post).with("/render/html", body).returns(response_mock)
31 |
32 | BreezyPDFLite::Client.expects(:new).returns(client_mock)
33 |
34 | assert_raises(BreezyPDFLite::RenderError) do
35 | tested_class.new(body).response
36 | end
37 | end
38 |
39 | def test_to_file
40 | body = "foo"
41 |
42 | response_mock.expects(:code).returns("201")
43 | response_mock.expects(:body).returns("body")
44 | client_mock.expects(:post).with("/render/html", body).returns(response_mock)
45 |
46 | BreezyPDFLite::Client.expects(:new).returns(client_mock)
47 |
48 | assert_kind_of Tempfile, tested_class.new(body).to_file
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/test/intercept/html_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 | require "rack/request"
5 | require "rack/file"
6 |
7 | class BreezyPDFLite::Intercept::HTMLTest < BreezyTest
8 | def request_mock
9 | @request_mock ||= mock("Rack::Request")
10 | end
11 |
12 | def response_mock
13 | @response_mock ||= mock("render request response")
14 | end
15 |
16 | def mock_rack_file
17 | @mock_rack_file ||= mock("Rack::File")
18 | end
19 |
20 | def render_request_instance_mock
21 | @render_request_instance_mock ||= mock("RenderRequest instance")
22 | end
23 |
24 | def test_responds_with_file
25 | body = "Foobar"
26 | file = Tempfile.new
27 | headers = {
28 | "Content-Disposition" => "blah"
29 | }
30 | response_headers = {
31 | "Content-Type" => "application/pdf",
32 | "Content-Disposition" => headers["Content-Disposition"]
33 | }
34 |
35 | response_mock.expects(:header).returns(headers)
36 | mock_rack_file.expects(:serving).with(request_mock, file.path)
37 |
38 | render_request_instance_mock.expects(:response).returns(response_mock)
39 | render_request_instance_mock.expects(:to_file).returns(file)
40 |
41 | BreezyPDFLite::RenderRequest.expects(:new).with(body).returns(render_request_instance_mock)
42 |
43 | Rack::Request.expects(:new).returns(request_mock)
44 | Rack::File.expects(:new).with(file.path, response_headers).returns(mock_rack_file)
45 |
46 | tested_class.new(body).call
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | breezy_pdf_lite (0.1.1)
5 |
6 | GEM
7 | remote: https://rubygems.org/
8 | specs:
9 | ast (2.4.2)
10 | json (2.6.3)
11 | language_server-protocol (3.17.0.3)
12 | minitest (5.17.0)
13 | mocha (2.0.2)
14 | ruby2_keywords (>= 0.0.5)
15 | parallel (1.22.1)
16 | parser (3.2.0.0)
17 | ast (~> 2.4.1)
18 | rack (3.0.4.1)
19 | rainbow (3.1.1)
20 | rake (13.0.1)
21 | regexp_parser (2.6.2)
22 | rexml (3.2.5)
23 | rubocop (1.42.0)
24 | json (~> 2.3)
25 | parallel (~> 1.10)
26 | parser (>= 3.1.2.1)
27 | rainbow (>= 2.2.2, < 4.0)
28 | regexp_parser (>= 1.8, < 3.0)
29 | rexml (>= 3.2.5, < 4.0)
30 | rubocop-ast (>= 1.24.1, < 2.0)
31 | ruby-progressbar (~> 1.7)
32 | unicode-display_width (>= 1.4.0, < 3.0)
33 | rubocop-ast (1.24.1)
34 | parser (>= 3.1.1.0)
35 | rubocop-performance (1.15.2)
36 | rubocop (>= 1.7.0, < 2.0)
37 | rubocop-ast (>= 0.4.0)
38 | ruby-progressbar (1.11.0)
39 | ruby2_keywords (0.0.5)
40 | standard (1.22.1)
41 | language_server-protocol (~> 3.17.0.2)
42 | rubocop (= 1.42.0)
43 | rubocop-performance (= 1.15.2)
44 | standardrb (1.0.1)
45 | standard
46 | unicode-display_width (2.4.2)
47 |
48 | PLATFORMS
49 | ruby
50 |
51 | DEPENDENCIES
52 | breezy_pdf_lite!
53 | bundler (~> 2.0)
54 | minitest (~> 5.0)
55 | mocha (= 2.0.2)
56 | rack (~> 3.0)
57 | rake (~> 13.0)
58 | standardrb
59 |
60 | BUNDLED WITH
61 | 2.3.8
62 |
--------------------------------------------------------------------------------
/lib/breezy_pdf_lite/interceptor.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BreezyPDFLite
4 | # Intercept a Rack request, determining if the app's response should
5 | # be intercepted or simply returned
6 | class Interceptor
7 | attr_reader :app, :env
8 |
9 | def initialize(app, env)
10 | @app = app
11 | @env = env
12 | end
13 |
14 | def call
15 | if (200..299).cover?(app_response_status) # Did the app respond well?
16 | Intercept::HTML.new(app_response_body).call # Try to return a PDF
17 | else
18 | app_response # Bad app response, just send respond with that
19 | end
20 | end
21 |
22 | private
23 |
24 | def app_response
25 | @app_response ||= app.call(doctored_env)
26 | end
27 |
28 | def app_response_status
29 | @app_response_status ||= app_response[0].to_i
30 | end
31 |
32 | def app_response_headers
33 | @app_response_headers ||= app_response[1]
34 | end
35 |
36 | def app_response_body
37 | if app_response[2].respond_to?(:join)
38 | app_response[2].join
39 | elsif app_response[2].respond_to?(:each)
40 | content = []
41 | app_response[2].each { |part| content << part }
42 |
43 | content.join
44 | else
45 | app_response[2]
46 | end
47 | end
48 |
49 | def doctored_env
50 | env.dup.tap do |hash|
51 | hash["HTTP_ACCEPT"] = "text/html"
52 | hash["PATH_INFO"] = path
53 | end
54 | end
55 |
56 | def path
57 | env["PATH_INFO"].gsub(/\.pdf/, "")
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4 |
5 | require "breezy_pdf_lite"
6 |
7 | require "minitest/autorun"
8 | require "mocha/minitest"
9 |
10 | class BreezyTest < Minitest::Test
11 | def tested_class
12 | constantize(self.class.name.gsub(/Test$/, ""))
13 | end
14 |
15 | def fixture(name)
16 | File.open(File.expand_path("fixtures/#{name}", __dir__))
17 | end
18 |
19 | def constantize(camel_cased_word)
20 | names = camel_cased_word.split("::")
21 |
22 | # Trigger a built-in NameError exception including the ill-formed constant in the message.
23 | Object.const_get(camel_cased_word) if names.empty?
24 |
25 | # Remove the first blank element in case of '::ClassName' notation.
26 | names.shift if names.size > 1 && names.first.empty?
27 |
28 | names.inject(Object) do |constant, name|
29 | if constant == Object
30 | constant.const_get(name)
31 | else
32 | candidate = constant.const_get(name)
33 | next candidate if constant.const_defined?(name, false)
34 | next candidate unless Object.const_defined?(name)
35 |
36 | # Go down the ancestors to check if it is owned directly. The check
37 | # stops when we reach Object or the end of ancestors tree.
38 | constant = constant.ancestors.each_with_object(constant) do |ancestor, const|
39 | break const if ancestor == Object
40 | break ancestor if ancestor.const_defined?(name, false)
41 | end
42 |
43 | # owner is in Object, so raise
44 | constant.const_get(name, false)
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/test/interceptor_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class BreezyPDFLite::InterceptorTest < BreezyTest
6 | def app
7 | @app ||= mock("app")
8 | end
9 |
10 | def test_app_receives_doctored_env
11 | env = {"PATH_INFO" => "/foo.pdf"}
12 | doctored_env = {"HTTP_ACCEPT" => "text/html", "PATH_INFO" => "/foo"}
13 | response = ["301", {}, ["body"]]
14 |
15 | app.expects(:call).with(doctored_env).returns(response)
16 | tested_class.new(app, env).call
17 | end
18 |
19 | def test_non_2xx_app_response_returns_app_response
20 | env = {"PATH_INFO" => "/foo.pdf"}
21 | response = ["404", {}, ["body"]]
22 |
23 | app.expects(:call).returns(response)
24 | assert_equal response, tested_class.new(app, env).call
25 | end
26 |
27 | def test_response_body_is_array
28 | env = {"PATH_INFO" => "/foo.pdf"}
29 | response = ["200", {}, %w[body bar]]
30 |
31 | html_intercept_mock = mock("html intercept")
32 | html_intercept_mock.expects(:call).returns(true)
33 |
34 | BreezyPDFLite::Intercept::HTML.expects(:new).with(response[2].join).returns(html_intercept_mock)
35 |
36 | app.expects(:call).returns(response)
37 | assert tested_class.new(app, env).call
38 | end
39 |
40 | def test_response_body_is_eachable
41 | response_body_mock = mock("response body")
42 | response_body_mock.expects(:each).yields("FooBar")
43 |
44 | env = {"PATH_INFO" => "/foo.pdf"}
45 | response = ["200", {}, response_body_mock]
46 |
47 | html_intercept_mock = mock("html intercept")
48 | html_intercept_mock.expects(:call).returns(true)
49 |
50 | BreezyPDFLite::Intercept::HTML.expects(:new).with("FooBar").returns(html_intercept_mock)
51 |
52 | app.expects(:call).returns(response)
53 | assert tested_class.new(app, env).call
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BreezyPDFLite
2 |
3 | [](https://travis-ci.org/danielwestendorf/breezy_pdf_lite-ruby)
4 | [](https://badge.fury.io/rb/breezy_pdf_lite)
5 |
6 | A ruby client for [BreezyPDFLite](https://github.com/danielwestendorf/breezy-pdf-lite), a one-click-to-deploy microservice for converting HTML to PDF with Google Chrome. Send the library a chunk of HTML, get a PDF of it back. Configure how the PDF is rendered via [`meta` tags](https://github.com/danielwestendorf/breezy-pdf-lite#2-configure-with-meta-tags-optional) in the HTML.
7 |
8 | Use pragmatically, or as a Rack Middleware.
9 |
10 | ## Installation
11 |
12 | Add this line to your application's Gemfile:
13 |
14 | ```ruby
15 | gem 'breezy_pdf_lite'
16 | ```
17 |
18 | And then execute:
19 |
20 | $ bundle
21 |
22 | Or install it yourself as:
23 |
24 | $ gem install breezy_pdf_lite
25 |
26 | ## Usage
27 |
28 | BreezyPDFLite requires some configuration before you can use it. Somewhere in your application (perhaps in `config/intializers/breezy_pdf_lite.rb`), configure the `base_url`and the `secret_api_key` of your service. Get these from your Heroku configuration.
29 |
30 | ```ruby
31 | BreezyPDFLite.setup do |config|
32 | config.secret_api_key = ENV["BREEZYPDF_SECRET_API_KEY"]
33 | config.base_url = ENV.fetch("BREEZYPDF_BASE_URL", "http://localhost:5001")
34 | end
35 | ```
36 |
37 | ### Middleware
38 |
39 | Add the Middleware to your stack.
40 |
41 | _Ruby on Rails_
42 | ```ruby
43 | # config/application.rb
44 | config.middleware.use BreezyPDFLite::Middleware
45 | ```
46 |
47 | Any URL ending in `.pdf` will be intercepted, rendered as HTML, rendered to a PDF, and then returned to the client.
48 |
49 | _Rack/Sinatra/etc_
50 |
51 | See `example/config.ru`
52 |
53 | ### Pragmatic
54 |
55 | See `example/pragmatic.rb`
56 |
57 |
58 | ## Examples
59 | Examples depend on the [BreezyPDFLite](https://github.com/danielwestendorf/breezy-pdf-lite) microservice being avialable.
60 |
61 | _Middleware_
62 |
63 | `BREEZYPDF_SECRET_API_KEY=YOURSECRETKEY BREEZYPDF_BASE_URL=https://YOURHEROKUAPPORWHATEVER.herokuapp.com/ rackup example/config.ru`
64 |
65 | Visit `https://localhost:9292` and click the link to download the PDF.
66 |
67 | _Pragmatic_
68 |
69 | `BREEZYPDF_SECRET_API_KEY=YOURSECRETKEY BREEZYPDF_BASE_URL=https://YOURHEROKUAPPORWHATEVER.herokuapp.com/ ruby example/pragmatic.rb`
70 |
71 | The PDF will be downloaded to `example/example.pdf`
72 |
73 | ## License
74 |
75 | See `LICENSE.txt`.
76 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.