Skip to content

Commit fae6ffc

Browse files
authored
Merge pull request #217 from redbadger/rustdoc-typegen
rustdoc typegen
2 parents 7997fa5 + 03f0a1d commit fae6ffc

File tree

127 files changed

+7305
-3871
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+7305
-3871
lines changed

Cargo.lock

Lines changed: 708 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crux_cli/Cargo.toml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ edition.workspace = true
77
repository.workspace = true
88
license.workspace = true
99
keywords.workspace = true
10-
rust-version.workspace = true
10+
rust-version = "1.70"
1111

1212
[[bin]]
1313
name = "crux"
@@ -16,9 +16,28 @@ path = "src/main.rs"
1616
[dependencies]
1717
anyhow.workspace = true
1818
clap = { version = "4.5.35", features = ["derive"] }
19+
ascent = "0.8.0"
1920
console = "0.15.11"
21+
env_logger = "0.10.2"
22+
guppy = "0.17.4"
23+
heck = "0.5.0"
2024
ignore = "0.4.23"
25+
iter_tools = "0.24.0"
26+
lazy-regex = "3.4.1"
27+
libc = "0.2.171"
28+
log = "0.4.26"
2129
ramhorns = "1.0.1"
30+
rustdoc-json = "0.9.5"
31+
rustdoc-types = "0.38.0"
2232
serde = { workspace = true, features = ["derive"] }
33+
serde-generate = "0.29.0"
34+
serde-reflection = "0.5.0"
35+
serde_json = "1.0.140"
2336
similar = { version = "2.7.0", features = ["inline"] }
37+
thiserror = "2.0.12"
2438
toml = "0.8.20"
39+
40+
[dev-dependencies]
41+
insta = { version = "1.42.2" }
42+
pretty_assertions = "1.4.1"
43+
rstest = "0.25.0"

crux_cli/codegen.fish

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env fish
2+
argparse h/help -- $argv
3+
or return
4+
5+
if set -q _flag_help
6+
echo must specify example as the single argument
7+
return 0
8+
end
9+
10+
argparse --min-args=1 -- $argv
11+
or return
12+
13+
cargo build
14+
15+
pushd $argv[1]
16+
RUST_LOG=info ../../target/debug/crux codegen \
17+
--lib shared \
18+
--output ./shared_types/generated
19+
popd

crux_cli/src/args.rs

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::path::PathBuf;
22

3-
use clap::{ArgAction, Args, Parser, Subcommand};
3+
use clap::{ArgAction, Args, Parser, Subcommand, ValueHint::DirPath};
4+
use heck::{ToPascalCase, ToSnakeCase};
45

56
#[derive(Parser)]
67
#[command(
@@ -13,12 +14,27 @@ use clap::{ArgAction, Args, Parser, Subcommand};
1314
arg_required_else_help(true),
1415
propagate_version = true
1516
)]
16-
pub(crate) struct Cli {
17+
pub struct Cli {
1718
#[command(subcommand)]
18-
pub command: Option<Commands>,
19+
pub command: Commands,
1920

2021
#[arg(long, short, action = ArgAction::Count)]
2122
pub verbose: u8,
23+
}
24+
25+
#[derive(Subcommand)]
26+
pub enum Commands {
27+
#[command(visible_alias = "doc")]
28+
Doctor(DoctorArgs),
29+
30+
#[command(visible_alias = "gen")]
31+
Codegen(CodegenArgs),
32+
}
33+
34+
#[derive(Args)]
35+
pub struct DoctorArgs {
36+
#[arg(long, short)]
37+
pub fix: Option<PathBuf>,
2238

2339
#[arg(long, short, default_value = "false")]
2440
pub include_source_code: bool,
@@ -31,16 +47,69 @@ pub(crate) struct Cli {
3147
pub path: Option<PathBuf>,
3248
}
3349

34-
#[derive(Subcommand)]
35-
pub(crate) enum Commands {
36-
#[command(visible_alias = "doc")]
37-
Doctor(DoctorArgs),
50+
#[derive(Args)]
51+
pub struct CodegenArgs {
52+
/// name of the library containing your Crux App
53+
#[arg(long, short, value_name = "STRING")]
54+
pub lib: String,
55+
/// Output directory for generated code
56+
#[arg(
57+
long,
58+
short,
59+
value_name = "DIR",
60+
value_hint = DirPath,
61+
default_value = "./shared/generated",
62+
)]
63+
pub output: PathBuf,
64+
/// Java package name
65+
#[arg(
66+
long,
67+
short,
68+
value_name = "dotted.case",
69+
value_parser = dotted_case,
70+
default_value = "com.crux.example.shared.types"
71+
)]
72+
pub java_package: String,
73+
/// Swift package name
74+
#[arg(
75+
long,
76+
short,
77+
value_name = "PascalCase",
78+
value_parser = pascal_case,
79+
default_value = "SharedTypes")]
80+
pub swift_package: String,
81+
/// TypeScript package name
82+
#[arg(
83+
long,
84+
short,
85+
value_name = "snake_case",
86+
value_parser = snake_case,
87+
default_value = "shared_types")]
88+
pub typescript_package: String,
3889
}
3990

