@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.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +44 -0
- package/dist/engine.d.ts +3 -0
- package/dist/engine.js +60 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/openapi/zod-components.d.ts +13 -0
- package/dist/openapi/zod-components.js +47 -0
- package/dist/shared/naming.d.ts +2 -0
- package/dist/shared/naming.js +6 -0
- package/dist/shared/reads.d.ts +5 -0
- package/dist/shared/reads.js +14 -0
- package/dist/targets/csharp.d.ts +2 -0
- package/dist/targets/csharp.js +137 -0
- package/dist/targets/go.d.ts +2 -0
- package/dist/targets/go.js +185 -0
- package/dist/targets/kotlin.d.ts +2 -0
- package/dist/targets/kotlin.js +180 -0
- package/dist/targets/next.d.ts +2 -0
- package/dist/targets/next.js +80 -0
- package/dist/targets/openapi.d.ts +2 -0
- package/dist/targets/openapi.js +223 -0
- package/dist/targets/php.d.ts +2 -0
- package/dist/targets/php.js +165 -0
- package/dist/targets/python.d.ts +2 -0
- package/dist/targets/python.js +124 -0
- package/dist/targets/react.d.ts +2 -0
- package/dist/targets/react.js +82 -0
- package/dist/targets/ruby.d.ts +2 -0
- package/dist/targets/ruby.js +122 -0
- package/dist/targets/svelte.d.ts +2 -0
- package/dist/targets/svelte.js +76 -0
- package/dist/targets/swift.d.ts +2 -0
- package/dist/targets/swift.js +179 -0
- package/dist/targets/vue.d.ts +2 -0
- package/dist/targets/vue.js +67 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +1 -0
- package/package.json +39 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env bun
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { generateSdks, listTargets } from "./engine";
|
|
6
|
+
function repoRootFromImportMeta(metaUrl) {
|
|
7
|
+
const here = dirname(fileURLToPath(metaUrl));
|
|
8
|
+
return join(here, "..", "..", "..");
|
|
9
|
+
}
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const [command = "help", ...rest] = argv;
|
|
12
|
+
let target = "all";
|
|
13
|
+
let dryRun = false;
|
|
14
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
15
|
+
const arg = rest[i];
|
|
16
|
+
if (arg === "--target" && rest[i + 1]) {
|
|
17
|
+
target = rest[i + 1];
|
|
18
|
+
i += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === "--dry-run")
|
|
22
|
+
dryRun = true;
|
|
23
|
+
}
|
|
24
|
+
return { command, target, dryRun };
|
|
25
|
+
}
|
|
26
|
+
const { command, target, dryRun } = parseArgs(process.argv.slice(2));
|
|
27
|
+
const repoRoot = repoRootFromImportMeta(import.meta.url);
|
|
28
|
+
switch (command) {
|
|
29
|
+
case "generate":
|
|
30
|
+
generateSdks({ repoRoot, target, dryRun });
|
|
31
|
+
break;
|
|
32
|
+
case "list":
|
|
33
|
+
for (const entry of listTargets()) {
|
|
34
|
+
console.log(`${entry.id}\t${entry.description}`);
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
console.log(`Usage:
|
|
39
|
+
bun src/cli.ts generate [--target <id>|all] [--dry-run]
|
|
40
|
+
bun src/cli.ts list
|
|
41
|
+
|
|
42
|
+
Targets: ${listTargets().map((t) => t.id).join(", ")}, all`);
|
|
43
|
+
break;
|
|
44
|
+
}
|
package/dist/engine.d.ts
ADDED
package/dist/engine.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { csharpTarget } from "./targets/csharp";
|
|
4
|
+
import { goTarget } from "./targets/go";
|
|
5
|
+
import { kotlinTarget } from "./targets/kotlin";
|
|
6
|
+
import { nextTarget } from "./targets/next";
|
|
7
|
+
import { openapiTarget } from "./targets/openapi";
|
|
8
|
+
import { phpTarget } from "./targets/php";
|
|
9
|
+
import { pythonTarget } from "./targets/python";
|
|
10
|
+
import { reactTarget } from "./targets/react";
|
|
11
|
+
import { rubyTarget } from "./targets/ruby";
|
|
12
|
+
import { svelteTarget } from "./targets/svelte";
|
|
13
|
+
import { swiftTarget } from "./targets/swift";
|
|
14
|
+
import { vueTarget } from "./targets/vue";
|
|
15
|
+
const targets = [
|
|
16
|
+
pythonTarget,
|
|
17
|
+
reactTarget,
|
|
18
|
+
vueTarget,
|
|
19
|
+
svelteTarget,
|
|
20
|
+
goTarget,
|
|
21
|
+
csharpTarget,
|
|
22
|
+
rubyTarget,
|
|
23
|
+
phpTarget,
|
|
24
|
+
openapiTarget,
|
|
25
|
+
kotlinTarget,
|
|
26
|
+
swiftTarget,
|
|
27
|
+
nextTarget,
|
|
28
|
+
];
|
|
29
|
+
export function listTargets() {
|
|
30
|
+
return targets;
|
|
31
|
+
}
|
|
32
|
+
function resolveTargets(target) {
|
|
33
|
+
if (target === "all")
|
|
34
|
+
return targets;
|
|
35
|
+
const match = targets.find((entry) => entry.id === target);
|
|
36
|
+
if (!match) {
|
|
37
|
+
throw new Error(`Unknown target "${target}". Available: ${targets.map((t) => t.id).join(", ")}, all`);
|
|
38
|
+
}
|
|
39
|
+
return [match];
|
|
40
|
+
}
|
|
41
|
+
function writeGeneratedFile(baseDir, file, dryRun) {
|
|
42
|
+
const fullPath = isAbsolute(file.path) ? file.path : join(baseDir, file.path);
|
|
43
|
+
if (dryRun) {
|
|
44
|
+
console.log(`[dry-run] would write ${fullPath} (${file.contents.length} bytes)`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
48
|
+
writeFileSync(fullPath, file.contents, "utf8");
|
|
49
|
+
console.log(`wrote ${fullPath}`);
|
|
50
|
+
}
|
|
51
|
+
export function generateSdks(options) {
|
|
52
|
+
const selected = resolveTargets(options.target);
|
|
53
|
+
for (const target of selected) {
|
|
54
|
+
const outDir = target.outputDir(options.repoRoot);
|
|
55
|
+
const files = target.generate(options.repoRoot);
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
writeGeneratedFile(outDir, file, options.dryRun ?? false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type OpenApiComponents = {
|
|
2
|
+
schemas: Record<string, Record<string, unknown>>;
|
|
3
|
+
};
|
|
4
|
+
/** JSON Schema components generated from `@rivi-software/radiotools-sdk` Zod types (`sdkSchemas`). */
|
|
5
|
+
export declare function buildZodOpenApiComponents(): OpenApiComponents;
|
|
6
|
+
export declare const operationResponseSchemas: Record<string, {
|
|
7
|
+
schema: string;
|
|
8
|
+
nullable?: boolean;
|
|
9
|
+
description: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function jsonSchemaRef(componentName: string): {
|
|
12
|
+
$ref: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { sdkSchemas } from "@rivi-software/radiotools-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function stripMeta(schema) {
|
|
4
|
+
const { $schema: _, ...rest } = schema;
|
|
5
|
+
return rest;
|
|
6
|
+
}
|
|
7
|
+
/** JSON Schema components generated from `@rivi-software/radiotools-sdk` Zod types (`sdkSchemas`). */
|
|
8
|
+
export function buildZodOpenApiComponents() {
|
|
9
|
+
const schemas = {};
|
|
10
|
+
for (const [name, zodSchema] of Object.entries(sdkSchemas)) {
|
|
11
|
+
const json = z.toJSONSchema(zodSchema, {
|
|
12
|
+
io: "output",
|
|
13
|
+
reused: "inline",
|
|
14
|
+
unrepresentable: "any",
|
|
15
|
+
});
|
|
16
|
+
schemas[name] = stripMeta(json);
|
|
17
|
+
}
|
|
18
|
+
return { schemas };
|
|
19
|
+
}
|
|
20
|
+
export const operationResponseSchemas = {
|
|
21
|
+
getStation: { schema: "PublicStation", description: "Published station metadata" },
|
|
22
|
+
getSchemas: { schema: "PublicSchemaList", description: "Content type schemas" },
|
|
23
|
+
getContent: { schema: "PublishedCollection", description: "Published content page" },
|
|
24
|
+
getContentRecord: { schema: "PublishedRecord", description: "One published record" },
|
|
25
|
+
getSchedule: { schema: "PublicSchedule", description: "Broadcast schedule" },
|
|
26
|
+
getPrograms: { schema: "PublicPrograms", description: "Program summaries" },
|
|
27
|
+
getCurrentBroadcast: { schema: "PublicCurrentBroadcast", description: "Current broadcast" },
|
|
28
|
+
getNowPlaying: { schema: "PublicNowPlaying", description: "Now playing" },
|
|
29
|
+
getTrackHistory: { schema: "PublicTrackHistory", description: "Track history page" },
|
|
30
|
+
getWeather: {
|
|
31
|
+
schema: "WeatherPublic",
|
|
32
|
+
nullable: true,
|
|
33
|
+
description: "Weather snapshot when enabled",
|
|
34
|
+
},
|
|
35
|
+
getTraffic: {
|
|
36
|
+
schema: "TrafficPublic",
|
|
37
|
+
nullable: true,
|
|
38
|
+
description: "Traffic snapshot when enabled",
|
|
39
|
+
},
|
|
40
|
+
reportPlayback: {
|
|
41
|
+
schema: "PlaybackObservationResult",
|
|
42
|
+
description: "Playback ingest acknowledgement",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
export function jsonSchemaRef(componentName) {
|
|
46
|
+
return { $ref: `#/components/schemas/${componentName}` };
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Client reads with no required parameters — safe for simple hooks/composables/stores. */
|
|
2
|
+
export declare const ZERO_ARG_READ_METHODS: readonly ["getStation", "getCurrentBroadcast", "getNowPlaying", "getWeather", "getTraffic"];
|
|
3
|
+
export type ZeroArgReadMethod = (typeof ZERO_ARG_READ_METHODS)[number];
|
|
4
|
+
export declare function isZeroArgReadMethod(name: string): name is ZeroArgReadMethod;
|
|
5
|
+
export declare function hookNameFromMethod(method: ZeroArgReadMethod): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Client reads with no required parameters — safe for simple hooks/composables/stores. */
|
|
2
|
+
export const ZERO_ARG_READ_METHODS = [
|
|
3
|
+
"getStation",
|
|
4
|
+
"getCurrentBroadcast",
|
|
5
|
+
"getNowPlaying",
|
|
6
|
+
"getWeather",
|
|
7
|
+
"getTraffic",
|
|
8
|
+
];
|
|
9
|
+
export function isZeroArgReadMethod(name) {
|
|
10
|
+
return ZERO_ARG_READ_METHODS.includes(name);
|
|
11
|
+
}
|
|
12
|
+
export function hookNameFromMethod(method) {
|
|
13
|
+
return `use${method.charAt(0).toUpperCase()}${method.slice(1)}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { listenerSdkManifest } from "@rivi-software/radiotools-contract";
|
|
2
|
+
function renderCsharpClient() {
|
|
3
|
+
const playbackPath = listenerSdkManifest.integrations.playback.path;
|
|
4
|
+
return `// Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand.
|
|
5
|
+
using System.Net.Http.Headers;
|
|
6
|
+
using System.Net.Http.Json;
|
|
7
|
+
using System.Text.Json;
|
|
8
|
+
|
|
9
|
+
namespace RadioTools.Sdk;
|
|
10
|
+
|
|
11
|
+
public sealed class StationClient : IDisposable
|
|
12
|
+
{
|
|
13
|
+
private readonly HttpClient _http;
|
|
14
|
+
private readonly string _baseUrl;
|
|
15
|
+
private readonly string _stationSlug;
|
|
16
|
+
private readonly string? _apiToken;
|
|
17
|
+
private readonly string? _ingestToken;
|
|
18
|
+
|
|
19
|
+
public StationClient(string baseUrl, string stationSlug, string? apiToken = null, string? ingestToken = null, HttpClient? http = null)
|
|
20
|
+
{
|
|
21
|
+
if (string.IsNullOrWhiteSpace(apiToken) && string.IsNullOrWhiteSpace(ingestToken))
|
|
22
|
+
throw new ArgumentException("apiToken or ingestToken is required");
|
|
23
|
+
_baseUrl = baseUrl.TrimEnd('/');
|
|
24
|
+
_stationSlug = stationSlug;
|
|
25
|
+
_apiToken = apiToken;
|
|
26
|
+
_ingestToken = ingestToken;
|
|
27
|
+
_http = http ?? new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static StationClient FromEnvironment()
|
|
31
|
+
{
|
|
32
|
+
var baseUrl = Environment.GetEnvironmentVariable("RADIOTOOLS_BASE_URL") ?? "";
|
|
33
|
+
var slug = Environment.GetEnvironmentVariable("RADIOTOOLS_STATION_SLUG") ?? "";
|
|
34
|
+
var api = Environment.GetEnvironmentVariable("RADIOTOOLS_API_TOKEN");
|
|
35
|
+
var ingest = Environment.GetEnvironmentVariable("RADIOTOOLS_INGEST_TOKEN");
|
|
36
|
+
if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(slug))
|
|
37
|
+
throw new InvalidOperationException("Set RADIOTOOLS_BASE_URL and RADIOTOOLS_STATION_SLUG");
|
|
38
|
+
return new StationClient(baseUrl, slug, api, ingest);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public async Task<JsonElement> GetStationAsync(CancellationToken cancellationToken = default)
|
|
42
|
+
{
|
|
43
|
+
var path = $"/api/v1/stations/{Uri.EscapeDataString(_stationSlug)}";
|
|
44
|
+
return await GetJsonAsync(path, ingest: false, cancellationToken);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async Task<JsonElement?> GetWeatherAsync(CancellationToken cancellationToken = default)
|
|
48
|
+
{
|
|
49
|
+
var station = await GetStationAsync(cancellationToken);
|
|
50
|
+
if (!station.TryGetProperty("resources", out var resources)) return null;
|
|
51
|
+
if (!resources.TryGetProperty("weather", out var hrefProp)) return null;
|
|
52
|
+
var href = hrefProp.GetString();
|
|
53
|
+
if (string.IsNullOrEmpty(href)) return null;
|
|
54
|
+
return await GetJsonAsync(href, ingest: false, cancellationToken);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async Task<JsonElement?> GetTrafficAsync(CancellationToken cancellationToken = default)
|
|
58
|
+
{
|
|
59
|
+
var station = await GetStationAsync(cancellationToken);
|
|
60
|
+
if (!station.TryGetProperty("resources", out var resources)) return null;
|
|
61
|
+
if (!resources.TryGetProperty("traffic", out var hrefProp)) return null;
|
|
62
|
+
var href = hrefProp.GetString();
|
|
63
|
+
if (string.IsNullOrEmpty(href)) return null;
|
|
64
|
+
return await GetJsonAsync(href, ingest: false, cancellationToken);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async Task<JsonElement> ReportPlaybackAsync(object observation, string? stationId = null, CancellationToken cancellationToken = default)
|
|
68
|
+
{
|
|
69
|
+
var query = stationId is not null
|
|
70
|
+
? $"?stationId={Uri.EscapeDataString(stationId)}"
|
|
71
|
+
: $"?stationSlug={Uri.EscapeDataString(_stationSlug)}";
|
|
72
|
+
var url = $"{_baseUrl}${playbackPath}{query}";
|
|
73
|
+
using var request = new HttpRequestMessage(HttpMethod.Post, url)
|
|
74
|
+
{
|
|
75
|
+
Content = JsonContent.Create(observation),
|
|
76
|
+
};
|
|
77
|
+
ApplyAuth(request, ingest: true);
|
|
78
|
+
using var response = await _http.SendAsync(request, cancellationToken);
|
|
79
|
+
response.EnsureSuccessStatusCode();
|
|
80
|
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
81
|
+
return await JsonSerializer.DeserializeAsync<JsonElement>(stream, cancellationToken: cancellationToken)
|
|
82
|
+
?? throw new InvalidOperationException("empty playback response");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async Task<JsonElement> GetJsonAsync(string path, bool ingest, CancellationToken cancellationToken)
|
|
86
|
+
{
|
|
87
|
+
var url = path.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? path : $"{_baseUrl}{path}";
|
|
88
|
+
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
89
|
+
ApplyAuth(request, ingest);
|
|
90
|
+
using var response = await _http.SendAsync(request, cancellationToken);
|
|
91
|
+
response.EnsureSuccessStatusCode();
|
|
92
|
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
93
|
+
return await JsonSerializer.DeserializeAsync<JsonElement>(stream, cancellationToken: cancellationToken)
|
|
94
|
+
?? throw new InvalidOperationException("empty JSON response");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private void ApplyAuth(HttpRequestMessage request, bool ingest)
|
|
98
|
+
{
|
|
99
|
+
var token = ingest ? _ingestToken : _apiToken;
|
|
100
|
+
if (string.IsNullOrWhiteSpace(token))
|
|
101
|
+
throw new InvalidOperationException("Missing bearer token");
|
|
102
|
+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public void Dispose() => _http.Dispose();
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
export const csharpTarget = {
|
|
110
|
+
id: "csharp",
|
|
111
|
+
description: ".NET HttpClient stubs (packages/sdk-dotnet)",
|
|
112
|
+
outputDir: (repoRoot) => `${repoRoot}/packages/sdk-dotnet/src`,
|
|
113
|
+
generate: () => [
|
|
114
|
+
{ path: "StationClient.cs", contents: renderCsharpClient() },
|
|
115
|
+
{
|
|
116
|
+
path: "../RadioTools.Sdk.csproj",
|
|
117
|
+
contents: `<Project Sdk="Microsoft.NET.Sdk">
|
|
118
|
+
<PropertyGroup>
|
|
119
|
+
<TargetFramework>net8.0</TargetFramework>
|
|
120
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
121
|
+
<Nullable>enable</Nullable>
|
|
122
|
+
<RootNamespace>RadioTools.Sdk</RootNamespace>
|
|
123
|
+
<AssemblyName>RadioTools.Sdk</AssemblyName>
|
|
124
|
+
<Version>0.2.0</Version>
|
|
125
|
+
</PropertyGroup>
|
|
126
|
+
</Project>
|
|
127
|
+
`,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
path: "../README.md",
|
|
131
|
+
contents: `# RadioTools.Sdk (.NET)
|
|
132
|
+
|
|
133
|
+
Generated listener REST stubs. Regenerate with \`bun run --filter @rivi-software/radiotools-sdk-generator generate --target csharp\`.
|
|
134
|
+
`,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { listenerSdkManifest } from "@rivi-software/radiotools-contract";
|
|
2
|
+
function renderGoClient() {
|
|
3
|
+
const manifest = listenerSdkManifest;
|
|
4
|
+
return `// Generated by @rivi-software/radiotools-sdk-generator — do not edit by hand.
|
|
5
|
+
package radiotools
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"bytes"
|
|
9
|
+
"context"
|
|
10
|
+
"encoding/json"
|
|
11
|
+
"fmt"
|
|
12
|
+
"io"
|
|
13
|
+
"net/http"
|
|
14
|
+
"net/url"
|
|
15
|
+
"os"
|
|
16
|
+
"strings"
|
|
17
|
+
"time"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
type StationClient struct {
|
|
21
|
+
BaseURL string
|
|
22
|
+
StationSlug string
|
|
23
|
+
APIToken string
|
|
24
|
+
IngestToken string
|
|
25
|
+
HTTPClient *http.Client
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func NewStationClient(baseURL, stationSlug, apiToken, ingestToken string) (*StationClient, error) {
|
|
29
|
+
if apiToken == "" && ingestToken == "" {
|
|
30
|
+
return nil, fmt.Errorf("apiToken or ingestToken is required")
|
|
31
|
+
}
|
|
32
|
+
return &StationClient{
|
|
33
|
+
BaseURL: strings.TrimRight(baseURL, "/"),
|
|
34
|
+
StationSlug: stationSlug,
|
|
35
|
+
APIToken: apiToken,
|
|
36
|
+
IngestToken: ingestToken,
|
|
37
|
+
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
|
38
|
+
}, nil
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func StationClientFromEnv() (*StationClient, error) {
|
|
42
|
+
baseURL := strings.TrimSpace(os.Getenv("RADIOTOOLS_BASE_URL"))
|
|
43
|
+
slug := strings.TrimSpace(os.Getenv("RADIOTOOLS_STATION_SLUG"))
|
|
44
|
+
apiToken := strings.TrimSpace(os.Getenv("RADIOTOOLS_API_TOKEN"))
|
|
45
|
+
ingestToken := strings.TrimSpace(os.Getenv("RADIOTOOLS_INGEST_TOKEN"))
|
|
46
|
+
if baseURL == "" || slug == "" {
|
|
47
|
+
return nil, fmt.Errorf("set RADIOTOOLS_BASE_URL and RADIOTOOLS_STATION_SLUG")
|
|
48
|
+
}
|
|
49
|
+
return NewStationClient(baseURL, slug, apiToken, ingestToken)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func (c *StationClient) getJSON(ctx context.Context, path string, ingest bool) (map[string]any, error) {
|
|
53
|
+
target := path
|
|
54
|
+
if !strings.HasPrefix(path, "http") {
|
|
55
|
+
target = c.BaseURL + path
|
|
56
|
+
}
|
|
57
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
|
58
|
+
if err != nil {
|
|
59
|
+
return nil, err
|
|
60
|
+
}
|
|
61
|
+
token := c.APIToken
|
|
62
|
+
if ingest {
|
|
63
|
+
token = c.IngestToken
|
|
64
|
+
}
|
|
65
|
+
if token == "" {
|
|
66
|
+
return nil, fmt.Errorf("missing bearer token")
|
|
67
|
+
}
|
|
68
|
+
req.Header.Set("Authorization", "Bearer "+token)
|
|
69
|
+
res, err := c.HTTPClient.Do(req)
|
|
70
|
+
if err != nil {
|
|
71
|
+
return nil, err
|
|
72
|
+
}
|
|
73
|
+
defer res.Body.Close()
|
|
74
|
+
if res.StatusCode >= 400 {
|
|
75
|
+
body, _ := io.ReadAll(res.Body)
|
|
76
|
+
return nil, fmt.Errorf("GET %s: %s (%s)", target, res.Status, strings.TrimSpace(string(body)))
|
|
77
|
+
}
|
|
78
|
+
var payload map[string]any
|
|
79
|
+
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
|
80
|
+
return nil, err
|
|
81
|
+
}
|
|
82
|
+
return payload, nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func (c *StationClient) GetStation(ctx context.Context) (map[string]any, error) {
|
|
86
|
+
path := fmt.Sprintf("/api/v1/stations/%s", url.PathEscape(c.StationSlug))
|
|
87
|
+
return c.getJSON(ctx, path, false)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func (c *StationClient) GetWeather(ctx context.Context) (map[string]any, error) {
|
|
91
|
+
station, err := c.GetStation(ctx)
|
|
92
|
+
if err != nil {
|
|
93
|
+
return nil, err
|
|
94
|
+
}
|
|
95
|
+
resources, _ := station["resources"].(map[string]any)
|
|
96
|
+
if resources == nil {
|
|
97
|
+
return nil, nil
|
|
98
|
+
}
|
|
99
|
+
href, _ := resources["weather"].(string)
|
|
100
|
+
if href == "" {
|
|
101
|
+
return nil, nil
|
|
102
|
+
}
|
|
103
|
+
return c.getJSON(ctx, href, false)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func (c *StationClient) GetTraffic(ctx context.Context) (map[string]any, error) {
|
|
107
|
+
station, err := c.GetStation(ctx)
|
|
108
|
+
if err != nil {
|
|
109
|
+
return nil, err
|
|
110
|
+
}
|
|
111
|
+
resources, _ := station["resources"].(map[string]any)
|
|
112
|
+
if resources == nil {
|
|
113
|
+
return nil, nil
|
|
114
|
+
}
|
|
115
|
+
href, _ := resources["traffic"].(string)
|
|
116
|
+
if href == "" {
|
|
117
|
+
return nil, nil
|
|
118
|
+
}
|
|
119
|
+
return c.getJSON(ctx, href, false)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (c *StationClient) ReportPlayback(ctx context.Context, observation map[string]any, stationID string) (map[string]any, error) {
|
|
123
|
+
q := url.Values{}
|
|
124
|
+
if stationID != "" {
|
|
125
|
+
q.Set("stationId", stationID)
|
|
126
|
+
} else {
|
|
127
|
+
q.Set("stationSlug", c.StationSlug)
|
|
128
|
+
}
|
|
129
|
+
target := c.BaseURL + "${manifest.integrations.playback.path}?" + q.Encode()
|
|
130
|
+
body, err := json.Marshal(observation)
|
|
131
|
+
if err != nil {
|
|
132
|
+
return nil, err
|
|
133
|
+
}
|
|
134
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewReader(body))
|
|
135
|
+
if err != nil {
|
|
136
|
+
return nil, err
|
|
137
|
+
}
|
|
138
|
+
if c.IngestToken == "" {
|
|
139
|
+
return nil, fmt.Errorf("ingest token required")
|
|
140
|
+
}
|
|
141
|
+
req.Header.Set("Authorization", "Bearer "+c.IngestToken)
|
|
142
|
+
req.Header.Set("Content-Type", "application/json")
|
|
143
|
+
res, err := c.HTTPClient.Do(req)
|
|
144
|
+
if err != nil {
|
|
145
|
+
return nil, err
|
|
146
|
+
}
|
|
147
|
+
defer res.Body.Close()
|
|
148
|
+
if res.StatusCode >= 400 {
|
|
149
|
+
raw, _ := io.ReadAll(res.Body)
|
|
150
|
+
return nil, fmt.Errorf("POST playback: %s (%s)", res.Status, strings.TrimSpace(string(raw)))
|
|
151
|
+
}
|
|
152
|
+
var payload map[string]any
|
|
153
|
+
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
|
154
|
+
return nil, err
|
|
155
|
+
}
|
|
156
|
+
return payload, nil
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
export const goTarget = {
|
|
161
|
+
id: "go",
|
|
162
|
+
description: "Go net/http client stubs (packages/sdk-go)",
|
|
163
|
+
outputDir: (repoRoot) => `${repoRoot}/packages/sdk-go/radiotools`,
|
|
164
|
+
generate: () => [
|
|
165
|
+
{ path: "client.go", contents: renderGoClient() },
|
|
166
|
+
{
|
|
167
|
+
path: "../go.mod",
|
|
168
|
+
contents: `module github.com/radiotools/radiotools-sdk-go
|
|
169
|
+
|
|
170
|
+
go 1.22
|
|
171
|
+
`,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
path: "../README.md",
|
|
175
|
+
contents: `# radiotools-sdk-go
|
|
176
|
+
|
|
177
|
+
Generated Go client stubs for RadioTools listener contract v${listenerSdkManifest.contractVersion}.
|
|
178
|
+
|
|
179
|
+
Regenerate: \`bun run --filter @rivi-software/radiotools-sdk-generator generate --target go\`
|
|
180
|
+
|
|
181
|
+
For GraphQL, WebSocket, and ETag behavior use \`@rivi-software/radiotools-sdk\` (TypeScript).
|
|
182
|
+
`,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|