From cfd9f6d1f6559d3e36dfcd4ac481576cc88b85c6 Mon Sep 17 00:00:00 2001 From: Brooke Kuhlmann Date: Mon, 21 Apr 2025 19:57:51 -0600 Subject: [PATCH] Added client Necessary to provide the primary client for making low-level API requests. RuboCop and Reek issues have been marked for future consideration and improvement because these issues stem from Core's API (and subsequent Firmware design) and should be fixed in the Core/Firmware implementation first. --- lib/trmnl/api/client.rb | 39 +++++++++++ spec/lib/trmnl/api/client_spec.rb | 108 ++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 lib/trmnl/api/client.rb create mode 100644 spec/lib/trmnl/api/client_spec.rb diff --git a/lib/trmnl/api/client.rb b/lib/trmnl/api/client.rb new file mode 100644 index 0000000..e56c460 --- /dev/null +++ b/lib/trmnl/api/client.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "dry/monads" +require "http" + +module TRMNL + module API + # Provides a low level configurable and monadic API client. + # :reek:DataClump + class Client + include Dry::Monads[:result] + + HEADERS = {}.freeze + + def initialize settings: TRMNL::API::Configuration::Loader.new.call, http: HTTP + @settings = settings + @http = http + + yield settings if block_given? + end + + def get(path, headers: HEADERS, **params) = call(__method__, path, headers, params:) + + def post(path, headers: HEADERS, **json) = call(__method__, path, headers, json:) + + private + + attr_reader :settings, :http + + # rubocop:todo Metrics/ParameterLists + def call method, path, headers, **options + http.headers(settings.headers.merge(headers)) + .public_send(method, "#{settings.uri}/#{path}", options) + .then { |response| response.status.success? ? Success(response) : Failure(response) } + end + # rubocop:enable Metrics/ParameterLists + end + end +end diff --git a/spec/lib/trmnl/api/client_spec.rb b/spec/lib/trmnl/api/client_spec.rb new file mode 100644 index 0000000..70535fa --- /dev/null +++ b/spec/lib/trmnl/api/client_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe TRMNL::API::Client do + using Refinements::Hash + + subject(:client) { described_class.new http: } + + describe "#initialize" do + it "initializes with block" do + client = described_class.new { |settings| settings.content_type = "application/xml" } + body = client.get("bogus").failure.to_s + + expect(body).to include("Not Found") + end + end + + describe "#get" do + context "with success" do + let :http do + HTTP::Fake::Client.new do + get "/api/current_screen" do + headers["Content-Type"] = "application/json" + status 200 + + <<~JSON + { + "status": 200, + "refresh_rate": 3200, + "image_url": "https://test.io/images/test.bmp", + "filename": "test.bmp", + "rendered_at": null + } + JSON + end + end + end + + it "answers response" do + response = client.get "current_screen" + payload = response.fmap(&:parse).bind(&:symbolize_keys!) + + expect(payload).to eq( + status: 200, + refresh_rate: 3200, + image_url: "https://test.io/images/test.bmp", + filename: "test.bmp", + rendered_at: nil + ) + end + end + + context "with custom header" do + let(:http) { class_spy HTTP, headers: HTTP } + let(:response) { instance_spy HTTP::Response } + + it "includes custom header" do + client.get "current_screen", headers: {"ID" => "123"} + + expect(http).to have_received(:headers).with( + "Content-Type" => "application/json", + "ID" => "123" + ) + end + end + + context "with failure" do + let :http do + HTTP::Fake::Client.new do + get "/api/current_screen" do + headers["Content-Type"] = "application/json" + status 404 + + <<~JSON + { + "message": "Danger!" + } + JSON + end + end + end + + it "answers failure response" do + response = client.get "current_screen" + payload = response.alt_map { |result| result.parse.symbolize_keys! } + + expect(payload).to be_failure(message: "Danger!") + end + end + end + + describe "#post" do + let :http do + HTTP::Fake::Client.new do + post "/api/log" do + headers["Content-Type"] = "application/json" + status 204 + end + end + end + + it "answers response" do + result = client.post "log", log: {} + expect(result.success.status).to eq(204) + end + end +end