# frozen_string_literal: true require 'json' require 'typhoeus' Typhoeus::Config.memoize = true class ZoomingProxy class << self attr_accessor :default_scheme attr_accessor :default_host end # Middleware # class Middleware attr_reader :host, :scheme, :forwarded MAX_DEPTH = 10 def initialize(app) @app = app end def call(env) @host = env['SERVER_NAME'] @scheme = env['rack.url_scheme'] @forwarded = env['REMOTE_ADDR'] @encoding = env['HTTP_ACCEPT_ENCODING'] handle_response(*@app.call(env)) end private def handle_response(status, headers, response) proxy_headers = headers.dup proxy_headers['X-Forwarded-For'] = forwarded proxy_headers['X-HAL-Zooming'] = '0' proxy_headers.delete('Accept-Encoding') compress embed(status, proxy_headers, response) end def compress(response) return response unless @encoding.to_s.include?('gzip') response[2][0] = string_to_gzip(response[2][0]) response[1]['Content-Encoding'] = 'gzip' response[1]['Content-Length'] = response[2][0].bytesize response end def string_to_gzip(source) output = StringIO.new gz = Zlib::GzipWriter.new(output) gz.write(source) gz.close output.string end def embed(status, headers, response, depth: 1) body = response_to_hash(response) return zoomed_response(status, headers, body, depth) unless zoomable?(headers, body, depth) body['_embedded'] ||= {} headers['X-HAL-Zooming'] = depth body = parallel_zooms(body, headers, depth) zoomed_response(status, headers, body, depth) end def parallel_zooms(body, headers, depth) hydra = Typhoeus::Hydra.new requested_links(body, headers).map do |url, attributes| request = request_rel(url, headers) request.on_complete do |res| body = zoom_rel(attributes[:relation], res, body, attributes[:array], depth) end hydra.queue(request) end hydra.run body end def zoom_rel(relation, response, body, array, depth) embedded_body = handle_nested_embeds(response, depth) if array body['_embedded'][relation] ||= [] body['_embedded'][relation] << embedded_body else body['_embedded'][relation] = embedded_body end body end def request_rel(href, headers) uri = URI.parse(href) uri.scheme = ZoomingProxy.default_scheme || scheme uri.host = ZoomingProxy.default_host || host Typhoeus::Request.new(uri, headers: headers) end def response_to_hash(response) response = response.to_json if response.respond_to?(:each_key) JSON.parse(body_from_rack_response(response)) rescue JSON::ParserError nil end def handle_nested_embeds(res, depth) _, _, response = embed(res.code, res.headers, res.body, depth: depth + 1) response_to_hash(response) end def body_from_rack_response(response) [*response].join end def zoomed_response(status, headers, body, depth) json = body.to_json headers['Content-Length'] = json.bytesize headers['X-HAL-Zoomed'] = depth.to_s [status, headers, [json]] end def zoomable?(headers, body, depth) zoomable = depth <= MAX_DEPTH zoomable &= headers.include?('x-hal-zoom') zoomable &= headers['content-type'].to_s.include?('json') zoomable && !body.nil? end def requested_links(body, headers) body['_links'] .to_h .select { |k, _| headers['x-hal-zoom'].to_s.split(/\s+/).include?(k) } .each_with_object({}) do |(k, v), urls| array = v.respond_to?(:to_ary) [v].flatten.each { |url| urls[url['href']] = { relation: k, array: array } } end end end end