@weft-js/start 0.1.1 → 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.
Files changed (213) hide show
  1. package/README.md +36 -3
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/rust-toolchain.toml +3 -0
  5. package/crate/src/build/emit.rs +103 -0
  6. package/crate/src/build.rs +367 -0
  7. package/crate/src/ctx.rs +186 -0
  8. package/crate/src/envelope.rs +163 -0
  9. package/crate/src/functions.rs +173 -0
  10. package/crate/src/lib.rs +118 -0
  11. package/crate/src/native.rs +212 -0
  12. package/crate/src/otel.rs +62 -0
  13. package/crate/src/server.rs +203 -0
  14. package/package.json +46 -37
  15. package/src/app.mjs +38 -8
  16. package/src/asset-receipt.d.mts +13 -0
  17. package/src/asset-receipt.mjs +61 -0
  18. package/src/auth/better-auth.ts +215 -17
  19. package/src/auth/drizzle.ts +1 -1
  20. package/src/auth.ts +21 -6
  21. package/src/boot.ts +19 -8
  22. package/src/bus/postgres.ts +32 -19
  23. package/src/bus.ts +7 -4
  24. package/src/{capture.mjs → capture.ts} +34 -1
  25. package/src/changes-sql.d.mts +8 -2
  26. package/src/changes-sql.mjs +26 -10
  27. package/src/changes.ts +165 -66
  28. package/src/channel/connections.ts +216 -0
  29. package/src/channel/socket.ts +396 -0
  30. package/src/channel-fake.ts +19 -4
  31. package/src/channel.ts +120 -492
  32. package/src/client.tsx +9 -2
  33. package/src/contract/describe.mjs +231 -0
  34. package/src/contract.mjs +144 -157
  35. package/src/data/cache.ts +354 -0
  36. package/src/data/definitions.ts +94 -0
  37. package/src/data/draft.ts +104 -0
  38. package/src/data/foundation.ts +95 -0
  39. package/src/data/live-api.ts +22 -0
  40. package/src/data/mutation.ts +138 -0
  41. package/src/data/primitives.ts +135 -0
  42. package/src/data/query.ts +106 -0
  43. package/src/data/transport.ts +121 -0
  44. package/src/data/types.ts +303 -0
  45. package/src/data.ts +69 -894
  46. package/src/db/bun-sqlite.d.ts +4 -1
  47. package/src/db/changes-mysql.ts +61 -21
  48. package/src/db/changes-postgres.ts +197 -118
  49. package/src/db/changes-sqlite.ts +131 -64
  50. package/src/db/core.ts +38 -13
  51. package/src/db/index.ts +20 -4
  52. package/src/db/mysql.ts +22 -6
  53. package/src/db/postgres.ts +49 -13
  54. package/src/db/sqlite.ts +53 -27
  55. package/src/derive.d.mts +4 -1
  56. package/src/derive.mjs +136 -39
  57. package/src/dev-runtime.mjs +8 -4
  58. package/src/exec/errors.ts +57 -0
  59. package/src/exec/landing.ts +66 -0
  60. package/src/exec/scope.ts +174 -0
  61. package/src/exec/touch.ts +80 -0
  62. package/src/exec/validation.ts +176 -0
  63. package/src/exec.ts +191 -341
  64. package/src/extension.ts +8 -2
  65. package/src/first-paint.ts +26 -9
  66. package/src/form/value.ts +15 -0
  67. package/src/form.tsx +132 -43
  68. package/src/hub/channels.ts +102 -0
  69. package/src/hub/connections.ts +147 -0
  70. package/src/hub/delivery.ts +62 -0
  71. package/src/hub/feed.ts +168 -0
  72. package/src/hub/presence.ts +64 -0
  73. package/src/hub/route.ts +158 -0
  74. package/src/hub/subscriptions.ts +212 -0
  75. package/src/hub/topic.ts +98 -0
  76. package/src/hub/tracked.ts +12 -0
  77. package/src/hub/types.ts +190 -0
  78. package/src/hub/view.ts +147 -0
  79. package/src/hub.ts +132 -758
  80. package/src/index.ts +84 -10
  81. package/src/inline.mjs +356 -0
  82. package/src/inspect/compare.mjs +162 -0
  83. package/src/inspect/routes.mjs +154 -0
  84. package/src/inspect/samples.mjs +59 -0
  85. package/src/inspect/url.mjs +2 -0
  86. package/src/inspect.mjs +146 -187
  87. package/src/link/election.ts +178 -0
  88. package/src/link/lines.ts +27 -0
  89. package/src/link.ts +90 -166
  90. package/src/list-drizzle.ts +104 -18
  91. package/src/list.ts +127 -36
  92. package/src/listen.ts +32 -6
  93. package/src/live/api.ts +18 -0
  94. package/src/live/backoff.ts +6 -0
  95. package/src/live/frames.ts +42 -0
  96. package/src/live/read.ts +77 -0
  97. package/src/live/sub-types.ts +24 -0
  98. package/src/live/transport.ts +86 -0
  99. package/src/live/types.ts +52 -0
  100. package/src/live-fake.ts +20 -5
  101. package/src/live-target.mjs +112 -37
  102. package/src/live.ts +116 -225
  103. package/src/media.ts +8 -1
  104. package/src/migrations.mjs +14 -6
  105. package/src/mux.ts +161 -64
  106. package/src/og/DefaultCard.tsx +40 -9
  107. package/src/og/fonts.mjs +16 -8
  108. package/src/og/images.mjs +43 -22
  109. package/src/og/index.mjs +119 -43
  110. package/src/og/native.mjs +11 -4
  111. package/src/range.mjs +15 -3
  112. package/src/router.ts +56 -6
  113. package/src/routes.mjs +73 -22
  114. package/src/run/admission.ts +80 -0
  115. package/src/run/boot.tsx +115 -0
  116. package/src/run/cache-presentation.ts +67 -0
  117. package/src/run/cache.ts +254 -0
  118. package/src/run/helpers.ts +197 -0
  119. package/src/run/listeners.ts +155 -0
  120. package/src/run/navigation.ts +382 -0
  121. package/src/run/page-skeleton.tsx +35 -0
  122. package/src/run/prefetch.ts +128 -0
  123. package/src/run/retain-view.ts +18 -0
  124. package/src/run/shape-preparation.ts +97 -0
  125. package/src/run/shape-styles.ts +72 -0
  126. package/src/run/source-context/pin.ts +124 -0
  127. package/src/run/source-context.ts +311 -0
  128. package/src/run.tsx +50 -178
  129. package/src/rust/common.ts +163 -0
  130. package/src/rust/native.ts +282 -0
  131. package/src/rust.ts +103 -229
  132. package/src/server/card.tsx +65 -0
  133. package/src/server/compression.ts +115 -0
  134. package/src/server/core.ts +70 -0
  135. package/src/server/dispatch.ts +70 -0
  136. package/src/server/form.ts +85 -0
  137. package/src/server/functions.ts +129 -0
  138. package/src/server/handler.ts +39 -0
  139. package/src/server/head.ts +96 -0
  140. package/src/server/media.ts +60 -0
  141. package/src/server/navigation-cache.ts +80 -0
  142. package/src/server/navigation.tsx +85 -0
  143. package/src/server/page-cold.ts +95 -0
  144. package/src/server/page-stream.tsx +181 -0
  145. package/src/server/page-types.ts +51 -0
  146. package/src/server/page-warm.ts +130 -0
  147. package/src/server/page.ts +69 -0
  148. package/src/server/payload.ts +89 -0
  149. package/src/server/prerender.ts +60 -0
  150. package/src/server/preview.tsx +67 -0
  151. package/src/server/render.ts +167 -0
  152. package/src/server/request.ts +98 -0
  153. package/src/server/resolver.ts +228 -0
  154. package/src/server/routes.ts +150 -0
  155. package/src/server/services.ts +69 -0
  156. package/src/server/text.ts +29 -0
  157. package/src/server/types.ts +160 -0
  158. package/src/server.tsx +82 -971
  159. package/src/shape.mjs +30 -6
  160. package/src/share/display.ts +50 -0
  161. package/src/share/network.ts +119 -0
  162. package/src/share/receive.ts +70 -0
  163. package/src/share/state.ts +30 -0
  164. package/src/share/tab-queue.ts +28 -0
  165. package/src/share-server.ts +128 -43
  166. package/src/share.ts +103 -209
  167. package/src/sheets.mjs +30 -18
  168. package/src/sidecar/build.mjs +170 -0
  169. package/src/sidecar/cargo.mjs +164 -0
  170. package/src/sidecar/common.mjs +17 -0
  171. package/src/sidecar/dev-build.mjs +91 -0
  172. package/src/sidecar/dev-watch.mjs +88 -0
  173. package/src/sidecar/manifest.mjs +19 -0
  174. package/src/sidecar/process.mjs +58 -0
  175. package/src/sidecar/refresh.mjs +19 -0
  176. package/src/sidecar.mjs +75 -416
  177. package/src/socket/check.ts +50 -0
  178. package/src/socket/deliver.ts +16 -0
  179. package/src/socket/publish.ts +56 -0
  180. package/src/socket/rooms.ts +41 -0
  181. package/src/socket/route.ts +79 -0
  182. package/src/socket/shared.ts +26 -0
  183. package/src/socket/types.ts +48 -0
  184. package/src/socket.ts +205 -194
  185. package/src/stream.ts +10 -1
  186. package/src/verify/app.ts +39 -0
  187. package/src/verify/cleanup.ts +27 -0
  188. package/src/verify/database.ts +11 -0
  189. package/src/verify/live.ts +63 -0
  190. package/src/verify/notify.ts +35 -0
  191. package/src/verify/skip.ts +19 -0
  192. package/src/verify/support.ts +149 -0
  193. package/src/verify/timing.ts +15 -0
  194. package/src/verify/trigger.ts +14 -0
  195. package/src/verify.ts +77 -176
  196. package/src/view.tsx +96 -11
  197. package/src/vite/asset-bytes.mjs +95 -0
  198. package/src/vite/assets.mjs +117 -0
  199. package/src/vite/config-hooks.mjs +166 -0
  200. package/src/vite/dev-hooks.mjs +103 -0
  201. package/src/vite/dev.mjs +102 -0
  202. package/src/vite/entries.mjs +145 -0
  203. package/src/vite/module-hooks.mjs +288 -0
  204. package/src/vite/route-modules.mjs +289 -0
  205. package/src/vite/server-functions.mjs +222 -0
  206. package/src/vite/shape-entry-css.mjs +187 -0
  207. package/src/vite/shape-override.mjs +226 -0
  208. package/src/vite/shared.mjs +51 -0
  209. package/src/vite/sheets.mjs +92 -0
  210. package/src/vite/state.mjs +89 -0
  211. package/src/vite/syntax.mjs +139 -0
  212. package/src/vite.mjs +152 -853
  213. package/src/capture.d.mts +0 -1
@@ -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,3 @@
1
+ [toolchain]
2
+ channel = "1.99.1"
3
+ components = ["clippy", "rustfmt"]
@@ -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
+ }