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.
This commit is contained in:
Brooke Kuhlmann
2025-04-22 14:24:32 -06:00
parent 70e1d24796
commit cfd9f6d1f6
2 changed files with 147 additions and 0 deletions
+39
View File
@@ -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
+108
View File
@@ -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