@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,180 @@
1
+ import { listenerSdkManifest } from "@rivi-software/radiotools-contract";
2
+ function renderKotlinClient() {
3
+ const manifest = listenerSdkManifest;
4
+ return `// Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand.
5
+ package com.radiotools.sdk
6
+
7
+ import kotlinx.serialization.json.Json
8
+ import kotlinx.serialization.json.JsonObject
9
+ import kotlinx.serialization.json.buildJsonObject
10
+ import kotlinx.serialization.json.put
11
+ import okhttp3.HttpUrl.Companion.toHttpUrl
12
+ import okhttp3.OkHttpClient
13
+ import okhttp3.Request
14
+ import okhttp3.RequestBody.Companion.toRequestBody
15
+ import okhttp3.MediaType.Companion.toMediaType
16
+ import java.io.IOException
17
+ import java.util.concurrent.TimeUnit
18
+
19
+ class StationClient private constructor(
20
+ private val baseUrl: String,
21
+ private val stationSlug: String,
22
+ private val apiToken: String?,
23
+ private val ingestToken: String?,
24
+ private val http: OkHttpClient,
25
+ private val json: Json,
26
+ ) {
27
+ companion object {
28
+ fun create(
29
+ baseUrl: String,
30
+ stationSlug: String,
31
+ apiToken: String? = null,
32
+ ingestToken: String? = null,
33
+ http: OkHttpClient? = null,
34
+ ): StationClient {
35
+ require(!apiToken.isNullOrBlank() || !ingestToken.isNullOrBlank()) {
36
+ "apiToken or ingestToken is required"
37
+ }
38
+ val client =
39
+ http
40
+ ?: OkHttpClient.Builder()
41
+ .connectTimeout(30, TimeUnit.SECONDS)
42
+ .readTimeout(30, TimeUnit.SECONDS)
43
+ .build()
44
+ return StationClient(
45
+ baseUrl.trimEnd('/'),
46
+ stationSlug,
47
+ apiToken?.trim()?.takeIf { it.isNotEmpty() },
48
+ ingestToken?.trim()?.takeIf { it.isNotEmpty() },
49
+ client,
50
+ Json { ignoreUnknownKeys = true },
51
+ )
52
+ }
53
+ }
54
+
55
+ @Throws(IOException::class)
56
+ fun getStation(): JsonObject = getJson("/api/v1/stations/\${encode(stationSlug)}", ingest = false)
57
+
58
+ @Throws(IOException::class)
59
+ fun getNowPlaying(): JsonObject {
60
+ val station = getStation()
61
+ val href =
62
+ station["resources"]?.let { it as? JsonObject }?.get("nowPlaying")?.toString()?.trim('"')
63
+ ?: error("nowPlaying resource unavailable")
64
+ return getJson(href, ingest = false)
65
+ }
66
+
67
+ @Throws(IOException::class)
68
+ fun getWeather(): JsonObject? {
69
+ val href =
70
+ getStation()["resources"]?.let { it as? JsonObject }?.get("weather")?.toString()?.trim('"')
71
+ if (href.isNullOrEmpty()) return null
72
+ return getJson(href, ingest = false)
73
+ }
74
+
75
+ @Throws(IOException::class)
76
+ fun getTraffic(): JsonObject? {
77
+ val href =
78
+ getStation()["resources"]?.let { it as? JsonObject }?.get("traffic")?.toString()?.trim('"')
79
+ if (href.isNullOrEmpty()) return null
80
+ return getJson(href, ingest = false)
81
+ }
82
+
83
+ @Throws(IOException::class)
84
+ fun reportPlayback(observation: JsonObject, stationId: String? = null): JsonObject {
85
+ val query =
86
+ if (!stationId.isNullOrBlank()) {
87
+ "stationId=\${encode(stationId)}"
88
+ } else {
89
+ "stationSlug=\${encode(stationSlug)}"
90
+ }
91
+ val path = "${manifest.integrations.playback.path}?\$query"
92
+ return postJson(path, observation, ingest = true)
93
+ }
94
+
95
+ private fun getJson(path: String, ingest: Boolean): JsonObject {
96
+ val url = if (path.startsWith("http")) path else "\$baseUrl\$path"
97
+ val request =
98
+ Request.Builder()
99
+ .url(url.toHttpUrl())
100
+ .header("Authorization", bearer(ingest))
101
+ .get()
102
+ .build()
103
+ http.newCall(request).execute().use { response ->
104
+ val body = response.body?.string() ?: "{}"
105
+ if (!response.isSuccessful) error("GET \${'$'}url failed: \${response.code} \${'$'}body")
106
+ return json.decodeFromString(JsonObject.serializer(), body)
107
+ }
108
+ }
109
+
110
+ private fun postJson(path: String, body: JsonObject, ingest: Boolean): JsonObject {
111
+ val url = "\$baseUrl\$path".toHttpUrl()
112
+ val mediaType = "application/json; charset=utf-8".toMediaType()
113
+ val payload = json.encodeToString(JsonObject.serializer(), body)
114
+ val request =
115
+ Request.Builder()
116
+ .url(url)
117
+ .header("Authorization", bearer(ingest))
118
+ .post(payload.toRequestBody(mediaType))
119
+ .build()
120
+ http.newCall(request).execute().use { response ->
121
+ val text = response.body?.string() ?: "{}"
122
+ if (!response.isSuccessful) error("POST \${'$'}url failed: \${response.code} \${'$'}text")
123
+ return json.decodeFromString(JsonObject.serializer(), text)
124
+ }
125
+ }
126
+
127
+ private fun bearer(ingest: Boolean): String {
128
+ val token = if (ingest) ingestToken else apiToken
129
+ require(!token.isNullOrBlank()) { "Missing bearer token" }
130
+ return "Bearer \$token"
131
+ }
132
+
133
+ private fun encode(value: String): String =
134
+ java.net.URLEncoder.encode(value, Charsets.UTF_8.name()).replace("+", "%20")
135
+ }
136
+ `;
137
+ }
138
+ export const kotlinTarget = {
139
+ id: "kotlin",
140
+ description: "Kotlin/Android OkHttp client stubs (packages/sdk-kotlin)",
141
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-kotlin/src/main/kotlin/com/radiotools/sdk`,
142
+ generate: (repoRoot) => [
143
+ { path: "StationClient.kt", contents: renderKotlinClient() },
144
+ {
145
+ path: `${repoRoot}/packages/sdk-kotlin/build.gradle.kts`,
146
+ contents: `plugins {
147
+ kotlin("jvm") version "2.1.0"
148
+ kotlin("plugin.serialization") version "2.1.0"
149
+ }
150
+
151
+ group = "com.radiotools"
152
+ version = "0.2.0"
153
+
154
+ repositories {
155
+ mavenCentral()
156
+ }
157
+
158
+ dependencies {
159
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
160
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
161
+ }
162
+
163
+ kotlin {
164
+ jvmToolchain(17)
165
+ }
166
+ `,
167
+ },
168
+ {
169
+ path: `${repoRoot}/packages/sdk-kotlin/README.md`,
170
+ contents: `# RadioTools Kotlin SDK
171
+
172
+ Generated Android/JVM REST stubs (OkHttp + kotlinx.serialization).
173
+
174
+ Regenerate: \`bun run --filter @rivi-software/radiotools-sdk-generator generate --target kotlin\`
175
+
176
+ OpenAPI models: see \`packages/openapi/openapi.json\` (from Zod \`sdkSchemas\`).
177
+ `,
178
+ },
179
+ ],
180
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const nextTarget: SdkTarget;
@@ -0,0 +1,80 @@
1
+ import { ZERO_ARG_READ_METHODS } from "../shared/reads";
2
+ function renderNextServerHelpers() {
3
+ const cachedReads = ZERO_ARG_READ_METHODS.map((method) => {
4
+ const fnName = `fetch${method.charAt(0).toUpperCase()}${method.slice(1)}`;
5
+ return `export async function ${fnName}(client: StationClient) {
6
+ return client.${method}();
7
+ }`;
8
+ }).join("\n\n");
9
+ const routeHandlers = ZERO_ARG_READ_METHODS.map((method) => {
10
+ const suffix = method.replace(/^get/, "");
11
+ const exportName = `jsonResponse${suffix}`;
12
+ const routeName = suffix.replace(/([A-Z])/g, "-$1").toLowerCase();
13
+ return `/** App Router helper for \`/api/radiotools/${routeName}\` */
14
+ export async function ${exportName}(client: StationClient) {
15
+ const data = await client.${method}();
16
+ return NextResponse.json(data);
17
+ }`;
18
+ }).join("\n\n");
19
+ return `/** Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand. */
20
+ import "server-only";
21
+
22
+ import {
23
+ createStationClientFromEnv,
24
+ type CreateStationClientOptions,
25
+ type StationClient,
26
+ } from "@rivi-software/radiotools-sdk";
27
+ import { unstable_cache } from "next/cache";
28
+ import { NextResponse } from "next/server";
29
+
30
+ const DEFAULT_REVALIDATE_SECONDS = 30;
31
+
32
+ export function createRadioToolsServerClient(
33
+ overrides: Partial<CreateStationClientOptions> = {},
34
+ ): StationClient {
35
+ return createStationClientFromEnv(undefined, overrides);
36
+ }
37
+
38
+ export function createCachedRadioToolsReader<T>(
39
+ cacheKey: string,
40
+ read: (client: StationClient) => Promise<T>,
41
+ revalidateSeconds: number = DEFAULT_REVALIDATE_SECONDS,
42
+ ) {
43
+ return unstable_cache(async () => read(createRadioToolsServerClient()), [cacheKey], {
44
+ revalidate: revalidateSeconds,
45
+ });
46
+ }
47
+
48
+ ${cachedReads}
49
+
50
+ export async function fetchRecommendedPollIntervalMs(client: StationClient) {
51
+ return client.recommendedPollIntervalMs();
52
+ }
53
+
54
+ export const cachedGetStation = createCachedRadioToolsReader("radiotools:station", (c) => c.getStation());
55
+ export const cachedGetNowPlaying = createCachedRadioToolsReader("radiotools:now-playing", (c) =>
56
+ c.getNowPlaying(),
57
+ );
58
+ export const cachedGetCurrentBroadcast = createCachedRadioToolsReader(
59
+ "radiotools:current-broadcast",
60
+ (c) => c.getCurrentBroadcast(),
61
+ );
62
+
63
+ ${routeHandlers}
64
+
65
+ export type { StationClient, CreateStationClientOptions };
66
+ `;
67
+ }
68
+ export const nextTarget = {
69
+ id: "next",
70
+ description: "Next.js App Router server helpers (packages/sdk-next)",
71
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-next/src`,
72
+ generate: () => [
73
+ { path: "server.generated.ts", contents: renderNextServerHelpers() },
74
+ {
75
+ path: "index.ts",
76
+ contents: `export * from "./server.generated";
77
+ `,
78
+ },
79
+ ],
80
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const openapiTarget: SdkTarget;
@@ -0,0 +1,223 @@
1
+ import { listenerSdkManifest } from "@rivi-software/radiotools-contract";
2
+ import { buildZodOpenApiComponents, jsonSchemaRef, operationResponseSchemas, } from "../openapi/zod-components";
3
+ function errorResponseContent() {
4
+ return {
5
+ "application/json": {
6
+ schema: jsonSchemaRef("ErrorResponse"),
7
+ },
8
+ };
9
+ }
10
+ function successResponse(operationId) {
11
+ const mapping = operationResponseSchemas[operationId];
12
+ if (!mapping) {
13
+ return { description: "Success" };
14
+ }
15
+ const schema = jsonSchemaRef(mapping.schema);
16
+ return {
17
+ description: mapping.description,
18
+ content: {
19
+ "application/json": {
20
+ schema: mapping.nullable ? { anyOf: [schema, { type: "null" }] } : schema,
21
+ },
22
+ },
23
+ };
24
+ }
25
+ function renderOpenApi() {
26
+ const manifest = listenerSdkManifest;
27
+ const envDescriptions = Object.fromEntries(manifest.env.map((entry) => [entry.name, entry.description]));
28
+ const zodComponents = buildZodOpenApiComponents();
29
+ const paths = {
30
+ "/api/v1/stations/{stationSlug}": {
31
+ get: {
32
+ operationId: "getStation",
33
+ summary: "Published station metadata",
34
+ tags: ["listener"],
35
+ security: [{ bearerListener: [] }],
36
+ parameters: [
37
+ {
38
+ name: "stationSlug",
39
+ in: "path",
40
+ required: true,
41
+ schema: { type: "string" },
42
+ },
43
+ ],
44
+ responses: {
45
+ "200": successResponse("getStation"),
46
+ "401": { description: "Missing or invalid listener token", content: errorResponseContent() },
47
+ "404": { description: "Station not found", content: errorResponseContent() },
48
+ },
49
+ },
50
+ },
51
+ [manifest.integrations.playback.path]: {
52
+ post: {
53
+ operationId: "reportPlayback",
54
+ summary: "Encoder playback observation",
55
+ tags: ["ingest"],
56
+ security: [{ bearerIngest: [] }],
57
+ parameters: [
58
+ {
59
+ name: "stationSlug",
60
+ in: "query",
61
+ required: false,
62
+ schema: { type: "string" },
63
+ },
64
+ {
65
+ name: "stationId",
66
+ in: "query",
67
+ required: false,
68
+ schema: { type: "string" },
69
+ },
70
+ ],
71
+ requestBody: {
72
+ required: true,
73
+ content: {
74
+ "application/json": {
75
+ schema: jsonSchemaRef("PlaybackObservationIngestBody"),
76
+ },
77
+ },
78
+ },
79
+ responses: {
80
+ "200": successResponse("reportPlayback"),
81
+ "401": { description: "Missing or invalid ingest token", content: errorResponseContent() },
82
+ "503": { description: "Temporary failure", content: errorResponseContent() },
83
+ },
84
+ },
85
+ },
86
+ };
87
+ const queryParamsByOperation = {
88
+ getContent: [
89
+ { name: "type", required: true, schema: { type: "string" } },
90
+ { name: "cursor", schema: { type: "string", format: "uuid" } },
91
+ { name: "limit", schema: { type: "integer", minimum: 1, maximum: 50, default: 25 } },
92
+ ],
93
+ getSchedule: [
94
+ { name: "fromDate", required: true, schema: { type: "string", format: "date" } },
95
+ { name: "throughDate", required: true, schema: { type: "string", format: "date" } },
96
+ ],
97
+ getPrograms: [
98
+ { name: "fromDate", required: true, schema: { type: "string", format: "date" } },
99
+ { name: "throughDate", required: true, schema: { type: "string", format: "date" } },
100
+ ],
101
+ getTrackHistory: [
102
+ { name: "cursor", schema: { type: "string" } },
103
+ { name: "limit", schema: { type: "integer", minimum: 1, maximum: 50, default: 25 } },
104
+ ],
105
+ };
106
+ for (const method of manifest.clientMethods) {
107
+ if (!method.rest || method.rest.method !== "GET" || !method.rest.resourceKey)
108
+ continue;
109
+ if (method.name === "getStation")
110
+ continue;
111
+ const pathKey = method.rest.resourceKey === "content"
112
+ ? `/api/v1/stations/{stationSlug}/content`
113
+ : method.rest.resourceKey === "schemas"
114
+ ? `/api/v1/stations/{stationSlug}/schemas`
115
+ : `/api/v1/stations/{stationSlug}/${method.rest.resourceKey}`;
116
+ const extraParams = queryParamsByOperation[method.name] ?? [];
117
+ paths[pathKey] = {
118
+ get: {
119
+ operationId: method.name,
120
+ summary: method.description,
121
+ tags: ["listener"],
122
+ security: [{ bearerListener: [] }],
123
+ parameters: [
124
+ { name: "stationSlug", in: "path", required: true, schema: { type: "string" } },
125
+ ...extraParams.map((param) => ({
126
+ name: param.name,
127
+ in: "query",
128
+ required: param.required ?? false,
129
+ schema: param.schema,
130
+ })),
131
+ ],
132
+ responses: {
133
+ "200": successResponse(method.name),
134
+ "401": { description: "Unauthorized", content: errorResponseContent() },
135
+ "404": { description: "Resource disabled or not found", content: errorResponseContent() },
136
+ },
137
+ },
138
+ };
139
+ if (method.name === "getContent") {
140
+ paths[`/api/v1/stations/{stationSlug}/content/{id}`] = {
141
+ get: {
142
+ operationId: "getContentRecord",
143
+ summary: "One published content record by id",
144
+ tags: ["listener"],
145
+ security: [{ bearerListener: [] }],
146
+ parameters: [
147
+ { name: "stationSlug", in: "path", required: true, schema: { type: "string" } },
148
+ { name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } },
149
+ ],
150
+ responses: {
151
+ "200": successResponse("getContentRecord"),
152
+ "404": { description: "Not found", content: errorResponseContent() },
153
+ },
154
+ },
155
+ };
156
+ }
157
+ }
158
+ const doc = {
159
+ openapi: "3.1.0",
160
+ info: {
161
+ title: "RadioTools Listener API",
162
+ version: `${manifest.contractVersion}.0.0`,
163
+ description: "OpenAPI generated from @rivi-software/radiotools-contract paths and @rivi-software/radiotools-sdk Zod schemas (sdkSchemas). Station resource hrefs may differ from these templates.",
164
+ },
165
+ servers: [
166
+ {
167
+ url: "{RADIOTOOLS_BASE_URL}",
168
+ variables: { RADIOTOOLS_BASE_URL: { default: "https://radio.example" } },
169
+ },
170
+ ],
171
+ tags: [
172
+ { name: "listener", description: "Requires listener:read (RADIOTOOLS_API_TOKEN)" },
173
+ { name: "ingest", description: "Requires ingest:write (RADIOTOOLS_INGEST_TOKEN)" },
174
+ ],
175
+ components: {
176
+ securitySchemes: {
177
+ bearerListener: {
178
+ type: "http",
179
+ scheme: "bearer",
180
+ description: envDescriptions.RADIOTOOLS_API_TOKEN ?? "Station listener key",
181
+ },
182
+ bearerIngest: {
183
+ type: "http",
184
+ scheme: "bearer",
185
+ description: envDescriptions.RADIOTOOLS_INGEST_TOKEN ?? "Playback ingest key",
186
+ },
187
+ },
188
+ schemas: {
189
+ ...zodComponents.schemas,
190
+ },
191
+ },
192
+ paths,
193
+ "x-radiotools": {
194
+ contractVersion: manifest.contractVersion,
195
+ socketProtocol: manifest.socketProtocol,
196
+ zodSchemaSource: "@rivi-software/radiotools-sdk/schemas#sdkSchemas",
197
+ graphqlSchema: "@rivi-software/radiotools-contract/graphql",
198
+ },
199
+ };
200
+ return `${JSON.stringify(doc, null, 2)}\n`;
201
+ }
202
+ export const openapiTarget = {
203
+ id: "openapi",
204
+ description: "OpenAPI 3.1 with components from Zod sdkSchemas (packages/openapi)",
205
+ outputDir: (repoRoot) => `${repoRoot}/packages/openapi`,
206
+ generate: () => [
207
+ { path: "openapi.json", contents: renderOpenApi() },
208
+ {
209
+ path: "README.md",
210
+ contents: `# RadioTools OpenAPI
211
+
212
+ \`openapi.json\` is generated from:
213
+
214
+ - REST paths in \`@rivi-software/radiotools-contract\`
215
+ - Response and request bodies from \`@rivi-software/radiotools-sdk\` **Zod** types (\`sdkSchemas\`) via \`z.toJSONSchema\`
216
+
217
+ \`\`\`bash
218
+ bun run --filter @rivi-software/radiotools-sdk-generator generate --target openapi
219
+ \`\`\`
220
+ `,
221
+ },
222
+ ],
223
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const phpTarget: SdkTarget;
@@ -0,0 +1,165 @@
1
+ import { listenerSdkManifest } from "@rivi-software/radiotools-contract";
2
+ function renderPhpClient() {
3
+ const playbackPath = listenerSdkManifest.integrations.playback.path;
4
+ return `<?php
5
+ // Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand.
6
+ declare(strict_types=1);
7
+
8
+ namespace RadioTools\\Sdk;
9
+
10
+ final class StationClient
11
+ {
12
+ public function __construct(
13
+ private readonly string $baseUrl,
14
+ private readonly string $stationSlug,
15
+ private readonly ?string $apiToken = null,
16
+ private readonly ?string $ingestToken = null,
17
+ ) {
18
+ if (($this->apiToken ?? '') === '' && ($this->ingestToken ?? '') === '') {
19
+ throw new \\InvalidArgumentException('apiToken or ingestToken is required');
20
+ }
21
+ }
22
+
23
+ public static function fromEnv(): self
24
+ {
25
+ $base = trim(getenv('RADIOTOOLS_BASE_URL') ?: '');
26
+ $slug = trim(getenv('RADIOTOOLS_STATION_SLUG') ?: '');
27
+ if ($base === '' || $slug === '') {
28
+ throw new \\RuntimeException('Set RADIOTOOLS_BASE_URL and RADIOTOOLS_STATION_SLUG');
29
+ }
30
+ return new self(
31
+ $base,
32
+ $slug,
33
+ getenv('RADIOTOOLS_API_TOKEN') ?: null,
34
+ getenv('RADIOTOOLS_INGEST_TOKEN') ?: null,
35
+ );
36
+ }
37
+
38
+ /** @return array<string, mixed> */
39
+ public function getStation(): array
40
+ {
41
+ $path = '/api/v1/stations/' . rawurlencode($this->stationSlug);
42
+ return $this->getJson($path, ingest: false);
43
+ }
44
+
45
+ /** @return array<string, mixed>|null */
46
+ public function getWeather(): ?array
47
+ {
48
+ $station = $this->getStation();
49
+ $href = $station['resources']['weather'] ?? null;
50
+ if (!is_string($href) || $href === '') {
51
+ return null;
52
+ }
53
+ return $this->getJson($href, ingest: false);
54
+ }
55
+
56
+ /** @return array<string, mixed>|null */
57
+ public function getTraffic(): ?array
58
+ {
59
+ $station = $this->getStation();
60
+ $href = $station['resources']['traffic'] ?? null;
61
+ if (!is_string($href) || $href === '') {
62
+ return null;
63
+ }
64
+ return $this->getJson($href, ingest: false);
65
+ }
66
+
67
+ /** @param array<string, mixed> $observation
68
+ * @return array<string, mixed>
69
+ */
70
+ public function reportPlayback(array $observation, ?string $stationId = null): array
71
+ {
72
+ $query = $stationId !== null
73
+ ? 'stationId=' . rawurlencode($stationId)
74
+ : 'stationSlug=' . rawurlencode($this->stationSlug);
75
+ return $this->postJson('${playbackPath}?' . $query, $observation, ingest: true);
76
+ }
77
+
78
+ /** @return array<string, mixed> */
79
+ private function getJson(string $path, bool $ingest): array
80
+ {
81
+ $url = str_starts_with($path, 'http') ? $path : rtrim($this->baseUrl, '/') . $path;
82
+ return $this->request('GET', $url, null, $ingest);
83
+ }
84
+
85
+ /** @param array<string, mixed> $body
86
+ * @return array<string, mixed>
87
+ */
88
+ private function postJson(string $path, array $body, bool $ingest): array
89
+ {
90
+ $url = rtrim($this->baseUrl, '/') . $path;
91
+ return $this->request('POST', $url, json_encode($body, JSON_THROW_ON_ERROR), $ingest);
92
+ }
93
+
94
+ /** @return array<string, mixed> */
95
+ private function request(string $method, string $url, ?string $body, bool $ingest): array
96
+ {
97
+ $token = $ingest ? $this->ingestToken : $this->apiToken;
98
+ if ($token === null || $token === '') {
99
+ throw new \\RuntimeException('Missing bearer token');
100
+ }
101
+
102
+ $ch = curl_init($url);
103
+ if ($ch === false) {
104
+ throw new \\RuntimeException('curl_init failed');
105
+ }
106
+
107
+ $headers = [
108
+ 'Authorization: Bearer ' . $token,
109
+ 'Accept: application/json',
110
+ ];
111
+ if ($body !== null) {
112
+ $headers[] = 'Content-Type: application/json';
113
+ }
114
+
115
+ curl_setopt_array($ch, [
116
+ CURLOPT_RETURNTRANSFER => true,
117
+ CURLOPT_CUSTOMREQUEST => $method,
118
+ CURLOPT_HTTPHEADER => $headers,
119
+ CURLOPT_POSTFIELDS => $body,
120
+ ]);
121
+
122
+ $raw = curl_exec($ch);
123
+ $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
124
+ curl_close($ch);
125
+
126
+ if ($raw === false || $code >= 400) {
127
+ throw new \\RuntimeException("HTTP {$code}: " . (string) $raw);
128
+ }
129
+
130
+ /** @var array<string, mixed> */
131
+ return json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
132
+ }
133
+ }
134
+ `;
135
+ }
136
+ export const phpTarget = {
137
+ id: "php",
138
+ description: "PHP curl client stubs (packages/sdk-php)",
139
+ outputDir: (repoRoot) => `${repoRoot}/packages/sdk-php/src`,
140
+ generate: () => [
141
+ { path: "StationClient.php", contents: renderPhpClient() },
142
+ {
143
+ path: "../composer.json",
144
+ contents: JSON.stringify({
145
+ name: "radiotools/sdk",
146
+ description: "Generated RadioTools listener REST stubs",
147
+ type: "library",
148
+ license: "MIT",
149
+ require: { php: ">=8.2" },
150
+ autoload: {
151
+ "psr-4": {
152
+ "RadioTools\\Sdk\\": "src/",
153
+ },
154
+ },
155
+ }, null, 2),
156
+ },
157
+ {
158
+ path: "../README.md",
159
+ contents: `# radiotools/sdk (PHP)
160
+
161
+ Generated REST stubs. Regenerate: \`bun run --filter @rivi-software/radiotools-sdk-generator generate --target php\`.
162
+ `,
163
+ },
164
+ ],
165
+ };
@@ -0,0 +1,2 @@
1
+ import type { SdkTarget } from "../types";
2
+ export declare const pythonTarget: SdkTarget;