@rivi-software/radiotools-sdk-generator 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ import { listenerSdkManifest } from "@rivi-software/radiotools-contract";
2
+ import { snakeCase } from "../shared/naming";
3
+ function renderPythonClient() {
4
+ const manifest = listenerSdkManifest;
5
+ const readMethods = manifest.clientMethods.filter((m) => m.scope === "listener:read" && m.rest?.method === "GET" && m.name.startsWith("get"));
6
+ const methodBlocks = readMethods
7
+ .map((method) => {
8
+ const pyName = snakeCase(method.name);
9
+ if (method.name === "getStation") {
10
+ return ` def ${pyName}(self) -> dict[str, Any]:
11
+ """${method.description}"""
12
+ return self._get_json(f"/api/v1/stations/{quote(self.station_slug, safe='')}")`;
13
+ }
14
+ if (method.name === "getWeather" || method.name === "getTraffic") {
15
+ return ` def ${pyName}(self) -> dict[str, Any] | None:
16
+ """${method.description}"""
17
+ station = self.get_station()
18
+ href = station.get("resources", {}).get("${method.name === "getWeather" ? "weather" : "traffic"}")
19
+ if not href:
20
+ return None
21
+ return self._get_json(href)`;
22
+ }
23
+ return ` def ${pyName}(self, **params: Any) -> dict[str, Any]:
24
+ """${method.description}"""
25
+ raise NotImplementedError("Use the TypeScript SDK or extend this stub with REST href resolution.")`;
26
+ })
27
+ .join("\n\n");
28
+ return `"""Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand."""
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+ from urllib.parse import quote
33
+
34
+ import httpx
35
+
36
+
37
+ class RadioToolsStationClient:
38
+ """Minimal REST client for RadioTools listener contract v${manifest.contractVersion}."""
39
+
40
+ def __init__(
41
+ self,
42
+ base_url: str,
43
+ station_slug: str,
44
+ *,
45
+ api_token: str | None = None,
46
+ ingest_token: str | None = None,
47
+ client: httpx.Client | None = None,
48
+ ) -> None:
49
+ if not api_token and not ingest_token:
50
+ raise ValueError("api_token or ingest_token is required")
51
+ self.base_url = base_url.rstrip("/")
52
+ self.station_slug = station_slug
53
+ self.api_token = api_token
54
+ self.ingest_token = ingest_token
55
+ self._client = client or httpx.Client(timeout=30.0)
56
+
57
+ def _headers(self, *, ingest: bool = False) -> dict[str, str]:
58
+ token = self.ingest_token if ingest else self.api_token
59
+ if not token:
60
+ scope = "ingest:write" if ingest else "listener:read"
61
+ raise ValueError(f"Missing token for {scope}")
62
+ return {"Authorization": f"Bearer {token}"}
63
+
64
+ def _get_json(self, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
65
+ url = path if path.startswith("http") else f"{self.base_url}{path}"
66
+ response = self._client.get(
67
+ url,
68
+ params=params,
69
+ headers=self._headers(),
70
+ )
71
+ response.raise_for_status()
72
+ return response.json()
73
+
74
+ def _post_json(self, path: str, body: dict[str, Any], *, ingest: bool = True) -> dict[str, Any]:
75
+ url = f"{self.base_url}{path}"
76
+ response = self._client.post(url, json=body, headers=self._headers(ingest=ingest))
77
+ response.raise_for_status()
78
+ return response.json()
79
+
80
+ ${methodBlocks}
81
+
82
+ def report_playback(self, observation: dict[str, Any], *, station_id: str | None = None) -> dict[str, Any]:
83
+ """Send one encoder playback observation."""
84
+ params: dict[str, str] = {}
85
+ if station_id:
86
+ params["stationId"] = station_id
87
+ else:
88
+ params["stationSlug"] = self.station_slug
89
+ path = "${manifest.integrations.playback.path}"
90
+ query = "&".join(f"{k}={quote(v, safe='')}" for k, v in params.items())
91
+ return self._post_json(f"{path}?{query}", observation)
92
+
93
+ @classmethod
94
+ def from_env(cls) -> RadioToolsStationClient:
95
+ import os
96
+
97
+ base_url = os.environ.get("RADIOTOOLS_BASE_URL", "").strip()
98
+ station_slug = os.environ.get("RADIOTOOLS_STATION_SLUG", "").strip()
99
+ api_token = os.environ.get("RADIOTOOLS_API_TOKEN", "").strip() or None
100
+ ingest_token = os.environ.get("RADIOTOOLS_INGEST_TOKEN", "").strip() or None
101
+ if not base_url or not station_slug:
102
+ raise ValueError("Set RADIOTOOLS_BASE_URL and RADIOTOOLS_STATION_SLUG")
103
+ return cls(base_url, station_slug, api_token=api_token, ingest_token=ingest_token)
104
+ `;
105
+ }
106
+ export const pythonTarget = {
107
+ id: "python",
108
+ description: "Python httpx client stubs (packages/sdk-python)",
109
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-python/src/radiotools_sdk`,
110
+ generate: () => [
111
+ {
112
+ path: "client.py",
113
+ contents: renderPythonClient(),
114
+ },
115
+ {
116
+ path: "__init__.py",
117
+ contents: `"""${listenerSdkManifest.packageNames.python} — generated listener SDK."""
118
+ from .client import RadioToolsStationClient
119
+
120
+ __all__ = ["RadioToolsStationClient"]
121
+ `,
122
+ },
123
+ ],
124
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const reactTarget: SdkTarget;
@@ -0,0 +1,82 @@
1
+ import { hookNameFromMethod, ZERO_ARG_READ_METHODS } from "../shared/reads";
2
+ function renderReactHooks() {
3
+ const hooks = ZERO_ARG_READ_METHODS.map((method) => {
4
+ const hookName = hookNameFromMethod(method);
5
+ return `export function ${hookName}(client: StationClient) {
6
+ return useStationQuery(client, "${method}", () => client.${method}());
7
+ }`;
8
+ }).join("\n\n");
9
+ return `/** Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand. */
10
+ import { useCallback, useEffect, useRef, useState } from "react";
11
+ import type { StationClient } from "@rivi-software/radiotools-sdk";
12
+
13
+ type QueryState<T> = {
14
+ data: T | undefined;
15
+ error: Error | undefined;
16
+ loading: boolean;
17
+ refresh: () => void;
18
+ };
19
+
20
+ function useStationQuery<T>(
21
+ client: StationClient,
22
+ _method: string,
23
+ read: () => Promise<T>,
24
+ ): QueryState<T> {
25
+ const [data, setData] = useState<T | undefined>(undefined);
26
+ const [error, setError] = useState<Error | undefined>(undefined);
27
+ const [loading, setLoading] = useState(true);
28
+ const mounted = useRef(true);
29
+
30
+ const refresh = useCallback(() => {
31
+ setLoading(true);
32
+ void read()
33
+ .then((value) => {
34
+ if (!mounted.current) return;
35
+ setData(value);
36
+ setError(undefined);
37
+ })
38
+ .catch((cause: unknown) => {
39
+ if (!mounted.current) return;
40
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
41
+ })
42
+ .finally(() => {
43
+ if (mounted.current) setLoading(false);
44
+ });
45
+ }, [read]);
46
+
47
+ useEffect(() => {
48
+ mounted.current = true;
49
+ refresh();
50
+ return () => {
51
+ mounted.current = false;
52
+ };
53
+ }, [refresh]);
54
+
55
+ return { data, error, loading, refresh };
56
+ }
57
+
58
+ export function useRecommendedPollInterval(client: StationClient): QueryState<number> {
59
+ return useStationQuery(client, "recommendedPollIntervalMs", () =>
60
+ client.recommendedPollIntervalMs(),
61
+ );
62
+ }
63
+
64
+ ${hooks}
65
+ `;
66
+ }
67
+ export const reactTarget = {
68
+ id: "react",
69
+ description: "React hooks wrapping @rivi-software/radiotools-sdk (packages/sdk-react)",
70
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-react/src`,
71
+ generate: () => [
72
+ {
73
+ path: "hooks.generated.ts",
74
+ contents: renderReactHooks(),
75
+ },
76
+ {
77
+ path: "index.ts",
78
+ contents: `export * from "./hooks.generated";
79
+ `,
80
+ },
81
+ ],
82
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const rubyTarget: SdkTarget;
@@ -0,0 +1,122 @@
1
+ import { listenerSdkManifest } from "@rivi-software/radiotools-contract";
2
+ function renderRubyClient() {
3
+ const playbackPath = listenerSdkManifest.integrations.playback.path;
4
+ return `# Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand.
5
+ # frozen_string_literal: true
6
+
7
+ require "json"
8
+ require "net/http"
9
+ require "uri"
10
+
11
+ module RadioTools
12
+ class StationClient
13
+ def initialize(base_url:, station_slug:, api_token: nil, ingest_token: nil)
14
+ raise ArgumentError, "api_token or ingest_token is required" if api_token.to_s.empty? && ingest_token.to_s.empty?
15
+
16
+ @base_url = base_url.to_s.chomp("/")
17
+ @station_slug = station_slug
18
+ @api_token = api_token
19
+ @ingest_token = ingest_token
20
+ end
21
+
22
+ def self.from_env
23
+ new(
24
+ base_url: ENV.fetch("RADIOTOOLS_BASE_URL", ""),
25
+ station_slug: ENV.fetch("RADIOTOOLS_STATION_SLUG", ""),
26
+ api_token: ENV["RADIOTOOLS_API_TOKEN"],
27
+ ingest_token: ENV["RADIOTOOLS_INGEST_TOKEN"],
28
+ )
29
+ end
30
+
31
+ def get_station
32
+ get_json("/api/v1/stations/#{URI.encode_uri_component(@station_slug)}", ingest: false)
33
+ end
34
+
35
+ def get_weather
36
+ href = station_resource("weather")
37
+ return nil if href.nil? || href.empty?
38
+
39
+ get_json(href, ingest: false)
40
+ end
41
+
42
+ def get_traffic
43
+ href = station_resource("traffic")
44
+ return nil if href.nil? || href.empty?
45
+
46
+ get_json(href, ingest: false)
47
+ end
48
+
49
+ def report_playback(observation, station_id: nil)
50
+ params = station_id ? { stationId: station_id } : { stationSlug: @station_slug }
51
+ query = URI.encode_www_form(params)
52
+ post_json("${playbackPath}?#{query}", observation, ingest: true)
53
+ end
54
+
55
+ private
56
+
57
+ def station_resource(key)
58
+ station = get_station
59
+ station.dig("resources", key)
60
+ end
61
+
62
+ def get_json(path, ingest:)
63
+ uri = path.start_with?("http") ? URI(path) : URI("#{@base_url}#{path}")
64
+ request = Net::HTTP::Get.new(uri)
65
+ apply_auth!(request, ingest)
66
+ execute(request, uri)
67
+ end
68
+
69
+ def post_json(path, body, ingest:)
70
+ uri = URI("#{@base_url}#{path}")
71
+ request = Net::HTTP::Post.new(uri)
72
+ request["Content-Type"] = "application/json"
73
+ request.body = JSON.generate(body)
74
+ apply_auth!(request, ingest)
75
+ execute(request, uri)
76
+ end
77
+
78
+ def apply_auth!(request, ingest)
79
+ token = ingest ? @ingest_token : @api_token
80
+ raise ArgumentError, "missing bearer token" if token.to_s.empty?
81
+
82
+ request["Authorization"] = "Bearer #{token}"
83
+ end
84
+
85
+ def execute(request, uri)
86
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
87
+ http.request(request)
88
+ end
89
+ raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
90
+
91
+ JSON.parse(response.body)
92
+ end
93
+ end
94
+ end
95
+ `;
96
+ }
97
+ export const rubyTarget = {
98
+ id: "ruby",
99
+ description: "Ruby Net::HTTP client stubs (packages/sdk-ruby)",
100
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-ruby/lib/radiotools_sdk`,
101
+ generate: () => [
102
+ { path: "client.rb", contents: renderRubyClient() },
103
+ {
104
+ path: "../radiotools_sdk.rb",
105
+ contents: `# frozen_string_literal: true
106
+
107
+ require_relative "radiotools_sdk/client"
108
+
109
+ module RadioToolsSdk
110
+ Client = RadioTools::StationClient
111
+ end
112
+ `,
113
+ },
114
+ {
115
+ path: "../../README.md",
116
+ contents: `# radiotools_sdk (Ruby)
117
+
118
+ Generated REST stubs. Regenerate: \`bun run --filter @rivi-software/radiotools-sdk-generator generate --target ruby\`.
119
+ `,
120
+ },
121
+ ],
122
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const svelteTarget: SdkTarget;
@@ -0,0 +1,76 @@
1
+ import { pascalCase } from "../shared/naming";
2
+ import { ZERO_ARG_READ_METHODS } from "../shared/reads";
3
+ function renderSvelteStores() {
4
+ const stores = ZERO_ARG_READ_METHODS.map((method) => {
5
+ const storeFn = `create${pascalCase(method)}Store`;
6
+ return `export function ${storeFn}(client: StationClient) {
7
+ return createStationStore(() => client.${method}());
8
+ }`;
9
+ }).join("\n\n");
10
+ return `/** Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand. */
11
+ import { derived, writable, type Readable } from "svelte/store";
12
+ import type { StationClient } from "@rivi-software/radiotools-sdk";
13
+
14
+ export type StationStoreState<T> = {
15
+ data: T | undefined;
16
+ error: Error | undefined;
17
+ loading: boolean;
18
+ };
19
+
20
+ export type StationStore<T> = Readable<StationStoreState<T>> & {
21
+ refresh: () => Promise<void>;
22
+ };
23
+
24
+ function createStationStore<T>(read: () => Promise<T>): StationStore<T> {
25
+ const inner = writable<StationStoreState<T>>({
26
+ data: undefined,
27
+ error: undefined,
28
+ loading: true,
29
+ });
30
+
31
+ const refresh = async () => {
32
+ inner.update((state) => ({ ...state, loading: true }));
33
+ try {
34
+ const data = await read();
35
+ inner.set({ data, error: undefined, loading: false });
36
+ } catch (cause: unknown) {
37
+ inner.set({
38
+ data: undefined,
39
+ error: cause instanceof Error ? cause : new Error(String(cause)),
40
+ loading: false,
41
+ });
42
+ }
43
+ };
44
+
45
+ void refresh();
46
+
47
+ return {
48
+ subscribe: inner.subscribe,
49
+ refresh,
50
+ };
51
+ }
52
+
53
+ export function createRecommendedPollIntervalStore(client: StationClient) {
54
+ return createStationStore(() => client.recommendedPollIntervalMs());
55
+ }
56
+
57
+ ${stores}
58
+
59
+ /** Convenience derived store: true when any nested field is loading. */
60
+ export function stationStoreLoading<T>(store: StationStore<T>): Readable<boolean> {
61
+ return derived(store, ($state) => $state.loading);
62
+ }
63
+ `;
64
+ }
65
+ export const svelteTarget = {
66
+ id: "svelte",
67
+ description: "Svelte stores wrapping @rivi-software/radiotools-sdk (packages/sdk-svelte)",
68
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-svelte/src`,
69
+ generate: () => [
70
+ { path: "stores.generated.ts", contents: renderSvelteStores() },
71
+ {
72
+ path: "index.ts",
73
+ contents: `export * from "./stores.generated";\n`,
74
+ },
75
+ ],
76
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const swiftTarget: SdkTarget;
@@ -0,0 +1,179 @@
1
+ import { listenerSdkManifest } from "@rivi-software/radiotools-contract";
2
+ function renderSwiftClient() {
3
+ const playbackPath = listenerSdkManifest.integrations.playback.path.replace(/^\//, "");
4
+ return `// Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand.
5
+ import Foundation
6
+
7
+ public enum RadioToolsSDKError: Error, LocalizedError {
8
+ case missingToken
9
+ case invalidURL
10
+ case httpStatus(Int, String)
11
+
12
+ public var errorDescription: String? {
13
+ switch self {
14
+ case .missingToken: return "Missing bearer token"
15
+ case .invalidURL: return "Invalid URL"
16
+ case let .httpStatus(code, body): return "HTTP \\(code): \\(body)"
17
+ }
18
+ }
19
+ }
20
+
21
+ public final class StationClient: @unchecked Sendable {
22
+ private let baseURL: URL
23
+ private let stationSlug: String
24
+ private let apiToken: String?
25
+ private let ingestToken: String?
26
+ private let session: URLSession
27
+
28
+ public init(
29
+ baseURL: URL,
30
+ stationSlug: String,
31
+ apiToken: String? = nil,
32
+ ingestToken: String? = nil,
33
+ session: URLSession = .shared
34
+ ) throws {
35
+ guard apiToken?.isEmpty == false || ingestToken?.isEmpty == false else {
36
+ throw RadioToolsSDKError.missingToken
37
+ }
38
+ self.baseURL = baseURL
39
+ self.stationSlug = stationSlug
40
+ self.apiToken = apiToken
41
+ self.ingestToken = ingestToken
42
+ self.session = session
43
+ }
44
+
45
+ public func getStation() async throws -> [String: Any] {
46
+ let path = "/api/v1/stations/\\(stationSlug.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? stationSlug)"
47
+ return try await getJSON(path: path, ingest: false)
48
+ }
49
+
50
+ public func getNowPlaying() async throws -> [String: Any] {
51
+ let station = try await getStation()
52
+ guard
53
+ let resources = station["resources"] as? [String: Any],
54
+ let href = resources["nowPlaying"] as? String,
55
+ !href.isEmpty
56
+ else {
57
+ throw RadioToolsSDKError.httpStatus(404, "nowPlaying unavailable")
58
+ }
59
+ return try await getJSON(path: href, ingest: false)
60
+ }
61
+
62
+ public func getWeather() async throws -> [String: Any]? {
63
+ let station = try await getStation()
64
+ guard
65
+ let resources = station["resources"] as? [String: Any],
66
+ let href = resources["weather"] as? String,
67
+ !href.isEmpty
68
+ else { return nil }
69
+ return try await getJSON(path: href, ingest: false)
70
+ }
71
+
72
+ public func getTraffic() async throws -> [String: Any]? {
73
+ let station = try await getStation()
74
+ guard
75
+ let resources = station["resources"] as? [String: Any],
76
+ let href = resources["traffic"] as? String,
77
+ !href.isEmpty
78
+ else { return nil }
79
+ return try await getJSON(path: href, ingest: false)
80
+ }
81
+
82
+ public func reportPlayback(
83
+ observation: [String: Any],
84
+ stationId: String? = nil
85
+ ) async throws -> [String: Any] {
86
+ var items: [URLQueryItem] = []
87
+ if let stationId, !stationId.isEmpty {
88
+ items.append(URLQueryItem(name: "stationId", value: stationId))
89
+ } else {
90
+ items.append(URLQueryItem(name: "stationSlug", value: stationSlug))
91
+ }
92
+ var components = URLComponents(url: baseURL.appendingPathComponent("${playbackPath}"), resolvingAgainstBaseURL: false)
93
+ components?.queryItems = items
94
+ guard let url = components?.url else { throw RadioToolsSDKError.invalidURL }
95
+ return try await postJSON(url: url, body: observation, ingest: true)
96
+ }
97
+
98
+ private func getJSON(path: String, ingest: Bool) async throws -> [String: Any] {
99
+ let url: URL
100
+ if path.hasPrefix("http") {
101
+ guard let parsed = URL(string: path) else { throw RadioToolsSDKError.invalidURL }
102
+ url = parsed
103
+ } else {
104
+ url = URL(string: path, relativeTo: baseURL)?.absoluteURL ?? baseURL
105
+ }
106
+ var request = URLRequest(url: url)
107
+ request.httpMethod = "GET"
108
+ request.setValue(try bearer(ingest: ingest), forHTTPHeaderField: "Authorization")
109
+ let (data, response) = try await session.data(for: request)
110
+ try validate(response: response, data: data)
111
+ let object = try JSONSerialization.jsonObject(with: data)
112
+ guard let dict = object as? [String: Any] else { return [:] }
113
+ return dict
114
+ }
115
+
116
+ private func postJSON(url: URL, body: [String: Any], ingest: Bool) async throws -> [String: Any] {
117
+ var request = URLRequest(url: url)
118
+ request.httpMethod = "POST"
119
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
120
+ request.setValue(try bearer(ingest: ingest), forHTTPHeaderField: "Authorization")
121
+ request.httpBody = try JSONSerialization.data(withJSONObject: body)
122
+ let (data, response) = try await session.data(for: request)
123
+ try validate(response: response, data: data)
124
+ let object = try JSONSerialization.jsonObject(with: data)
125
+ return object as? [String: Any] ?? [:]
126
+ }
127
+
128
+ private func bearer(ingest: Bool) throws -> String {
129
+ let token = ingest ? ingestToken : apiToken
130
+ guard let token, !token.isEmpty else { throw RadioToolsSDKError.missingToken }
131
+ return "Bearer \\(token)"
132
+ }
133
+
134
+ private func validate(response: URLResponse, data: Data) throws {
135
+ guard let http = response as? HTTPURLResponse else { return }
136
+ guard (200 ... 299).contains(http.statusCode) else {
137
+ let body = String(data: data, encoding: .utf8) ?? ""
138
+ throw RadioToolsSDKError.httpStatus(http.statusCode, body)
139
+ }
140
+ }
141
+ }
142
+ `;
143
+ }
144
+ export const swiftTarget = {
145
+ id: "swift",
146
+ description: "Swift URLSession client stubs (packages/sdk-swift)",
147
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-swift/Sources/RadioToolsSDK`,
148
+ generate: (repoRoot) => [
149
+ { path: "StationClient.swift", contents: renderSwiftClient() },
150
+ {
151
+ path: `${repoRoot}/packages/sdk-swift/Package.swift`,
152
+ contents: `// swift-tools-version: 6.0
153
+ import PackageDescription
154
+
155
+ let package = Package(
156
+ name: "RadioToolsSDK",
157
+ platforms: [.iOS(.v16), .macOS(.v13)],
158
+ products: [
159
+ .library(name: "RadioToolsSDK", targets: ["RadioToolsSDK"]),
160
+ ],
161
+ targets: [
162
+ .target(name: "RadioToolsSDK"),
163
+ ]
164
+ )
165
+ `,
166
+ },
167
+ {
168
+ path: `${repoRoot}/packages/sdk-swift/README.md`,
169
+ contents: `# RadioTools Swift SDK
170
+
171
+ Generated iOS/macOS REST stubs using \`URLSession\` and async/await.
172
+
173
+ Regenerate: \`bun run --filter @rivi-software/radiotools-sdk-generator generate --target swift\`
174
+
175
+ Use \`packages/openapi/openapi.json\` (from Zod) to generate typed models with Swift OpenAPI Generator if needed.
176
+ `,
177
+ },
178
+ ],
179
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const vueTarget: SdkTarget;
@@ -0,0 +1,67 @@
1
+ import { hookNameFromMethod, ZERO_ARG_READ_METHODS } from "../shared/reads";
2
+ function renderVueComposables() {
3
+ const composables = ZERO_ARG_READ_METHODS.map((method) => {
4
+ const name = hookNameFromMethod(method);
5
+ return `export function ${name}(client: StationClient) {
6
+ return useStationQuery(() => client.${method}());
7
+ }`;
8
+ }).join("\n\n");
9
+ return `/** Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand. */
10
+ import { type Ref, onMounted, onUnmounted, ref, shallowRef } from "vue";
11
+ import type { StationClient } from "@rivi-software/radiotools-sdk";
12
+
13
+ export type StationQuery<T> = {
14
+ data: Ref<T | undefined>;
15
+ error: Ref<Error | undefined>;
16
+ loading: Ref<boolean>;
17
+ refresh: () => Promise<void>;
18
+ };
19
+
20
+ function useStationQuery<T>(read: () => Promise<T>): StationQuery<T> {
21
+ const data = shallowRef<T | undefined>(undefined);
22
+ const error = ref<Error | undefined>(undefined);
23
+ const loading = ref(true);
24
+ let active = true;
25
+
26
+ const refresh = async () => {
27
+ loading.value = true;
28
+ try {
29
+ const value = await read();
30
+ if (!active) return;
31
+ data.value = value;
32
+ error.value = undefined;
33
+ } catch (cause: unknown) {
34
+ if (!active) return;
35
+ error.value = cause instanceof Error ? cause : new Error(String(cause));
36
+ } finally {
37
+ if (active) loading.value = false;
38
+ }
39
+ };
40
+
41
+ onMounted(() => {
42
+ active = true;
43
+ void refresh();
44
+ });
45
+ onUnmounted(() => {
46
+ active = false;
47
+ });
48
+
49
+ return { data, error, loading, refresh };
50
+ }
51
+
52
+ export function useRecommendedPollInterval(client: StationClient): StationQuery<number> {
53
+ return useStationQuery(() => client.recommendedPollIntervalMs());
54
+ }
55
+
56
+ ${composables}
57
+ `;
58
+ }
59
+ export const vueTarget = {
60
+ id: "vue",
61
+ description: "Vue composables wrapping @rivi-software/radiotools-sdk (packages/sdk-vue)",
62
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-vue/src`,
63
+ generate: () => [
64
+ { path: "composables.generated.ts", contents: renderVueComposables() },
65
+ { path: "index.ts", contents: `export * from "./composables.generated";\n` },
66
+ ],
67
+ };
@@ -0,0 +1,17 @@
1
+ export type GeneratorTargetId = "python" | "react" | "vue" | "svelte" | "go" | "csharp" | "ruby" | "php" | "openapi" | "kotlin" | "swift" | "next";
2
+ export type GeneratorTarget = GeneratorTargetId | "all";
3
+ export type GenerateOptions = {
4
+ repoRoot: string;
5
+ target: GeneratorTarget;
6
+ dryRun?: boolean;
7
+ };
8
+ export type GeneratedFile = {
9
+ path: string;
10
+ contents: string;
11
+ };
12
+ export type SdkTarget = {
13
+ id: GeneratorTargetId;
14
+ description: string;
15
+ outputDir: (repoRoot: string) => string;
16
+ generate: (repoRoot: string) => GeneratedFile[];
17
+ };