stephancom/qrinator

View on GitHub
qrinator.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# typed: strict
require 'net/http'

#   ____  _____  _             _
#  / __ \|  __ \(_)        by | | stephan.com
# | |  | | |__) |_ _ __   __ _| |_ ___  _ __
# | |  | |  _  /| | '_ \ / _` | __/ _ \| '__|
# | |__| | | \ \| | | | | (_| | || (_) | |
#  \___\_\_|  \_\_|_| |_|\__,_|\__\___/|_|
class Qrinator
  extend T::Sig

  sig {returns(Integer)}
  attr_reader :size

  sig {params(base_url: String, logo_url: String, size: Integer).void}
  def initialize(base_url, logo_url, size)
    @base_url = T.let(base_url, String)
    @logo_url = T.let(logo_url, String)
    @size = T.let(size.to_i, Integer)
    @offset = T.let(@size / 3, Integer)
    @inset = T.let(@size / 3, Integer)
  end

  sig {returns(T::Hash[Symbol, String])}
  def redis_params
    return {} if ENV['REDISCLOUD_URL'].nil?

    uri = URI.parse(T.must(ENV['REDISCLOUD_URL']))
    { host: uri.host, port: uri.port, password: uri.password }
  end

  sig {returns(T.nilable(Redis))}
  def redis
    @redis ||= T.let(Redis.new(redis_params), T.nilable(Redis))
  end

  sig {params(key: String, blk: T.proc.params(k: String).returns(String)).returns(String)}
  def redis_set_unless_exists(key, &blk)
    if T.must(redis).exists(key)
      T.must(redis).get(key)
    else
      result = yield(key)
      T.must(redis).set(key, result)
      result
    end
  rescue Redis::CannotConnectError
    yield(key)
  end

  sig {returns(String)}
  def raw_logo_data
    redis_set_unless_exists(@logo_url) do |lurl|
      Net::HTTP.get(URI(lurl))
    end
  end

  sig {returns(T::Hash[String, String])}
  def download_headers
    {
      'Pragma' => 'public',
      'Expires' => '0',
      'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
      'Content-Type' => 'application/octet-stream',
      'Content-Disposition' => 'attachment;filename=qrcode.png',
      'Content-Transfer-Encoding' => 'binary'
    }
  end

  sig {returns(T::Hash[String, String])}
  def headers
    { 'Content-Type' => 'image/png' }
  end

  sig {returns(Integer)}
  def offset
    @size / 3
  end

  sig {returns(Integer)}
  def inset
    @size / 3
  end

  sig {returns(T.nilable(ChunkyPNG::Image))}
  def logo
    @logo ||= T.let(ChunkyPNG::Image.from_blob(raw_logo_data).resize(inset, inset), T.nilable(ChunkyPNG::Image))
  end

  sig {params(payload: String).returns(String)}
  def url(payload)
    URI.join(@base_url, CGI.unescape(payload)).to_s
  end

  sig {params(payload: String).returns(String)}
  def qr(payload)
    redis_set_unless_exists(payload) do
      RQRCode::QRCode.new(url(payload), level: :h).to_img
                     .resize(size, size)
                     .compose(logo, offset, offset)
                     .to_blob
    end
  end

  sig {params(env: T::Hash[String, String]).returns([Integer, T::Hash[String, String], T.any(StringIO, T::Array[String])])}
  def call(env)
    if env['REQUEST_METHOD'] == 'DELETE'
      begin
        T.must(redis).flushall
        [202, {}, []]
      rescue Redis::CannotConnectError
        [405, {}, ['no redis']]
      end
    else
      [200, headers, StringIO.new(qr(T.must(env['PATH_INFO'])))]
    end
  end
end