Skip to content

Commit f1ecd90

Browse files
more writeup
1 parent 8e6ae71 commit f1ecd90

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

content/docs/specta/rfc/flightscience.mdx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,3 +1730,88 @@ I also took this opportunity to simplify some stuff:
17301730
This all leads to a much simpler system, and it also means the job of preventing stack overflows is on Specta's core not the language exporters which is really nice for reducing bugs.
17311731

17321732
Work for this was done on: [#483](https://github.com/specta-rs/specta/pull/483)
1733+
1734+
## Semantic Types
1735+
1736+
The work for this was done on: [#203](https://github.com/specta-rs/specta/issues/203)
1737+
1738+
I did previously call this nuanced types but that's not really true anymore.
1739+
1740+
### Getting terminology right
1741+
1742+
The original nuanced types feature confused two different Specta features which ended up being quite related so I wanted to clarify the terminology.
1743+
1744+
Nuanced types has the goal of allowing a language exporter to differentiate which Rust type it's targeting. The primary usecase for this is Specta Rust, as the core `DataType` model expresses `BTreeMap`, `HashMap`, `index_map::IndexMap`, etc all as `DataType::Map`. For a Rust exporter to be good we want to be able to know *which map*.
1745+
1746+
This was solved by making *everything* bar Rust primitives (`str`, `bool`, `char`, etc.) a Specta `NamedDataType` but just marked as inline so they disappear, and don't end up in the final bindings file.
1747+
1748+
Semantic types aims to make `BigInt`, `Date`, `Uint8Array`, etc. work with Specta. This goes furthur than just Rust types as these have a specific Javascript runtime component. Eg. A `Date` may be serialized to JSON as a `string` so we need to revive using `new Date(dateStrFromJson)`.
1749+
1750+
The solution to this was `specta_typescript::semantic`.
1751+
1752+
### Semantic Types
1753+
1754+
I did a massive writeup [in a comment for #203](https://github.com/specta-rs/specta/issues/203#issuecomment-4387573925) to outline my conclusion here. But in essence:
1755+
1756+
| Rust type | jiff/chrono | bytes::Bytes | url::URL | u64/i64/higher |
1757+
|-------------------------------------|---------------|---------------------|--------------|------------------|
1758+
| JSON type | string | number[] | string | **_[1]_** |
1759+
| JS -> Rust type (serialize phase) | Date | Uint8Array | URL | bigint \| number |
1760+
| Rust -> JS type (deserialize phase) | Date | Uint8Array | URL | bigint |
1761+
| JS -> Rust runtime code | **_[2]_** | `[...v]` | **_[2]_** | **_[1]_** |
1762+
| Rust -> JS runtime code | `new Date(v)` | `new Uint8Array(v)` | `new URL(v)` | **_[1]_** |
1763+
1764+
Special notes:
1765+
- *[1]* - BigInt's are explained later on and are less cut and dry.
1766+
- *[2]* - These types are actually handled by `JSON.stringify` for us so we don't need to do any special code.
1767+
1768+
### `Date`, `Uint8Array` and `URL`
1769+
1770+
The core observation of these is that the JSON type and Rust serialization is all good. All we need is a way to change the Specta type and generate some runtime JS code.
1771+
1772+
So that's where [`specta_typescript::semantic::Configuration`](https://docs.rs/specta-typescript/latest/specta_typescript/semantic/struct.Configuration.html) comes in.
1773+
1774+
Internally it defines a set of default "rules" (with the option for the user to provide custom ones) which look similar to the following:
1775+
```rust
1776+
Rule {
1777+
name: "Bytes".into(),
1778+
module_path: "bytes".into(),
1779+
data_type: DataTypeFn::new(|_| define("Uint8Array").into()),
1780+
serialize: Some(Transform::new(|i| format!("[...{i}]"))),
1781+
deserialize: Some(Transform::new(|i| format!("new Uint8Array({i})"))),
1782+
},
1783+
```
1784+
1785+
(notice the carry over from the table above)
1786+
1787+
Using `specta_typescript::semantic::Configuration::default()` we provide the following default rules:
1788+
- [`bytes::Bytes`](https://docs.rs/bytes/latest/bytes/struct.Bytes.html) to become [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)
1789+
- [`bytes::BytesMut`](https://docs.rs/bytes/latest/bytes/struct.BytesMut.html) to become [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)
1790+
- [`url::Url`](https://docs.rs/url/latest/url/struct.Url.html) to become [`Url`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL)
1791+
- [`chrono::DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) to become [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
1792+
- [`chrono::NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html) to become [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
1793+
- [`jiff::Timestamp`](https://docs.rs/jiff/latest/jiff/struct.Timestamp.html) to become [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
1794+
- [`jiff::civil::Date`](https://docs.rs/jiff/latest/jiff/civil/struct.Date.html) to become [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
1795+
1796+
Then frameworks like Tauri Specta can use with the following APIs:
1797+
- `semantic::Configuration::apply_types(types: &mut Types)`
1798+
- For applying types changes to the type map.
1799+
- `semantic::Configuration::apply_*(types: &Types, dt: &DataType, js_ident: &str,) -> Option<(Option<DataType>, String)>`.
1800+
- Takes in a `DataType`, and `js_ident` (for example `v`), then it will apply the rules returning an updated `DataType` if required and a runtime string.
1801+
- The runtime string can be used like `outputFromRust.then((v) => {js_runtime_str})`.
1802+
- This generates something like `outputFromRust.then((v) => { date_field: new Date(v) })`. It also handles nesting, arrays, etc.
1803+
- You can use `apply_serialize` or `apply_deserialize` depending on the direction as the runtime needs to be different.
1804+
1805+
This all comes together to allow these types to work. I have implemented this into Tauri Specta in [#219](https://github.com/specta-rs/tauri-specta/pull/219) and TauRPC in [#22](https://github.com/fltsci/TauRPC/pull/22).
1806+
1807+
I have some concerns about this approach with [rspc](https://github.com/specta-rs/rspc) as by generating JS code that depends on the object shape, it requires your frontend and backend to be versioned in perfect sync. For Tauri this is how it will always be as the JS and Rust is shipped in the same binary, but for people building web APIs this is not usually the case. I have ideas for solving this but it is out of scope for now and rspc is not currently maintained so it's not a massive concern.
1808+
1809+
### BigInt and special-float support in Tauri
1810+
1811+
TODO:
1812+
- Is hard - Explain how `IpcResponse` is effectivly seal from HTTP headers
1813+
- `Emitter::emit_str` literally can't work
1814+
1815+
Solutions I have implemented:
1816+
- Fork?
1817+
- `jsone` crate

0 commit comments

Comments
 (0)