40-
#[derive(Args)]
41-
pub(crate) struct DoctorArgs {
42-
#[arg(long, short)]
43-
pub(crate) fix: Option<PathBuf>,
91+
fn dotted_case(s: &str) -> Result<String, String> {
92+
if s == s.to_snake_case().replace('_', ".") {
93+
Ok(s.to_string())
94+
} else {
95+
Err(format!("Invalid dotted case: {}", s))
96+
}
97+
}
98+
99+
fn pascal_case(s: &str) -> Result<String, String> {
100+
if s == s.to_pascal_case() {
101+
Ok(s.to_string())
102+
} else {
103+
Err(format!("Invalid pascal case: {}", s))
104+
}
105+
}
106+
107+
fn snake_case(s: &str) -> Result<String, String> {
108+
if s == s.to_snake_case() {
109+
Ok(s.to_string())
110+
} else {
111+
Err(format!("Invalid snake case: {}", s))
112+
}
44113
}
45114

46115
#[cfg(test)]

crux_cli/src/codegen/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Crux CLI codegen for foreign types
2+
3+
The codegen command on the `crux` CLI generates code for foreign types in Swift,
4+
Kotlin and TypeScript.
5+
6+
<!-- prettier-ignore -->
7+
> [!NOTE]
8+
> This is a work in progress and is not yet ready for general use.
9+
10+
```sh
11+
# currently requires Rust nightly to be installed (`rustup install nightly`)
12+
crux codegen --lib shared
13+
```
14+
15+
The `--lib` flag specifies the library to generate code for. The `shared`
16+
library is used in this example.
17+
18+
## How it works
19+
20+
### Source data
21+
22+
Generate `rustdoc` JSON for the library, using the
23+
[`rustdoc_json`][rustdocJsonReference] crate. We want to use the latest format
24+
version so we currently require Rust nightly to be installed
25+
(`rustup install nightly`). This JSON describes all the public and private items
26+
in the library and is deserialized using the
27+
[`rustdoc-types`][rustdocTypesReference] crate.
28+
29+
### Build a graph
30+
31+
```mermaid
32+
graph TD
33+
Ty([Type])
34+
S([Struct])
35+
SF([Struct Field])
36+
E([Enum])
37+
V([Variant])
38+
I([Impl])
39+
AI([Associated Item])
40+
TA([App Trait])
41+
TE([Effect Trait])
42+
S --> |Field| SF
43+
E --> |Variant| V
44+
V --> |Field| SF
45+
SF --> |Type| Ty
46+
I --> |AssociatedItem| AI
47+
AI --> |AssociatedType| Ty
48+
I -.-> |TraitApp| TA
49+
I -.-> |TraitEffect| TE
50+
```
51+
52+
Process the data using the [`ascent`][ascentCrateReference] crate to run a logic
53+
program (similar to Datalog) on the items, summaries and crates that are listed in the JSON.
54+
55+
We find any structs that implement the `App` trait, and build a hierarchy of those, so that we can determine which one is the root app.
56+
57+
We then dig into its associated items to find the ViewModel, Event, and Effect (with each effect's Operation and Output).
58+
59+
We also find any foreign crates that are needed and queue them for processing.
60+
61+
### Create an intermediate representation
62+
63+
Finally we take the set of edges we are interested in and use them to build an intermediate representation that is compatible with the [`serde_generate`][serdeGenerateReference] crate. This allows us to use the same backend that we are currently using in order to maintain backwards compatibility.
64+
65+
### Generate foreign types
66+
67+
The IR is used to generate code for foreign types in Swift, Kotlin and
68+
TypeScript, via a vendored version of the
69+
[`serde_generate`][serdeGenerateReference] crate.
70+
71+
We will likely want to change this in the future to allow us to generate more
72+
idiomatic code for each language, and support Crux more fully.
73+
74+
[ascentCrateReference]: https://crates.io/crates/ascent
75+
[rustdocJsonReference]: https://crates.io/crates/rustdoc-json
76+
[rustdocTypesReference]: https://crates.io/crates/rustdoc-types
77+
[serdeGenerateReference]: https://crates.io/crates/serde-generate

0 commit comments

Comments
 (0)