From 7ea4e06caa3047b1f4e3e7ecbe2793ef564d453b Mon Sep 17 00:00:00 2001 From: Brooke Kuhlmann Date: Tue, 13 Jan 2026 10:07:44 -0700 Subject: [PATCH] Added recipe endpoint Necessary to perform API requests and acquire models for further processing. This also wires up the client and exposes the endpoint for immediate use. Milestone: minor --- lib/trmnl/api/client.rb | 4 + lib/trmnl/api/endpoints/container.rb | 1 + lib/trmnl/api/endpoints/recipe.rb | 31 ++++++ spec/lib/trmnl/api/client_spec.rb | 10 ++ spec/lib/trmnl/api/endpoints/recipe_spec.rb | 114 ++++++++++++++++++++ 5 files changed, 160 insertions(+) create mode 100644 lib/trmnl/api/endpoints/recipe.rb create mode 100644 spec/lib/trmnl/api/endpoints/recipe_spec.rb diff --git a/lib/trmnl/api/client.rb b/lib/trmnl/api/client.rb index f4875c1..772e444 100644 --- a/lib/trmnl/api/client.rb +++ b/lib/trmnl/api/client.rb @@ -17,6 +17,7 @@ module TRMNL endpoint_log: :log, endpoint_models: :models, endpoint_palettes: :palettes, + endpoint_recipes: :recipes, endpoint_setup: :setup ] @@ -29,6 +30,7 @@ module TRMNL endpoint_log: :type, endpoint_models: :type, endpoint_palettes: :type, + endpoint_recipes: :type, endpoint_setup: :type ] @@ -53,6 +55,8 @@ module TRMNL def palettes = endpoint_palettes.call + def recipes(**) = endpoint_recipes.call(**) + def setup(**) = endpoint_setup.call(**) end end diff --git a/lib/trmnl/api/endpoints/container.rb b/lib/trmnl/api/endpoints/container.rb index cb97788..7e27b8e 100644 --- a/lib/trmnl/api/endpoints/container.rb +++ b/lib/trmnl/api/endpoints/container.rb @@ -17,6 +17,7 @@ module TRMNL register(:log) { Log.new } register(:models) { Model.new } register(:palettes) { Palette.new } + register(:recipes) { Recipe.new } register(:setup) { Setup.new } end end diff --git a/lib/trmnl/api/endpoints/recipe.rb b/lib/trmnl/api/endpoints/recipe.rb new file mode 100644 index 0000000..de42a91 --- /dev/null +++ b/lib/trmnl/api/endpoints/recipe.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "inspectable" +require "pipeable" + +module TRMNL + module API + module Endpoints + # Handles API request/response. + class Recipe + include TRMNL::API::Dependencies[ + :requester, + contract: "contracts.recipe", + model: "models.recipe" + ] + + include Inspectable[contract: :type] + include Pipeable + + def call(**) + pipe( + requester.get("recipes.json", **), + try(:parse, catch: JSON::ParserError), + validate(contract, as: :to_h), + to(model, :for) + ) + end + end + end + end +end diff --git a/spec/lib/trmnl/api/client_spec.rb b/spec/lib/trmnl/api/client_spec.rb index 10adc4e..e238fb7 100644 --- a/spec/lib/trmnl/api/client_spec.rb +++ b/spec/lib/trmnl/api/client_spec.rb @@ -103,6 +103,15 @@ RSpec.describe TRMNL::API::Client do end end + describe "#recipe" do + let(:endpoint) { instance_spy TRMNL::API::Endpoints::Recipe } + + it "messages endpoint" do + client = described_class.new endpoint_recipes: endpoint + expect(client.recipes).to have_received(:call).with(no_args) + end + end + describe "#setup" do let(:endpoint) { instance_spy TRMNL::API::Endpoints::Setup } @@ -122,6 +131,7 @@ RSpec.describe TRMNL::API::Client do endpoint_log: "TRMNL::API::Endpoints::Log", endpoint_models: "TRMNL::API::Endpoints::Model", endpoint_palettes: "TRMNL::API::Endpoints::Palette", + endpoint_recipes: "TRMNL::API::Endpoints::Recipe", endpoint_setup: "TRMNL::API::Endpoints::Setup" ) end diff --git a/spec/lib/trmnl/api/endpoints/recipe_spec.rb b/spec/lib/trmnl/api/endpoints/recipe_spec.rb new file mode 100644 index 0000000..9aefc40 --- /dev/null +++ b/spec/lib/trmnl/api/endpoints/recipe_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe TRMNL::API::Endpoints::Recipe do + subject(:endpoint) { described_class.new requester: } + + include_context "with application dependencies" + + let :requester do + TRMNL::API::Requester.new http: do |settings| + settings.uri = "https://usetrmnl.com" + end + end + + describe "#call" do + context "with success" do + let :http do + HTTP::Fake::Client.new do + get "/recipes.json" do + headers["Content-Type"] = "application/json" + status 200 + + <<~JSON + { + "data": [ + { + "id": 1, + "name": "test", + "screenshot_url": "https://screens.trmnl.io/test.png", + "published_at": "2026-01-02T03:04:05.000Z", + "custom_fields": [], + "author_bio": { + "keyname": "test", + "name": "Test", + "field_type": "author_bio" + }, + "stats": { + "installs": 1, + "forks": 2 + } + } + ], + "total": 2, + "from": 1, + "to": 2, + "per_page": 25, + "current_page": 1, + "prev_page_url": null, + "next_page_url": "/recipes.json?page=2" + } + JSON + end + end + end + + it "answers record" do + expect(endpoint.call.success).to match( + TRMNL::API::Models::Recipe[ + data: [ + TRMNL::API::Models::Recipes::Entry[ + id: 1, + name: "test", + published_at: Time.parse("2026-01-02T03:04:05.000Z"), + screenshot_url: "https://screens.trmnl.io/test.png", + author: TRMNL::API::Models::Recipes::Author[ + keyname: "test", + name: "Test", + field_type: "author_bio", + description_locales: {} + ], + custom_fields: [], + statistics: TRMNL::API::Models::Recipes::Statistics[installs: 1, forks: 2] + ] + ], + total: 2, + from: 1, + to: 2, + per_page: 25, + current_page: 1, + prev_page_url: nil, + next_page_url: "/recipes.json?page=2" + ] + ) + end + end + + context "with failure" do + let :http do + HTTP::Fake::Client.new do + get "/recipes.json" do + headers["Content-Type"] = "application/json" + status 404 + + <<~JSON + {"error": "Danger!"} + JSON + end + end + end + + it "answers failure" do + result = described_class.new(requester:).call + expect(result).to match(Failure(be_a(HTTP::Response))) + end + end + end + + describe "#inspect" do + it "has inspected attributes" do + expect(described_class.new.inspect).to match_inspection(contract: "Dry::Schema::JSON") + end + end +end