@lovrozagar/honey 0.1.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/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- package/src/yaml.ts +80 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/* Rust type emitter — IR-based printer with raw-schema shims for backward compatibility. */
|
|
2
|
+
|
|
3
|
+
import { schemaToIR } from "./codegen-ir.ts"
|
|
4
|
+
import type { IRSchema } from "./codegen-ir.ts"
|
|
5
|
+
import { goPascal } from "./go-type-emitter.ts"
|
|
6
|
+
|
|
7
|
+
export const RUST_KEYWORDS = new Set([
|
|
8
|
+
"as", "break", "const", "continue", "crate", "else", "enum", "extern",
|
|
9
|
+
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
|
|
10
|
+
"move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
|
|
11
|
+
"super", "trait", "true", "type", "unsafe", "use", "where", "while",
|
|
12
|
+
"async", "await", "dyn",
|
|
13
|
+
/* reserved / future */
|
|
14
|
+
"abstract", "become", "box", "do", "final", "macro", "override", "priv",
|
|
15
|
+
"typeof", "unsized", "virtual", "yield", "try",
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
export function rustPascal(name: string): string {
|
|
19
|
+
return goPascal(name)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function rustSnake(name: string): string {
|
|
23
|
+
return name
|
|
24
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
25
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
26
|
+
.replace(/[-\s]+/g, "_")
|
|
27
|
+
.replace(/[^a-zA-Z0-9_]/g, "_")
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Returns a safe Rust field identifier (snake_case; reserved keywords get `_` suffix). */
|
|
32
|
+
export function rustIdent(name: string): string {
|
|
33
|
+
const snake = rustSnake(name)
|
|
34
|
+
if (RUST_KEYWORDS.has(snake)) return `${snake}_`
|
|
35
|
+
return snake
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Returns a safe Rust method identifier (snake_case; reserved keywords get `r#` raw-ident prefix). */
|
|
39
|
+
export function rustMethodIdent(name: string): string {
|
|
40
|
+
const snake = rustSnake(name)
|
|
41
|
+
/* raw identifiers are the only valid escape for fn names — `_` suffix changes the API */
|
|
42
|
+
if (RUST_KEYWORDS.has(snake)) return `r#${snake}`
|
|
43
|
+
return snake
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type RustRenderUseCtx = {
|
|
47
|
+
parentName: string
|
|
48
|
+
fieldName: string
|
|
49
|
+
decls: Map<string, string>
|
|
50
|
+
circularRefs?: Set<string>
|
|
51
|
+
depth?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderHoistedStringEnum(typeName: string, enumVals: unknown[]): string {
|
|
55
|
+
const l: string[] = []
|
|
56
|
+
l.push(`#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]`)
|
|
57
|
+
l.push(`pub enum ${typeName} {`)
|
|
58
|
+
for (const raw of enumVals.map(String)) {
|
|
59
|
+
const variant = rustPascal(raw)
|
|
60
|
+
if (raw !== variant) {
|
|
61
|
+
l.push(`\t#[serde(rename = ${JSON.stringify(raw)})]`)
|
|
62
|
+
}
|
|
63
|
+
l.push(`\t${variant},`)
|
|
64
|
+
}
|
|
65
|
+
l.push(`}`)
|
|
66
|
+
/* Separate impl block so the derive list stays unchanged — derive(Default) would require
|
|
67
|
+
* #[default] on a variant, which breaks callers that assert the exact derive substring. */
|
|
68
|
+
if (enumVals.length > 0) {
|
|
69
|
+
const firstVariant = rustPascal(String(enumVals[0]))
|
|
70
|
+
l.push(``)
|
|
71
|
+
l.push(`impl Default for ${typeName} {`)
|
|
72
|
+
l.push(`\tfn default() -> Self {`)
|
|
73
|
+
l.push(`\t\tSelf::${firstVariant}`)
|
|
74
|
+
l.push(`\t}`)
|
|
75
|
+
l.push(`}`)
|
|
76
|
+
}
|
|
77
|
+
return l.join("\n")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderHoistedIntEnum(typeName: string, enumVals: unknown[]): string {
|
|
81
|
+
const l: string[] = []
|
|
82
|
+
l.push(`#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]`)
|
|
83
|
+
l.push(`#[repr(i64)]`)
|
|
84
|
+
l.push(`pub enum ${typeName} {`)
|
|
85
|
+
for (const v of enumVals) {
|
|
86
|
+
const n = Number(v)
|
|
87
|
+
const label = n >= 0 ? `L${n}` : `Neg${Math.abs(n)}`
|
|
88
|
+
l.push(`\t${label} = ${n},`)
|
|
89
|
+
}
|
|
90
|
+
l.push(`}`)
|
|
91
|
+
return l.join("\n")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hoistEnumName(parentName: string, fieldName: string): string {
|
|
95
|
+
return `${rustPascal(parentName)}${rustPascal(fieldName)}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function primitiveFor(t: string | undefined): string | null {
|
|
99
|
+
switch (t) {
|
|
100
|
+
case "string": return "String"
|
|
101
|
+
case "integer": return "i64"
|
|
102
|
+
case "number": return "f64"
|
|
103
|
+
case "boolean": return "bool"
|
|
104
|
+
default: return null
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function constBaseType(val: unknown): string {
|
|
109
|
+
if (typeof val === "boolean") return "bool"
|
|
110
|
+
if (typeof val === "number") return Number.isInteger(val) ? "i64" : "f64"
|
|
111
|
+
return "String"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Render an IR schema as a Rust type expression for a struct field position.
|
|
116
|
+
* Mutates `ctx.decls` when hoisting enums or anonymous structs.
|
|
117
|
+
*/
|
|
118
|
+
export function irRenderUseRust(ir: IRSchema, ctx: RustRenderUseCtx, depth = 0): string {
|
|
119
|
+
if (depth > 12) return "serde_json::Value"
|
|
120
|
+
|
|
121
|
+
switch (ir.kind) {
|
|
122
|
+
case "ref": {
|
|
123
|
+
const rustName = rustPascal(ir.name)
|
|
124
|
+
if (ctx.circularRefs?.has(rustName)) return `Box<${rustName}>`
|
|
125
|
+
return rustName
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "allOf": {
|
|
129
|
+
const firstRef = ir.parts.find((p) => p.kind === "ref")
|
|
130
|
+
if (firstRef && firstRef.kind === "ref") return rustPascal(firstRef.name)
|
|
131
|
+
return "serde_json::Value"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "nullable": {
|
|
135
|
+
const innerStr = irRenderUseRust(ir.inner, ctx, depth + 1)
|
|
136
|
+
/* inner is already serde_json::Value — don't wrap in Option */
|
|
137
|
+
if (innerStr === "serde_json::Value") return "serde_json::Value"
|
|
138
|
+
/* idempotent: don't double-wrap */
|
|
139
|
+
if (innerStr.startsWith("Option<")) return innerStr
|
|
140
|
+
return `Option<${innerStr}>`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case "union": {
|
|
144
|
+
/* union at use-position → always serde_json::Value */
|
|
145
|
+
return "serde_json::Value"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case "scalar": {
|
|
149
|
+
if (ir.enum) {
|
|
150
|
+
const name = hoistEnumName(ctx.parentName, ctx.fieldName)
|
|
151
|
+
if (!ctx.decls.has(name)) {
|
|
152
|
+
if (ir.type === "string") {
|
|
153
|
+
ctx.decls.set(name, renderHoistedStringEnum(name, ir.enum as unknown[]))
|
|
154
|
+
} else {
|
|
155
|
+
ctx.decls.set(name, renderHoistedIntEnum(name, ir.enum as unknown[]))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return name
|
|
159
|
+
}
|
|
160
|
+
const prim = primitiveFor(ir.type)
|
|
161
|
+
if (prim) return prim
|
|
162
|
+
return "serde_json::Value"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case "const": {
|
|
166
|
+
return constBaseType(ir.value)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case "array": {
|
|
170
|
+
const el = irRenderUseRust(ir.items, ctx, depth + 1)
|
|
171
|
+
return `Vec<${el}>`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case "tuple": {
|
|
175
|
+
/* Rust emitter ignores tuple item types — always Vec<serde_json::Value> */
|
|
176
|
+
return "Vec<serde_json::Value>"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case "object": {
|
|
180
|
+
const { fields, additional } = ir
|
|
181
|
+
|
|
182
|
+
if (fields.length === 0) {
|
|
183
|
+
if (additional !== undefined && additional !== false) {
|
|
184
|
+
const valType = irRenderUseRust(additional, ctx, depth + 1)
|
|
185
|
+
return `HashMap<String, ${valType}>`
|
|
186
|
+
}
|
|
187
|
+
if (additional === false) return "serde_json::Value"
|
|
188
|
+
return "HashMap<String, serde_json::Value>"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const hoistedName = hoistEnumName(ctx.parentName, ctx.fieldName)
|
|
192
|
+
if (!ctx.decls.has(hoistedName)) {
|
|
193
|
+
/* placeholder prevents infinite recursion on self-referential schemas */
|
|
194
|
+
ctx.decls.set(hoistedName, "")
|
|
195
|
+
const hoisted = irRenderTopLevelRust(hoistedName, ir, ctx.decls)
|
|
196
|
+
ctx.decls.set(hoistedName, hoisted)
|
|
197
|
+
}
|
|
198
|
+
return hoistedName
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case "binary": {
|
|
202
|
+
return "String"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case "unknown": {
|
|
206
|
+
return "serde_json::Value"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* `raw` 4th arg is for the const-with-declared-type branch only: IR `const` kind
|
|
213
|
+
* strips the type field; `raw.type` provides the override.
|
|
214
|
+
*/
|
|
215
|
+
export function irRenderTopLevelRust(
|
|
216
|
+
name: string,
|
|
217
|
+
ir: IRSchema,
|
|
218
|
+
decls: Map<string, string>,
|
|
219
|
+
raw?: Record<string, unknown>,
|
|
220
|
+
): string {
|
|
221
|
+
const typeName = rustPascal(name)
|
|
222
|
+
const circularRefs = new Set([typeName])
|
|
223
|
+
|
|
224
|
+
if (ir.kind === "scalar" && ir.enum) {
|
|
225
|
+
if (ir.type === "string") {
|
|
226
|
+
return renderHoistedStringEnum(typeName, ir.enum as unknown[])
|
|
227
|
+
}
|
|
228
|
+
return renderHoistedIntEnum(typeName, ir.enum as unknown[])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (ir.kind === "const") {
|
|
232
|
+
const rawType = raw?.type as string | undefined
|
|
233
|
+
const base = rawType ? (primitiveFor(rawType) ?? "String") : constBaseType(ir.value)
|
|
234
|
+
return [
|
|
235
|
+
`#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]`,
|
|
236
|
+
`pub struct ${typeName}(pub ${base});`,
|
|
237
|
+
].join("\n")
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (ir.kind === "allOf") {
|
|
241
|
+
const l: string[] = []
|
|
242
|
+
l.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
|
|
243
|
+
l.push(`pub struct ${typeName} {`)
|
|
244
|
+
for (const part of ir.parts) {
|
|
245
|
+
if (part.kind === "ref") {
|
|
246
|
+
const refName = rustPascal(part.name)
|
|
247
|
+
l.push(`\t#[serde(flatten)]`)
|
|
248
|
+
l.push(`\tpub ${rustIdent(refName.toLowerCase())}: ${refName},`)
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if (part.kind !== "object") continue
|
|
252
|
+
const sorted = part.fields.slice().sort((a, b) => a.name.localeCompare(b.name))
|
|
253
|
+
for (const field of sorted) {
|
|
254
|
+
const fieldIdent = rustIdent(field.name)
|
|
255
|
+
const ft = irRenderUseRust(field.schema, {
|
|
256
|
+
circularRefs,
|
|
257
|
+
decls,
|
|
258
|
+
depth: 1,
|
|
259
|
+
fieldName: field.name,
|
|
260
|
+
parentName: typeName,
|
|
261
|
+
})
|
|
262
|
+
const finalType = !field.required && !ft.startsWith("Option<") ? `Option<${ft}>` : ft
|
|
263
|
+
const needsRename = fieldIdent !== field.name
|
|
264
|
+
const needsSkip = !field.required
|
|
265
|
+
if (needsRename) {
|
|
266
|
+
l.push(`\t#[serde(rename = ${JSON.stringify(field.name)})]`)
|
|
267
|
+
}
|
|
268
|
+
if (needsSkip) {
|
|
269
|
+
l.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
|
|
270
|
+
}
|
|
271
|
+
l.push(`\tpub ${fieldIdent}: ${finalType},`)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
l.push(`}`)
|
|
275
|
+
return l.join("\n")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (ir.kind === "union" && ir.discriminator) {
|
|
279
|
+
const discProp = ir.discriminator.propertyName
|
|
280
|
+
const l: string[] = []
|
|
281
|
+
l.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
|
|
282
|
+
l.push(`#[serde(tag = ${JSON.stringify(discProp)})]`)
|
|
283
|
+
l.push(`pub enum ${typeName} {`)
|
|
284
|
+
for (const v of ir.variants) {
|
|
285
|
+
if (v.kind === "ref") {
|
|
286
|
+
const refName = rustPascal(v.name)
|
|
287
|
+
l.push(`\t${refName}(${refName}),`)
|
|
288
|
+
} else {
|
|
289
|
+
l.push(`\tVariant(serde_json::Value),`)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
l.push(`}`)
|
|
293
|
+
return l.join("\n")
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (ir.kind === "union") {
|
|
297
|
+
const l: string[] = []
|
|
298
|
+
l.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
|
|
299
|
+
l.push(`#[serde(untagged)]`)
|
|
300
|
+
l.push(`pub enum ${typeName} {`)
|
|
301
|
+
for (let i = 0; i < ir.variants.length; i++) {
|
|
302
|
+
const v = ir.variants[i]
|
|
303
|
+
if (v.kind === "ref") {
|
|
304
|
+
const refName = rustPascal(v.name)
|
|
305
|
+
l.push(`\tVariant${i}(${refName}),`)
|
|
306
|
+
} else {
|
|
307
|
+
const inner = irRenderUseRust(v, {
|
|
308
|
+
circularRefs,
|
|
309
|
+
decls,
|
|
310
|
+
depth: 1,
|
|
311
|
+
fieldName: `variant${i}`,
|
|
312
|
+
parentName: typeName,
|
|
313
|
+
})
|
|
314
|
+
const finalType = inner === "serde_json::Value" ? "serde_json::Value" : inner
|
|
315
|
+
l.push(`\tVariant${i}(${finalType}),`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
l.push(`}`)
|
|
319
|
+
return l.join("\n")
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (ir.kind === "object") {
|
|
323
|
+
const { fields, additional } = ir
|
|
324
|
+
|
|
325
|
+
if (fields.length === 0 && additional !== undefined && additional !== false) {
|
|
326
|
+
const valType = irRenderUseRust(additional, {
|
|
327
|
+
circularRefs,
|
|
328
|
+
decls,
|
|
329
|
+
fieldName: "Value",
|
|
330
|
+
parentName: typeName,
|
|
331
|
+
})
|
|
332
|
+
return `pub type ${typeName} = HashMap<String, ${valType}>;`
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (fields.length === 0) {
|
|
336
|
+
if (additional === false) return `#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ${typeName};`
|
|
337
|
+
return `pub type ${typeName} = HashMap<String, serde_json::Value>;`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const sorted = fields.slice().sort((a, b) => a.name.localeCompare(b.name))
|
|
341
|
+
const l: string[] = []
|
|
342
|
+
l.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
|
|
343
|
+
l.push(`pub struct ${typeName} {`)
|
|
344
|
+
for (const field of sorted) {
|
|
345
|
+
const fieldIdent = rustIdent(field.name)
|
|
346
|
+
const ft = irRenderUseRust(field.schema, {
|
|
347
|
+
circularRefs,
|
|
348
|
+
decls,
|
|
349
|
+
depth: 1,
|
|
350
|
+
fieldName: field.name,
|
|
351
|
+
parentName: typeName,
|
|
352
|
+
})
|
|
353
|
+
let finalType: string
|
|
354
|
+
if (field.required) {
|
|
355
|
+
finalType = circularRefs.has(ft) && !ft.startsWith("Box<") ? `Box<${ft}>` : ft
|
|
356
|
+
} else {
|
|
357
|
+
finalType = ft.startsWith("Option<") ? ft : `Option<${ft}>`
|
|
358
|
+
}
|
|
359
|
+
const needsRename = fieldIdent !== field.name
|
|
360
|
+
const needsSkip = !field.required
|
|
361
|
+
if (needsRename) {
|
|
362
|
+
l.push(`\t#[serde(rename = ${JSON.stringify(field.name)})]`)
|
|
363
|
+
}
|
|
364
|
+
if (needsSkip) {
|
|
365
|
+
l.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
|
|
366
|
+
}
|
|
367
|
+
l.push(`\tpub ${fieldIdent}: ${finalType},`)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (additional) {
|
|
371
|
+
const valType = irRenderUseRust(additional, {
|
|
372
|
+
circularRefs,
|
|
373
|
+
decls,
|
|
374
|
+
depth: 1,
|
|
375
|
+
fieldName: "Extra",
|
|
376
|
+
parentName: typeName,
|
|
377
|
+
})
|
|
378
|
+
l.push(`\t#[serde(flatten)]`)
|
|
379
|
+
l.push(`\tpub extra: HashMap<String, ${valType}>,`)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
l.push(`}`)
|
|
383
|
+
return l.join("\n")
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const aliased = irRenderUseRust(ir, {
|
|
387
|
+
circularRefs,
|
|
388
|
+
decls,
|
|
389
|
+
fieldName: "Value",
|
|
390
|
+
parentName: typeName,
|
|
391
|
+
})
|
|
392
|
+
return `pub type ${typeName} = ${aliased};`
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Render a schema as a Rust type expression for a struct field position.
|
|
397
|
+
* Mutates `ctx.decls` when hoisting enums or anonymous structs.
|
|
398
|
+
*/
|
|
399
|
+
export function renderUseRust(
|
|
400
|
+
schema: Record<string, unknown> | undefined,
|
|
401
|
+
ctx: RustRenderUseCtx,
|
|
402
|
+
): string {
|
|
403
|
+
return irRenderUseRust(schemaToIR(schema), ctx, ctx.depth ?? 0)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function renderTopLevelRust(
|
|
407
|
+
name: string,
|
|
408
|
+
schema: Record<string, unknown>,
|
|
409
|
+
decls: Map<string, string>,
|
|
410
|
+
): string {
|
|
411
|
+
return irRenderTopLevelRust(name, schemaToIR(schema), decls, schema)
|
|
412
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { MiddlewareFn } from "./middleware.ts"
|
|
2
|
+
|
|
3
|
+
type SecureHeadersOptions = {
|
|
4
|
+
contentSecurityPolicy?: string
|
|
5
|
+
crossOriginEmbedderPolicy?: string
|
|
6
|
+
crossOriginOpenerPolicy?: string
|
|
7
|
+
crossOriginResourcePolicy?: string
|
|
8
|
+
permissionsPolicy?: string
|
|
9
|
+
referrerPolicy?: false | string
|
|
10
|
+
strictTransportSecurity?: string
|
|
11
|
+
xContentTypeOptions?: false | string
|
|
12
|
+
xFrameOptions?: false | string
|
|
13
|
+
xXssProtection?: false | string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function secureHeaders(options?: SecureHeadersOptions): MiddlewareFn<{}, {}> {
|
|
17
|
+
const opts = options ?? {}
|
|
18
|
+
|
|
19
|
+
const headers: Array<[string, string]> = []
|
|
20
|
+
|
|
21
|
+
const xContentType = opts.xContentTypeOptions
|
|
22
|
+
if (xContentType !== false) {
|
|
23
|
+
headers.push(["x-content-type-options", xContentType ?? "nosniff"])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const xFrame = opts.xFrameOptions
|
|
27
|
+
if (xFrame !== false) {
|
|
28
|
+
headers.push(["x-frame-options", xFrame ?? "SAMEORIGIN"])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const referrer = opts.referrerPolicy
|
|
32
|
+
if (referrer !== false) {
|
|
33
|
+
headers.push(["referrer-policy", referrer ?? "strict-origin-when-cross-origin"])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const xss = opts.xXssProtection
|
|
37
|
+
if (xss !== false) {
|
|
38
|
+
headers.push(["x-xss-protection", xss ?? "0"])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (opts.contentSecurityPolicy) {
|
|
42
|
+
headers.push(["content-security-policy", opts.contentSecurityPolicy])
|
|
43
|
+
}
|
|
44
|
+
if (opts.strictTransportSecurity) {
|
|
45
|
+
headers.push(["strict-transport-security", opts.strictTransportSecurity])
|
|
46
|
+
}
|
|
47
|
+
if (opts.permissionsPolicy) {
|
|
48
|
+
headers.push(["permissions-policy", opts.permissionsPolicy])
|
|
49
|
+
}
|
|
50
|
+
if (opts.crossOriginOpenerPolicy) {
|
|
51
|
+
headers.push(["cross-origin-opener-policy", opts.crossOriginOpenerPolicy])
|
|
52
|
+
}
|
|
53
|
+
if (opts.crossOriginResourcePolicy) {
|
|
54
|
+
headers.push(["cross-origin-resource-policy", opts.crossOriginResourcePolicy])
|
|
55
|
+
}
|
|
56
|
+
if (opts.crossOriginEmbedderPolicy) {
|
|
57
|
+
headers.push(["cross-origin-embedder-policy", opts.crossOriginEmbedderPolicy])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const mw: MiddlewareFn<{}, {}> = async (_ctx, next) => {
|
|
61
|
+
const response = await next()
|
|
62
|
+
for (const [name, value] of headers) {
|
|
63
|
+
response.headers.set(name, value)
|
|
64
|
+
}
|
|
65
|
+
return response
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return mw
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HoneyServeOptions, ServeHandle } from "./serve.ts"
|
|
2
|
+
|
|
3
|
+
export type ServeStart = (app: unknown, options?: HoneyServeOptions) => Promise<ServeHandle>
|
|
4
|
+
|
|
5
|
+
const MISSING = 'Honey.serve() requires `import "@lovrozagar/honey/serve"` in the app entry.'
|
|
6
|
+
|
|
7
|
+
let runtime: ServeStart | undefined
|
|
8
|
+
|
|
9
|
+
export function registerServeRuntime(next: ServeStart): void {
|
|
10
|
+
runtime = next
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resetServeRuntime(): void {
|
|
14
|
+
runtime = undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getServeRuntime(): ServeStart {
|
|
18
|
+
if (!runtime) throw new Error(MISSING)
|
|
19
|
+
return runtime
|
|
20
|
+
}
|
package/src/serve.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Honey } from "./index.ts"
|
|
2
|
+
import { cors, type CORSOptions } from "./cors.ts"
|
|
3
|
+
import { detectRuntime, type ServeRuntime } from "./detect-runtime.ts"
|
|
4
|
+
|
|
5
|
+
export type { ServeRuntime }
|
|
6
|
+
export { detectRuntime }
|
|
7
|
+
|
|
8
|
+
export type ServeHandle = {
|
|
9
|
+
close(): Promise<void>
|
|
10
|
+
hostname: string
|
|
11
|
+
port: number
|
|
12
|
+
runtime: Exclude<ServeRuntime, "cloudflare">
|
|
13
|
+
url: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type HoneyServeOptions = {
|
|
17
|
+
cors?: boolean | CORSOptions
|
|
18
|
+
env?: Record<string, unknown>
|
|
19
|
+
hostname?: string
|
|
20
|
+
port?: number
|
|
21
|
+
runtime?: ServeRuntime
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CF_SERVE_ERROR =
|
|
25
|
+
"Honey.serve() cannot run on Cloudflare Workers. Export fetch:\n\n" +
|
|
26
|
+
"export default {\n" +
|
|
27
|
+
" fetch: (req, env, ctx) => app.fetch(req, env, ctx),\n" +
|
|
28
|
+
"}\n"
|
|
29
|
+
|
|
30
|
+
function publicHost(hostname: string): string {
|
|
31
|
+
return hostname === "0.0.0.0" || hostname === "::" ? "127.0.0.1" : hostname
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function startHoneyServer(
|
|
35
|
+
app: Honey<Record<string, unknown>>,
|
|
36
|
+
options: HoneyServeOptions = {},
|
|
37
|
+
): Promise<ServeHandle> {
|
|
38
|
+
const runtime = options.runtime ?? detectRuntime()
|
|
39
|
+
if (runtime === "cloudflare") {
|
|
40
|
+
throw new Error(CF_SERVE_ERROR)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let listening: Honey<Record<string, unknown>> = app
|
|
44
|
+
if (options.cors) {
|
|
45
|
+
const corsOpts = options.cors === true ? undefined : options.cors
|
|
46
|
+
listening = app.use(cors(corsOpts)) as Honey<Record<string, unknown>>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hostname = options.hostname ?? (runtime === "deno" ? "127.0.0.1" : "0.0.0.0")
|
|
50
|
+
const port = options.port ?? 3000
|
|
51
|
+
const env = (options.env ?? {}) as Record<string, unknown>
|
|
52
|
+
|
|
53
|
+
if (runtime === "bun") {
|
|
54
|
+
const { bunWebSocket } = await import("./ws/bun.ts")
|
|
55
|
+
const bunWs = bunWebSocket()
|
|
56
|
+
listening.wsAdapter(bunWs)
|
|
57
|
+
const BunNs = (globalThis as unknown as { Bun: { serve: (opts: Record<string, unknown>) => BunServer } }).Bun
|
|
58
|
+
const server = BunNs.serve({
|
|
59
|
+
fetch: (req: Request, server: unknown) => listening.fetch(req, { ...env, server }),
|
|
60
|
+
hostname,
|
|
61
|
+
port,
|
|
62
|
+
websocket: bunWs.websocket,
|
|
63
|
+
})
|
|
64
|
+
const bound = server.port
|
|
65
|
+
return {
|
|
66
|
+
async close() {
|
|
67
|
+
await Promise.resolve(server.stop(true))
|
|
68
|
+
},
|
|
69
|
+
hostname,
|
|
70
|
+
port: bound,
|
|
71
|
+
runtime,
|
|
72
|
+
url: `http://${publicHost(hostname)}:${bound}`,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (runtime === "deno") {
|
|
77
|
+
const { denoWebSocket } = await import("./ws/deno.ts")
|
|
78
|
+
listening.wsAdapter(denoWebSocket())
|
|
79
|
+
const DenoNs = (
|
|
80
|
+
globalThis as unknown as {
|
|
81
|
+
Deno: {
|
|
82
|
+
serve: (
|
|
83
|
+
opts: { hostname: string; port: number; signal?: AbortSignal },
|
|
84
|
+
handler: (req: Request) => Response | Promise<Response>,
|
|
85
|
+
) => { addr?: { port?: number }; finished?: Promise<void>; shutdown?: () => Promise<void> }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
).Deno
|
|
89
|
+
const ac = new AbortController()
|
|
90
|
+
const server = DenoNs.serve(
|
|
91
|
+
{ hostname, port, signal: ac.signal },
|
|
92
|
+
(req) => listening.fetch(req, env),
|
|
93
|
+
)
|
|
94
|
+
const bound = server.addr?.port ?? port
|
|
95
|
+
return {
|
|
96
|
+
async close() {
|
|
97
|
+
ac.abort()
|
|
98
|
+
if (server.shutdown) {
|
|
99
|
+
await Promise.race([
|
|
100
|
+
server.shutdown(),
|
|
101
|
+
new Promise<void>((r) => setTimeout(r, 1_000)),
|
|
102
|
+
])
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
await Promise.race([
|
|
106
|
+
server.finished ?? Promise.resolve(),
|
|
107
|
+
new Promise<void>((r) => setTimeout(r, 1_000)),
|
|
108
|
+
])
|
|
109
|
+
},
|
|
110
|
+
hostname,
|
|
111
|
+
port: bound,
|
|
112
|
+
runtime,
|
|
113
|
+
url: `http://${publicHost(hostname)}:${bound}`,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { nodeWebSocket } = await import("./ws/node.ts")
|
|
118
|
+
const { serve } = await import("./node.ts")
|
|
119
|
+
listening.wsAdapter(nodeWebSocket())
|
|
120
|
+
const server = serve(listening as never, { env, hostname, port })
|
|
121
|
+
await new Promise<void>((resolve, reject) => {
|
|
122
|
+
server.once("listening", () => resolve())
|
|
123
|
+
server.once("error", reject)
|
|
124
|
+
})
|
|
125
|
+
const addr = server.address()
|
|
126
|
+
const bound = typeof addr === "object" && addr !== null ? addr.port : port
|
|
127
|
+
return {
|
|
128
|
+
async close() {
|
|
129
|
+
await server.shutdown(1_000)
|
|
130
|
+
},
|
|
131
|
+
hostname,
|
|
132
|
+
port: bound,
|
|
133
|
+
runtime: "node",
|
|
134
|
+
url: `http://${publicHost(hostname)}:${bound}`,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type BunServer = {
|
|
139
|
+
port: number
|
|
140
|
+
stop(closeActiveConnections?: boolean): void | Promise<void>
|
|
141
|
+
}
|