@weft-js/start 0.1.0 → 0.1.2
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 +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +51 -10
- package/src/asset-receipt.d.mts +13 -0
- package/src/asset-receipt.mjs +61 -0
- package/src/auth/better-auth.ts +215 -17
- package/src/auth/drizzle.ts +1 -1
- package/src/auth.ts +21 -6
- package/src/boot.ts +19 -8
- package/src/bus/postgres.ts +32 -19
- package/src/bus.ts +7 -4
- package/src/{capture.mjs → capture.ts} +34 -1
- package/src/changes-sql.d.mts +8 -2
- package/src/changes-sql.mjs +26 -10
- package/src/changes.ts +165 -66
- package/src/channel/connections.ts +216 -0
- package/src/channel/socket.ts +396 -0
- package/src/channel-fake.ts +19 -4
- package/src/channel.ts +124 -490
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +146 -155
- package/src/data/cache.ts +354 -0
- package/src/data/definitions.ts +94 -0
- package/src/data/draft.ts +104 -0
- package/src/data/foundation.ts +95 -0
- package/src/data/live-api.ts +22 -0
- package/src/data/mutation.ts +138 -0
- package/src/data/primitives.ts +135 -0
- package/src/data/query.ts +106 -0
- package/src/data/transport.ts +121 -0
- package/src/data/types.ts +303 -0
- package/src/data.ts +69 -894
- package/src/db/bun-sqlite.d.ts +4 -1
- package/src/db/changes-mysql.ts +61 -21
- package/src/db/changes-postgres.ts +197 -118
- package/src/db/changes-sqlite.ts +131 -64
- package/src/db/core.ts +38 -13
- package/src/db/index.ts +20 -4
- package/src/db/mysql.ts +22 -6
- package/src/db/postgres.ts +49 -13
- package/src/db/sqlite.ts +53 -27
- package/src/derive.d.mts +4 -1
- package/src/derive.mjs +136 -39
- package/src/dev-runtime.mjs +8 -4
- package/src/exec/errors.ts +57 -0
- package/src/exec/landing.ts +66 -0
- package/src/exec/scope.ts +174 -0
- package/src/exec/touch.ts +80 -0
- package/src/exec/validation.ts +176 -0
- package/src/exec.ts +191 -341
- package/src/extension.ts +8 -2
- package/src/first-paint.ts +26 -9
- package/src/form/value.ts +15 -0
- package/src/form.tsx +132 -43
- package/src/hub/channels.ts +102 -0
- package/src/hub/connections.ts +147 -0
- package/src/hub/delivery.ts +62 -0
- package/src/hub/feed.ts +168 -0
- package/src/hub/presence.ts +64 -0
- package/src/hub/route.ts +158 -0
- package/src/hub/subscriptions.ts +212 -0
- package/src/hub/topic.ts +98 -0
- package/src/hub/tracked.ts +12 -0
- package/src/hub/types.ts +190 -0
- package/src/hub/view.ts +147 -0
- package/src/hub.ts +132 -758
- package/src/index.ts +84 -10
- package/src/inline.mjs +356 -0
- package/src/inspect/compare.mjs +162 -0
- package/src/inspect/routes.mjs +154 -0
- package/src/inspect/samples.mjs +59 -0
- package/src/inspect/url.mjs +2 -0
- package/src/inspect.mjs +146 -187
- package/src/link/election.ts +178 -0
- package/src/link/lines.ts +27 -0
- package/src/link.ts +97 -168
- package/src/list-drizzle.ts +104 -18
- package/src/list.ts +127 -36
- package/src/listen.ts +32 -6
- package/src/live/api.ts +18 -0
- package/src/live/backoff.ts +6 -0
- package/src/live/frames.ts +42 -0
- package/src/live/read.ts +77 -0
- package/src/live/sub-types.ts +24 -0
- package/src/live/transport.ts +86 -0
- package/src/live/types.ts +52 -0
- package/src/live-fake.ts +20 -5
- package/src/live-target.mjs +113 -35
- package/src/live.ts +116 -225
- package/src/media.ts +8 -1
- package/src/migrations.mjs +14 -6
- package/src/mux.ts +161 -64
- package/src/og/DefaultCard.tsx +40 -9
- package/src/og/fonts.mjs +16 -8
- package/src/og/images.mjs +43 -22
- package/src/og/index.mjs +119 -43
- package/src/og/native.mjs +11 -4
- package/src/range.mjs +15 -3
- package/src/router.ts +56 -6
- package/src/routes.mjs +73 -22
- package/src/run/admission.ts +80 -0
- package/src/run/boot.tsx +115 -0
- package/src/run/cache-presentation.ts +67 -0
- package/src/run/cache.ts +254 -0
- package/src/run/helpers.ts +197 -0
- package/src/run/listeners.ts +155 -0
- package/src/run/navigation.ts +382 -0
- package/src/run/page-skeleton.tsx +35 -0
- package/src/run/prefetch.ts +128 -0
- package/src/run/retain-view.ts +18 -0
- package/src/run/shape-preparation.ts +97 -0
- package/src/run/shape-styles.ts +72 -0
- package/src/run/source-context/pin.ts +124 -0
- package/src/run/source-context.ts +311 -0
- package/src/run.tsx +50 -174
- package/src/rust/common.ts +163 -0
- package/src/rust/native.ts +282 -0
- package/src/rust.ts +103 -229
- package/src/server/card.tsx +65 -0
- package/src/server/compression.ts +115 -0
- package/src/server/core.ts +70 -0
- package/src/server/dispatch.ts +70 -0
- package/src/server/form.ts +85 -0
- package/src/server/functions.ts +129 -0
- package/src/server/handler.ts +39 -0
- package/src/server/head.ts +96 -0
- package/src/server/media.ts +60 -0
- package/src/server/navigation-cache.ts +80 -0
- package/src/server/navigation.tsx +85 -0
- package/src/server/page-cold.ts +95 -0
- package/src/server/page-stream.tsx +181 -0
- package/src/server/page-types.ts +51 -0
- package/src/server/page-warm.ts +130 -0
- package/src/server/page.ts +69 -0
- package/src/server/payload.ts +89 -0
- package/src/server/prerender.ts +60 -0
- package/src/server/preview.tsx +67 -0
- package/src/server/render.ts +167 -0
- package/src/server/request.ts +98 -0
- package/src/server/resolver.ts +228 -0
- package/src/server/routes.ts +150 -0
- package/src/server/services.ts +69 -0
- package/src/server/text.ts +29 -0
- package/src/server/types.ts +160 -0
- package/src/server.tsx +82 -964
- package/src/shape.mjs +30 -6
- package/src/share/display.ts +50 -0
- package/src/share/network.ts +119 -0
- package/src/share/receive.ts +70 -0
- package/src/share/state.ts +30 -0
- package/src/share/tab-queue.ts +28 -0
- package/src/share-server.ts +128 -43
- package/src/share.ts +103 -209
- package/src/sheets.mjs +30 -18
- package/src/sidecar/build.mjs +170 -0
- package/src/sidecar/cargo.mjs +164 -0
- package/src/sidecar/common.mjs +17 -0
- package/src/sidecar/dev-build.mjs +91 -0
- package/src/sidecar/dev-watch.mjs +88 -0
- package/src/sidecar/manifest.mjs +19 -0
- package/src/sidecar/process.mjs +58 -0
- package/src/sidecar/refresh.mjs +19 -0
- package/src/sidecar.mjs +75 -416
- package/src/socket/check.ts +50 -0
- package/src/socket/deliver.ts +16 -0
- package/src/socket/publish.ts +56 -0
- package/src/socket/rooms.ts +41 -0
- package/src/socket/route.ts +79 -0
- package/src/socket/shared.ts +26 -0
- package/src/socket/types.ts +48 -0
- package/src/socket.ts +205 -194
- package/src/stream.ts +10 -1
- package/src/verify/app.ts +39 -0
- package/src/verify/cleanup.ts +27 -0
- package/src/verify/database.ts +11 -0
- package/src/verify/live.ts +63 -0
- package/src/verify/notify.ts +35 -0
- package/src/verify/skip.ts +19 -0
- package/src/verify/support.ts +149 -0
- package/src/verify/timing.ts +15 -0
- package/src/verify/trigger.ts +14 -0
- package/src/verify.ts +77 -176
- package/src/view.tsx +96 -11
- package/src/vite/asset-bytes.mjs +95 -0
- package/src/vite/assets.mjs +117 -0
- package/src/vite/config-hooks.mjs +166 -0
- package/src/vite/dev-hooks.mjs +103 -0
- package/src/vite/dev.mjs +102 -0
- package/src/vite/entries.mjs +145 -0
- package/src/vite/module-hooks.mjs +288 -0
- package/src/vite/route-modules.mjs +289 -0
- package/src/vite/server-functions.mjs +222 -0
- package/src/vite/shape-entry-css.mjs +187 -0
- package/src/vite/shape-override.mjs +226 -0
- package/src/vite/shared.mjs +51 -0
- package/src/vite/sheets.mjs +92 -0
- package/src/vite/state.mjs +89 -0
- package/src/vite/syntax.mjs +139 -0
- package/src/vite.mjs +152 -853
- package/src/capture.d.mts +0 -1
package/crate/Cargo.toml
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "weft"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
description = "The Rust side of weft's remote() and native(): generated types from .weft/contract.json, Ctx, the signed envelope, an axum Router, a Node-API and WASM entry"
|
|
7
|
+
|
|
8
|
+
[features]
|
|
9
|
+
default = ["server"]
|
|
10
|
+
# What generated code needs on any target.
|
|
11
|
+
core = ["dep:chrono", "dep:regress", "dep:serde", "dep:serde_json", "dep:uuid"]
|
|
12
|
+
# What a sidecar links.
|
|
13
|
+
server = ["core", "dep:axum", "dep:base64", "dep:hmac", "dep:sha2", "dep:tokio", "dep:tracing", "dep:tracing-subscriber"]
|
|
14
|
+
# What a `native()` crate links, with `default-features = false`: an addon, or a .wasm on that target. No server, no tokio.
|
|
15
|
+
native = ["core", "dep:napi", "dep:napi-derive"]
|
|
16
|
+
# `span.set_parent` from the envelope's traceparent, for an app that installs an OpenTelemetry layer.
|
|
17
|
+
otel = ["server", "dep:opentelemetry", "dep:tracing-opentelemetry"]
|
|
18
|
+
# What a sidecar's build.rs links, with `default-features = false`: nothing of the server.
|
|
19
|
+
build = ["dep:napi-build", "dep:prettyplease", "dep:schemars", "dep:serde_json", "dep:syn", "dep:typify"]
|
|
20
|
+
|
|
21
|
+
[dependencies]
|
|
22
|
+
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "tokio"], optional = true }
|
|
23
|
+
base64 = { version = "0.23", optional = true }
|
|
24
|
+
chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"], optional = true }
|
|
25
|
+
hmac = { version = "0.13", optional = true }
|
|
26
|
+
napi-build = { version = "2.4", optional = true }
|
|
27
|
+
opentelemetry = { version = "0.33", optional = true }
|
|
28
|
+
prettyplease = { version = "0.3", optional = true }
|
|
29
|
+
regress = { version = "0.11", optional = true }
|
|
30
|
+
schemars = { version = "0.8.22", optional = true } # the version typify reads
|
|
31
|
+
serde = { version = "1", features = ["derive"], optional = true }
|
|
32
|
+
serde_json = { version = "1", optional = true }
|
|
33
|
+
sha2 = { version = "0.11", optional = true }
|
|
34
|
+
syn = { version = "3", features = ["full"], optional = true }
|
|
35
|
+
tokio = { version = "1.54", features = ["macros", "net", "rt-multi-thread", "signal", "time"], optional = true }
|
|
36
|
+
tracing = { version = "0.1", optional = true }
|
|
37
|
+
tracing-opentelemetry = { version = "0.34", optional = true }
|
|
38
|
+
tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "env-filter", "fmt"], optional = true }
|
|
39
|
+
typify = { version = "0.8.0", optional = true }
|
|
40
|
+
uuid = { version = "1", features = ["serde"], optional = true }
|
|
41
|
+
|
|
42
|
+
# Not on wasm: there the crate exports a small ABI of its own (src/native.rs).
|
|
43
|
+
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
|
44
|
+
napi = { version = "3.12", default-features = false, features = ["dyn-symbols", "napi4"], optional = true }
|
|
45
|
+
napi-derive = { version = "3.6", optional = true }
|
|
46
|
+
|
|
47
|
+
[dev-dependencies]
|
|
48
|
+
tower = { version = "0.5", features = ["util"] }
|
|
49
|
+
|
|
50
|
+
[lib]
|
|
51
|
+
doctest = true
|
|
52
|
+
|
|
53
|
+
[lints.rust]
|
|
54
|
+
unsafe_op_in_unsafe_fn = "deny"
|
|
55
|
+
|
|
56
|
+
[lints.clippy]
|
|
57
|
+
pedantic = { level = "deny", priority = -1 }
|
|
58
|
+
unwrap_used = "deny"
|
|
59
|
+
undocumented_unsafe_blocks = "deny"
|
|
60
|
+
allow_attributes_without_reason = "deny"
|
|
61
|
+
# Pedantic lints that cost more than they catch here. Byte offsets are u32 by Oxc's design; the rest are taste.
|
|
62
|
+
cast_possible_truncation = "allow"
|
|
63
|
+
cast_possible_wrap = "allow"
|
|
64
|
+
cast_sign_loss = "allow"
|
|
65
|
+
cast_precision_loss = "allow"
|
|
66
|
+
module_name_repetitions = "allow"
|
|
67
|
+
must_use_candidate = "allow"
|
|
68
|
+
missing_errors_doc = "allow"
|
|
69
|
+
missing_panics_doc = "allow"
|
|
70
|
+
too_many_lines = "deny"
|
|
71
|
+
similar_names = "allow"
|
|
72
|
+
struct_excessive_bools = "allow"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//! Emit each function's types, errors, trait method and dispatch entry.
|
|
2
|
+
|
|
3
|
+
use std::fmt::Write as _;
|
|
4
|
+
|
|
5
|
+
use typify::TypeSpace;
|
|
6
|
+
|
|
7
|
+
use super::{Spec, add, alias};
|
|
8
|
+
|
|
9
|
+
pub(super) fn function(
|
|
10
|
+
s: &Spec<'_>,
|
|
11
|
+
space: &mut TypeSpace,
|
|
12
|
+
extra: &mut String,
|
|
13
|
+
body: &mut String,
|
|
14
|
+
methods: &mut String,
|
|
15
|
+
table: &mut String,
|
|
16
|
+
) -> Result<(), String> {
|
|
17
|
+
let name = &s.type_name;
|
|
18
|
+
let input = match &s.input {
|
|
19
|
+
Some(schema) => {
|
|
20
|
+
*extra += &alias(
|
|
21
|
+
&format!("{name}Input"),
|
|
22
|
+
&add(space, schema, &format!("{name}Input"))?,
|
|
23
|
+
);
|
|
24
|
+
format!(", input: {name}Input")
|
|
25
|
+
}
|
|
26
|
+
None => String::new(),
|
|
27
|
+
};
|
|
28
|
+
*extra += &alias(
|
|
29
|
+
&format!("{name}Output"),
|
|
30
|
+
&add(space, &s.output, &format!("{name}Output"))?,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
let error = error(s, space, extra, body)?;
|
|
34
|
+
|
|
35
|
+
let _ = writeln!(
|
|
36
|
+
methods,
|
|
37
|
+
"/// `{id}`\nfn {method}(&self, ctx: ::weft::{ctx}{input}) -> impl ::std::future::Future<Output = ::std::result::Result<{name}Output, {error}>> + Send;",
|
|
38
|
+
id = s.id,
|
|
39
|
+
method = s.method,
|
|
40
|
+
ctx = s.ctx,
|
|
41
|
+
);
|
|
42
|
+
let (param, args) = if s.input.is_some() {
|
|
43
|
+
(format!("input: {name}Input"), "ctx, input")
|
|
44
|
+
} else {
|
|
45
|
+
("_: ()".to_string(), "ctx")
|
|
46
|
+
};
|
|
47
|
+
let _ = writeln!(
|
|
48
|
+
table,
|
|
49
|
+
".add({id:?}, {{ let s = service.clone(); move |ctx: ::weft::{ctx}, {param}| {{ let s = s.clone(); async move {{ s.{method}({args}).await }} }} }})",
|
|
50
|
+
id = s.id,
|
|
51
|
+
ctx = s.ctx,
|
|
52
|
+
method = s.method,
|
|
53
|
+
);
|
|
54
|
+
Ok(())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn error(
|
|
58
|
+
s: &Spec<'_>,
|
|
59
|
+
space: &mut TypeSpace,
|
|
60
|
+
extra: &mut String,
|
|
61
|
+
body: &mut String,
|
|
62
|
+
) -> Result<String, String> {
|
|
63
|
+
let name = &s.type_name;
|
|
64
|
+
Ok(if s.errors.is_empty() {
|
|
65
|
+
"::weft::Error".to_string()
|
|
66
|
+
} else {
|
|
67
|
+
let mut variants = String::new();
|
|
68
|
+
for (code, schema) in &s.errors {
|
|
69
|
+
let data = format!("{name}{code}");
|
|
70
|
+
*extra += &alias(&data, &add(space, schema, &data)?);
|
|
71
|
+
let _ = writeln!(variants, "{code}({data}),");
|
|
72
|
+
}
|
|
73
|
+
let _ = write!(
|
|
74
|
+
*body,
|
|
75
|
+
r#"
|
|
76
|
+
/// `{id}`'s declared errors, and `Internal` for the rest: any `std::error::Error` converts, so `?` works.
|
|
77
|
+
#[derive(Debug, ::serde::Serialize)]
|
|
78
|
+
#[serde(tag = "code", content = "data")]
|
|
79
|
+
pub enum {name}Error {{
|
|
80
|
+
{variants}
|
|
81
|
+
#[serde(skip)]
|
|
82
|
+
Internal(::std::string::String),
|
|
83
|
+
}}
|
|
84
|
+
impl<E: ::std::error::Error> ::std::convert::From<E> for {name}Error {{
|
|
85
|
+
fn from(e: E) -> Self {{ Self::Internal(e.to_string()) }}
|
|
86
|
+
}}
|
|
87
|
+
impl ::weft::Failure for {name}Error {{
|
|
88
|
+
fn failed(self) -> ::weft::Failed {{
|
|
89
|
+
match self {{
|
|
90
|
+
Self::Internal(detail) => ::weft::Failed::Internal(detail),
|
|
91
|
+
typed => match ::serde_json::to_value(typed) {{
|
|
92
|
+
Ok(v) => ::weft::Failed::Typed(v),
|
|
93
|
+
Err(e) => ::weft::Failed::Internal(e.to_string()),
|
|
94
|
+
}},
|
|
95
|
+
}}
|
|
96
|
+
}}
|
|
97
|
+
}}
|
|
98
|
+
"#,
|
|
99
|
+
id = s.id,
|
|
100
|
+
);
|
|
101
|
+
format!("{name}Error")
|
|
102
|
+
})
|
|
103
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
//! `weft::build::generate("search")` in a sidecar's or an addon's `build.rs`: `.weft/contract.json` → serde types (typify), a typed
|
|
2
|
+
//! error enum per function, the `Service` trait and `functions()`. Rust never defines a schema (PLAN §4.3). A contract
|
|
3
|
+
//! that changes reruns this, and cargo then stops exactly where the implementation no longer fits, which `weft dev`,
|
|
4
|
+
//! `weft build` and `weft check` report as R103 with the function's id.
|
|
5
|
+
|
|
6
|
+
mod emit;
|
|
7
|
+
|
|
8
|
+
use std::collections::BTreeMap;
|
|
9
|
+
use std::env;
|
|
10
|
+
use std::fs;
|
|
11
|
+
use std::path::{Path, PathBuf};
|
|
12
|
+
|
|
13
|
+
use schemars::schema::{RootSchema, Schema};
|
|
14
|
+
use serde_json::{Map, Value, json};
|
|
15
|
+
use typify::{TypeSpace, TypeSpaceSettings};
|
|
16
|
+
|
|
17
|
+
/// Generate the contract for `service` into `OUT_DIR/weft_contract.rs`, and a copy to read at `.weft/rust/<service>.rs`.
|
|
18
|
+
///
|
|
19
|
+
/// # Panics
|
|
20
|
+
/// When there's no contract or `service` has no functions. A build script reports failure by panicking; the message is
|
|
21
|
+
/// R103 with the fix.
|
|
22
|
+
pub fn generate(service: &str) {
|
|
23
|
+
println!("cargo:rerun-if-env-changed=WEFT_CONTRACT");
|
|
24
|
+
let path = contract_path().unwrap_or_else(|| panic!("R103: no .weft/contract.json above this crate. Run `weft dev` or `weft check` in the app, or set WEFT_CONTRACT"));
|
|
25
|
+
println!("cargo:rerun-if-changed={}", path.display());
|
|
26
|
+
let text = fs::read_to_string(&path)
|
|
27
|
+
.unwrap_or_else(|e| panic!("R103: can't read {}: {e}", path.display()));
|
|
28
|
+
let contract: Value = serde_json::from_str(&text)
|
|
29
|
+
.unwrap_or_else(|e| panic!("R103: {} isn't JSON: {e}", path.display()));
|
|
30
|
+
let code = render(&contract, service).unwrap_or_else(|e| panic!("R103: {e}"));
|
|
31
|
+
// An addon's undefined `napi_*` symbols are the host's, which macOS's linker has to be told.
|
|
32
|
+
if contract["services"][service]["kind"] == "native"
|
|
33
|
+
&& env::var("CARGO_CFG_TARGET_FAMILY").is_ok_and(|f| !f.contains("wasm"))
|
|
34
|
+
{
|
|
35
|
+
napi_build::setup();
|
|
36
|
+
}
|
|
37
|
+
let out =
|
|
38
|
+
PathBuf::from(env::var("OUT_DIR").expect("cargo sets OUT_DIR")).join("weft_contract.rs");
|
|
39
|
+
fs::write(&out, &code).expect("OUT_DIR is writable");
|
|
40
|
+
if let Some(dir) = path.parent().map(|p| p.join("rust")) {
|
|
41
|
+
let _ = fs::create_dir_all(&dir);
|
|
42
|
+
let _ = fs::write(dir.join(format!("{service}.rs")), &code); // to read: a build doesn't depend on it
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// `WEFT_CONTRACT`, or the nearest `.weft/contract.json` above the crate.
|
|
47
|
+
fn contract_path() -> Option<PathBuf> {
|
|
48
|
+
if let Ok(p) = env::var("WEFT_CONTRACT") {
|
|
49
|
+
return Some(PathBuf::from(p));
|
|
50
|
+
}
|
|
51
|
+
let dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").ok()?);
|
|
52
|
+
dir.ancestors()
|
|
53
|
+
.map(|d: &Path| d.join(".weft/contract.json"))
|
|
54
|
+
.find(|p| p.exists())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// `searchDocs` → `search_docs`, kept clear of what a method can't be called.
|
|
58
|
+
fn snake(name: &str) -> String {
|
|
59
|
+
let mut out = String::new();
|
|
60
|
+
for (i, c) in name.chars().enumerate() {
|
|
61
|
+
if c.is_ascii_uppercase() {
|
|
62
|
+
if i > 0 {
|
|
63
|
+
out.push('_');
|
|
64
|
+
}
|
|
65
|
+
out.push(c.to_ascii_lowercase());
|
|
66
|
+
} else if c.is_alphanumeric() || c == '_' {
|
|
67
|
+
out.push(c);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if syn::parse_str::<syn::Ident>(&out).is_err() {
|
|
71
|
+
format!("{out}_")
|
|
72
|
+
} else {
|
|
73
|
+
out
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// `searchDocs` → `SearchDocs`.
|
|
78
|
+
fn pascal(name: &str) -> String {
|
|
79
|
+
let clean: String = name
|
|
80
|
+
.chars()
|
|
81
|
+
.filter(|c| c.is_alphanumeric() || *c == '_')
|
|
82
|
+
.collect();
|
|
83
|
+
let mut chars = clean.chars();
|
|
84
|
+
chars
|
|
85
|
+
.next()
|
|
86
|
+
.map_or(String::new(), |c| c.to_uppercase().chain(chars).collect())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// typify reads schemars 0.8 schemas, which spell a tuple `items: […]`. Zod's `$defs` are collected by the caller.
|
|
90
|
+
fn normal(v: &Value, defs: &mut BTreeMap<String, Value>) -> Value {
|
|
91
|
+
match v {
|
|
92
|
+
Value::Object(m) => {
|
|
93
|
+
let mut out = Map::new();
|
|
94
|
+
for (k, x) in m {
|
|
95
|
+
match k.as_str() {
|
|
96
|
+
"$schema" => {}
|
|
97
|
+
"$defs" | "definitions" => {
|
|
98
|
+
for (name, def) in x.as_object().into_iter().flatten() {
|
|
99
|
+
let def = normal(def, defs);
|
|
100
|
+
defs.insert(name.clone(), def);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
"prefixItems" => {
|
|
104
|
+
out.insert("items".into(), normal(x, defs));
|
|
105
|
+
}
|
|
106
|
+
"$ref" => {
|
|
107
|
+
out.insert(
|
|
108
|
+
k.clone(),
|
|
109
|
+
json!(
|
|
110
|
+
x.as_str()
|
|
111
|
+
.unwrap_or("")
|
|
112
|
+
.replace("#/$defs/", "#/definitions/")
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
_ => {
|
|
117
|
+
out.insert(k.clone(), normal(x, defs));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
Value::Object(out)
|
|
122
|
+
}
|
|
123
|
+
Value::Array(a) => Value::Array(a.iter().map(|x| normal(x, defs)).collect()),
|
|
124
|
+
x => x.clone(),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
struct Spec<'a> {
|
|
129
|
+
id: &'a str,
|
|
130
|
+
method: String,
|
|
131
|
+
type_name: String,
|
|
132
|
+
ctx: &'static str,
|
|
133
|
+
input: Option<Value>,
|
|
134
|
+
output: Value,
|
|
135
|
+
errors: Vec<(String, Value)>,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn specs<'a>(functions: &[&'a Value], defs: &mut BTreeMap<String, Value>) -> Vec<Spec<'a>> {
|
|
139
|
+
let mut seen = BTreeMap::<String, usize>::new();
|
|
140
|
+
for f in functions {
|
|
141
|
+
*seen
|
|
142
|
+
.entry(f["name"].as_str().unwrap_or("").to_string())
|
|
143
|
+
.or_default() += 1;
|
|
144
|
+
}
|
|
145
|
+
functions
|
|
146
|
+
.iter()
|
|
147
|
+
.map(|f| {
|
|
148
|
+
let id = f["id"].as_str().unwrap_or("");
|
|
149
|
+
let name = f["name"].as_str().unwrap_or(id);
|
|
150
|
+
// Two files can export one name: the id's hash keeps them apart.
|
|
151
|
+
let unique = if seen[name] > 1 {
|
|
152
|
+
id.replace('.', "_")
|
|
153
|
+
} else {
|
|
154
|
+
name.to_string()
|
|
155
|
+
};
|
|
156
|
+
let imp = &f["impl"];
|
|
157
|
+
Spec {
|
|
158
|
+
id,
|
|
159
|
+
method: snake(&unique),
|
|
160
|
+
type_name: pascal(&unique),
|
|
161
|
+
ctx: match imp["ctx"].as_str() {
|
|
162
|
+
Some("shared") => "SharedCtx",
|
|
163
|
+
Some("user") => "UserCtx",
|
|
164
|
+
_ => "Ctx",
|
|
165
|
+
},
|
|
166
|
+
input: (!f["input"].is_null()).then(|| normal(&f["input"], defs)),
|
|
167
|
+
output: normal(&imp["output"], defs),
|
|
168
|
+
errors: imp["errors"]
|
|
169
|
+
.as_object()
|
|
170
|
+
.into_iter()
|
|
171
|
+
.flatten()
|
|
172
|
+
.map(|(k, v)| (k.clone(), normal(v, defs)))
|
|
173
|
+
.collect(),
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.collect()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn add(space: &mut TypeSpace, schema: &Value, name: &str) -> Result<String, String> {
|
|
180
|
+
let root: RootSchema = serde_json::from_value(schema.clone())
|
|
181
|
+
.map_err(|e| format!("`{name}` isn't a schema typify reads: {e}"))?;
|
|
182
|
+
let id = space
|
|
183
|
+
.add_type_with_name(&Schema::Object(root.schema), Some(name.to_string()))
|
|
184
|
+
.map_err(|e| format!("`{name}`: {e}"))?;
|
|
185
|
+
Ok(space
|
|
186
|
+
.get_type(&id)
|
|
187
|
+
.map_err(|e| e.to_string())?
|
|
188
|
+
.ident()
|
|
189
|
+
.to_string())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// `pub type X = …` unless typify already named the type `X`.
|
|
193
|
+
fn alias(name: &str, ident: &str) -> String {
|
|
194
|
+
if ident.replace(' ', "") == name {
|
|
195
|
+
String::new()
|
|
196
|
+
} else {
|
|
197
|
+
format!("pub type {name} = {ident};\n")
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// The Rust for `service`'s functions.
|
|
202
|
+
pub(crate) fn render(contract: &Value, service: &str) -> Result<String, String> {
|
|
203
|
+
let all = contract["functions"]
|
|
204
|
+
.as_array()
|
|
205
|
+
.ok_or("the contract has no `functions`")?;
|
|
206
|
+
let functions: Vec<&Value> = all
|
|
207
|
+
.iter()
|
|
208
|
+
.filter(|f| f["impl"]["service"] == service)
|
|
209
|
+
.collect();
|
|
210
|
+
if functions.is_empty() {
|
|
211
|
+
return Err(format!(
|
|
212
|
+
"no function in the contract has `impl: remote('rust://{service}')` or `impl: native('{service}')`. Point one at it, or rename the service in build.rs"
|
|
213
|
+
));
|
|
214
|
+
}
|
|
215
|
+
let hash = contract["services"][service]["hash"].as_str().unwrap_or("");
|
|
216
|
+
|
|
217
|
+
let mut defs = BTreeMap::new();
|
|
218
|
+
let specs = specs(&functions, &mut defs);
|
|
219
|
+
let mut settings = TypeSpaceSettings::default();
|
|
220
|
+
settings.with_struct_builder(false);
|
|
221
|
+
let mut space = TypeSpace::new(&settings);
|
|
222
|
+
let mut extra = String::new();
|
|
223
|
+
let mut body = String::new();
|
|
224
|
+
|
|
225
|
+
let refs = defs
|
|
226
|
+
.into_iter()
|
|
227
|
+
.map(|(k, v)| {
|
|
228
|
+
serde_json::from_value::<Schema>(v)
|
|
229
|
+
.map(|s| (k.clone(), s))
|
|
230
|
+
.map_err(|e| format!("`$defs/{k}`: {e}"))
|
|
231
|
+
})
|
|
232
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
233
|
+
space
|
|
234
|
+
.add_ref_types(refs)
|
|
235
|
+
.map_err(|e| format!("named schemas: {e}"))?;
|
|
236
|
+
|
|
237
|
+
let mut methods = String::new();
|
|
238
|
+
let mut table = String::new();
|
|
239
|
+
for s in &specs {
|
|
240
|
+
emit::function(
|
|
241
|
+
s,
|
|
242
|
+
&mut space,
|
|
243
|
+
&mut extra,
|
|
244
|
+
&mut body,
|
|
245
|
+
&mut methods,
|
|
246
|
+
&mut table,
|
|
247
|
+
)?;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let text = format!(
|
|
251
|
+
r"{types}
|
|
252
|
+
{extra}
|
|
253
|
+
{body}
|
|
254
|
+
/// The contract's hash: `GET /_weft/health` reports it, and neither side runs a binary built from another one.
|
|
255
|
+
pub const CONTRACT_HASH: &str = {hash:?};
|
|
256
|
+
|
|
257
|
+
/// Every function `{service}` implements. Written by `weft::build`: edit the TypeScript, not this.
|
|
258
|
+
pub trait Service: Send + Sync + 'static {{
|
|
259
|
+
{methods}
|
|
260
|
+
}}
|
|
261
|
+
|
|
262
|
+
/// What `weft::router` serves, or `weft::native!` exports.
|
|
263
|
+
pub fn functions<S: Service>(service: S) -> ::weft::Functions {{
|
|
264
|
+
let service = ::std::sync::Arc::new(service);
|
|
265
|
+
::weft::Functions::new({service:?}, CONTRACT_HASH)
|
|
266
|
+
{table}
|
|
267
|
+
}}
|
|
268
|
+
",
|
|
269
|
+
types = space.to_stream(),
|
|
270
|
+
);
|
|
271
|
+
let file = syn::parse_file(&text).map_err(|e| format!("generated code didn't parse: {e}"))?;
|
|
272
|
+
let pretty = prettyplease::unparse(&file);
|
|
273
|
+
Ok(pretty
|
|
274
|
+
.replace("::chrono::", "::weft::__rt::chrono::")
|
|
275
|
+
.replace("::uuid::", "::weft::__rt::uuid::")
|
|
276
|
+
.replace("::regress::", "::weft::__rt::regress::"))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[cfg(test)]
|
|
280
|
+
#[allow(
|
|
281
|
+
clippy::needless_pass_by_value,
|
|
282
|
+
reason = "fixtures take `json!` literals"
|
|
283
|
+
)]
|
|
284
|
+
mod tests {
|
|
285
|
+
use super::*;
|
|
286
|
+
|
|
287
|
+
fn contract(functions: Value) -> Value {
|
|
288
|
+
json!({ "functions": functions, "services": { "search": { "hash": "h1" } } })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn function(name: &str, input: Value, output: Value, ctx: &str, errors: Value) -> Value {
|
|
292
|
+
json!({ "id": format!("{name}.abc123"), "name": name, "input": input, "impl": { "service": "search", "ctx": ctx, "output": output, "errors": errors } })
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn names_follow_the_function() {
|
|
297
|
+
assert_eq!(snake("searchDocs"), "search_docs");
|
|
298
|
+
assert_eq!(snake("match"), "match_");
|
|
299
|
+
assert_eq!(pascal("searchDocs"), "SearchDocs");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn one_method_a_function_with_its_context_and_errors() {
|
|
304
|
+
let code = render(
|
|
305
|
+
&contract(json!([
|
|
306
|
+
function("searchDocs", json!({ "type": "object", "properties": { "q": { "type": "string" } }, "required": ["q"] }), json!({ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } }), "shared", json!({})),
|
|
307
|
+
function("saveDoc", Value::Null, json!({ "type": "null" }), "user", json!({ "NotFound": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } })),
|
|
308
|
+
function("other", Value::Null, json!({ "type": "string" }), "ctx", json!({})),
|
|
309
|
+
])),
|
|
310
|
+
"search",
|
|
311
|
+
)
|
|
312
|
+
.unwrap();
|
|
313
|
+
assert!(code.contains("fn search_docs("), "{code}");
|
|
314
|
+
assert!(code.contains("ctx: ::weft::SharedCtx"));
|
|
315
|
+
assert!(code.contains("input: SearchDocsInput"));
|
|
316
|
+
assert!(code.contains("pub type SearchDocsOutput ="));
|
|
317
|
+
assert!(code.contains("ctx: ::weft::UserCtx"));
|
|
318
|
+
assert!(!code.contains("SaveDocInput"), "no input, no parameter");
|
|
319
|
+
assert!(code.contains("pub enum SaveDocError"));
|
|
320
|
+
assert!(code.contains("NotFound(SaveDocNotFound)"));
|
|
321
|
+
assert!(code.contains("\"h1\""));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[test]
|
|
325
|
+
fn a_service_with_no_functions_says_how_to_fix_it() {
|
|
326
|
+
let e = render(&contract(json!([])), "search").unwrap_err();
|
|
327
|
+
assert!(e.contains("rust://search"), "{e}");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[test]
|
|
331
|
+
fn a_tuple_and_a_named_schema_from_zod() {
|
|
332
|
+
let output = json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", "items": { "$ref": "#/$defs/Hit" }, "$defs": { "Hit": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } } });
|
|
333
|
+
let tuple = json!({ "type": "array", "prefixItems": [{ "type": "string" }, { "type": "number" }], "minItems": 2, "maxItems": 2 });
|
|
334
|
+
let code = render(
|
|
335
|
+
&contract(json!([function("hits", tuple, output, "ctx", json!({}))])),
|
|
336
|
+
"search",
|
|
337
|
+
)
|
|
338
|
+
.unwrap();
|
|
339
|
+
assert!(code.contains("pub struct Hit"), "{code}");
|
|
340
|
+
assert!(
|
|
341
|
+
code.contains("pub type HitsOutput = ::std::vec::Vec<Hit>"),
|
|
342
|
+
"{code}"
|
|
343
|
+
);
|
|
344
|
+
assert!(
|
|
345
|
+
code.contains("pub type HitsInput = (::std::string::String, f64)"),
|
|
346
|
+
"{code}"
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[test]
|
|
351
|
+
fn dates_and_uuids_need_no_dependency_of_the_app() {
|
|
352
|
+
let input = json!({ "type": "object", "properties": { "at": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" } }, "required": ["at", "id"] });
|
|
353
|
+
let code = render(
|
|
354
|
+
&contract(json!([function(
|
|
355
|
+
"f",
|
|
356
|
+
input,
|
|
357
|
+
json!({ "type": "null" }),
|
|
358
|
+
"ctx",
|
|
359
|
+
json!({})
|
|
360
|
+
)])),
|
|
361
|
+
"search",
|
|
362
|
+
)
|
|
363
|
+
.unwrap();
|
|
364
|
+
assert!(code.contains("::weft::__rt::chrono::"), "{code}");
|
|
365
|
+
assert!(code.contains("::weft::__rt::uuid::"), "{code}");
|
|
366
|
+
}
|
|
367
|
+
}
|