Initial commit
This commit is contained in:
commit
75a589f235
43 changed files with 4840 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use flake .
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Cargo.lock
|
||||||
|
/target
|
||||||
|
/.direnv
|
42
Cargo.toml
Normal file
42
Cargo.toml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
[package]
|
||||||
|
name = "eva"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["macros"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
# Very long running.
|
||||||
|
get_time_test = []
|
||||||
|
|
||||||
|
[dependencies.schemars]
|
||||||
|
version = "=1.0.0-alpha.17"
|
||||||
|
default-features = false
|
||||||
|
features = [
|
||||||
|
"std",
|
||||||
|
"derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
eva-macros.path = "./macros"
|
||||||
|
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
|
auto_impl = { git = "https://github.com/nerodono/auto_impl.git", rev = "86021942264ceabe8542a5aadb2d922554c05a1b" }
|
||||||
|
trait-set = "0.3.0"
|
||||||
|
|
||||||
|
compact_str = { version = "0.8.0", features = ["serde"] }
|
||||||
|
const_format = { version = "0.2.34", features = ["rust_1_83"] }
|
||||||
|
paste = "1.0.15"
|
||||||
|
rand = "0.9.1"
|
||||||
|
rand_xoshiro = { version = "0.7.0", features = ["serde"] }
|
||||||
|
hashbrown = { version = "0.15.2", features = ["serde"] }
|
||||||
|
ahash = "0.8.11"
|
||||||
|
perfect-derive = "0.1.5"
|
||||||
|
|
||||||
|
seq-macro = "0.3.6"
|
||||||
|
bytesize = { version = "2.0.1", features = ["serde"] }
|
||||||
|
bytes = { version = "1.10.1", features = ["serde"] }
|
||||||
|
url = { version = "2.5.4", features = ["serde"] }
|
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Eva
|
||||||
|
|
||||||
|
A backbone standard library for the VienDesu! projects. Carefully crafted functionality.
|
||||||
|
|
||||||
|
We emphasize:
|
||||||
|
- Correctness - the code inside is carefully crafted to ensure that it would be used in a correct way
|
||||||
|
- Performance - no perf trade-offs where possible
|
||||||
|
- Precise types - `eva` contains and allows you to define types more precisely, this ensures correctness statically and reveals more use-cases to the user and provides more information to the compiler
|
||||||
|
|
||||||
|
This library may re-export other crates selectively or entirely, but I strive to do that as rarely as possible.
|
||||||
|
|
99
flake.lock
generated
Normal file
99
flake.lock
generated
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750487788,
|
||||||
|
"narHash": "sha256-79O83W9osY3wyvxZHqL0gw85tcACSX0TU5en3+dky/0=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "933bc78d45abaf764dbfe0fd117be981631f3e9a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750365781,
|
||||||
|
"narHash": "sha256-XE/lFNhz5lsriMm/yjXkvSZz5DfvKJLUjsS6pP8EC50=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "08f22084e6085d19bcfb4be30d1ca76ecb96fe54",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750405264,
|
||||||
|
"narHash": "sha256-EMFKnO+J3dZOa9J+uiKZgHYgzALv9dqxY7NHV0DbO/U=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "b0552d779f7137c76f109666ce0ad28395c0e582",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
41
flake.nix
Normal file
41
flake.nix
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { flake-utils, fenix, nixpkgs, ... }:
|
||||||
|
flake-utils.lib.eachDefaultSystem(system:
|
||||||
|
let
|
||||||
|
overlays = [ fenix.overlays.default ];
|
||||||
|
pkgs = import nixpkgs { inherit system overlays; };
|
||||||
|
rust = with pkgs.fenix; combine [
|
||||||
|
((fromToolchainName { name = "1.87"; sha256 = "sha256-KUm16pHj+cRedf8vxs/Hd2YWxpOrWZ7UOrwhILdSJBU="; }).withComponents [
|
||||||
|
"cargo"
|
||||||
|
"rustc"
|
||||||
|
"rust-src"
|
||||||
|
"rust-analyzer"
|
||||||
|
"clippy"
|
||||||
|
"llvm-tools-preview"
|
||||||
|
])
|
||||||
|
default.rustfmt
|
||||||
|
];
|
||||||
|
llvm = pkgs.llvmPackages_20;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||||
|
packages = [ rust ] ++ (with pkgs; [
|
||||||
|
libclang.lib
|
||||||
|
]);
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
stdenv.cc.cc.lib
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
18
macros/Cargo.toml
Normal file
18
macros/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "eva-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = "2.0.104"
|
||||||
|
quote = "1.0.40"
|
||||||
|
proc-macro-error = "1.0.4"
|
||||||
|
proc-macro2 = "1.0.95"
|
||||||
|
|
||||||
|
darling = "0.20.10"
|
||||||
|
heck = "0.5.0"
|
||||||
|
|
||||||
|
range-set-blaze = "0.3.0"
|
603
macros/src/data.rs
Normal file
603
macros/src/data.rs
Normal file
|
@ -0,0 +1,603 @@
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use syn::{Token, parse, punctuated::Punctuated, spanned::Spanned};
|
||||||
|
|
||||||
|
use proc_macro_error::abort;
|
||||||
|
|
||||||
|
use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase};
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use crate::utils::{self, join, maybe, mk_derive};
|
||||||
|
|
||||||
|
fn wildcard_arm(var: &syn::Variant) -> proc_macro2::TokenStream {
|
||||||
|
use syn::Fields as F;
|
||||||
|
let ident = &var.ident;
|
||||||
|
match var.fields {
|
||||||
|
F::Unit => quote! { #ident },
|
||||||
|
F::Named(..) => quote! { #ident { .. } },
|
||||||
|
F::Unnamed(..) => quote! { #ident(..) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn let_destructure_pat(fields: &syn::Fields) -> proc_macro2::TokenStream {
|
||||||
|
let mut f_num = 0_usize;
|
||||||
|
utils::construct_pat(fields, |field| {
|
||||||
|
let wildcard_pat = if let Some(ident) = &field.ident {
|
||||||
|
ident.clone()
|
||||||
|
} else {
|
||||||
|
let prev = quote::format_ident!("_{f_num}");
|
||||||
|
f_num += 1;
|
||||||
|
prev
|
||||||
|
};
|
||||||
|
|
||||||
|
utils::ConstructPat::Wildcard(wildcard_pat)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
let orig = body.clone();
|
||||||
|
let item = syn::parse_macro_input!(body as Item);
|
||||||
|
let args = syn::parse_macro_input!(args as Args);
|
||||||
|
|
||||||
|
let snake_ident = item.ident.to_string().to_snake_case();
|
||||||
|
let mut snake_ident = quote::format_ident!("{snake_ident}");
|
||||||
|
snake_ident.set_span(item.ident.span());
|
||||||
|
let snake_ident = snake_ident;
|
||||||
|
let eva = args.eva.unwrap_or({
|
||||||
|
let eva = quote::format_ident!("eva");
|
||||||
|
syn::parse_quote! {
|
||||||
|
::#eva
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let orig: proc_macro2::TokenStream = orig.into();
|
||||||
|
let derives = &args.derives;
|
||||||
|
let deps = join(&eva, "_priv");
|
||||||
|
let serde = join(&deps, "serde");
|
||||||
|
|
||||||
|
let std: syn::Path = syn::parse_quote!(::std);
|
||||||
|
let fmt = join(&std, "fmt");
|
||||||
|
let cmp = join(&std, "cmp");
|
||||||
|
let clone = join(&std, "clone");
|
||||||
|
let marker = join(&std, "marker");
|
||||||
|
|
||||||
|
let ser = maybe(derives.serialize, || mk_derive(&join(&serde, "Serialize")));
|
||||||
|
let de = maybe(derives.deserialize, || {
|
||||||
|
mk_derive(&join(&serde, "Deserialize"))
|
||||||
|
});
|
||||||
|
|
||||||
|
let schemars = join(&deps, "schemars");
|
||||||
|
let json_schema = maybe(derives.json_schema, || {
|
||||||
|
let derive = mk_derive(&join(&schemars, "JsonSchema"));
|
||||||
|
let eva = quote! { #schemars }.to_string();
|
||||||
|
quote! {
|
||||||
|
#derive
|
||||||
|
#[schemars(crate = #eva)]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let serde_attr = maybe(derives.serialize || derives.deserialize, || {
|
||||||
|
let eva = quote! { #serde }.to_string();
|
||||||
|
quote! { #[serde(crate = #eva)] }
|
||||||
|
});
|
||||||
|
|
||||||
|
let partial_eq = maybe(derives.partial_eq, || mk_derive(&join(&cmp, "PartialEq")));
|
||||||
|
let eq = maybe(derives.eq, || mk_derive(&join(&cmp, "Eq")));
|
||||||
|
let partial_ord = maybe(derives.partial_ord, || mk_derive(&join(&cmp, "PartialOrd")));
|
||||||
|
let ord = maybe(derives.ord, || mk_derive(&join(&cmp, "Ord")));
|
||||||
|
|
||||||
|
let clone = maybe(derives.clone, || mk_derive(&join(&clone, "Clone")));
|
||||||
|
let copy = maybe(derives.copy, || mk_derive(&join(&marker, "Copy")));
|
||||||
|
|
||||||
|
let as_static_str = maybe(
|
||||||
|
args.as_static_str && matches!(item.kind, ItemKind::Enum { .. }),
|
||||||
|
|| {
|
||||||
|
let ItemKind::Enum { variants } = &item.kind else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||||
|
let case = join(&join(&eva, "generic"), "Case");
|
||||||
|
let arms = variants.iter().fold(quote! {}, |acc, var| {
|
||||||
|
let pat = wildcard_arm(&var);
|
||||||
|
let pat = quote! { Self::#pat };
|
||||||
|
|
||||||
|
let var_name = var.ident.to_string();
|
||||||
|
let snake = var_name.to_snake_case();
|
||||||
|
let kebab = var_name.to_kebab_case();
|
||||||
|
let pascal = var_name.to_pascal_case();
|
||||||
|
let camel = var_name.to_lower_camel_case();
|
||||||
|
|
||||||
|
let case_match = quote! {
|
||||||
|
if let Some(cvt) = convert_case {
|
||||||
|
match cvt {
|
||||||
|
#case::Snake => #snake,
|
||||||
|
#case::Kebab => #kebab,
|
||||||
|
#case::Pascal => #pascal,
|
||||||
|
#case::Camel => #camel,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#var_name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! { #acc #pat => #case_match, }
|
||||||
|
});
|
||||||
|
let ident = &item.ident;
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl #ig #ident #tyg #where_clause {
|
||||||
|
/// Convert enum discriminant into the string.
|
||||||
|
pub const fn as_static_str(&self, convert_case: Option<#case>) -> &'static str {
|
||||||
|
match *self {
|
||||||
|
#arms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let display_impl = if let Some(display) = args.display {
|
||||||
|
match &display {
|
||||||
|
DisplayImpl::Fmt(DisplayFmt {
|
||||||
|
fmt: fmt_string,
|
||||||
|
args,
|
||||||
|
}) => {
|
||||||
|
let let_assign = if let ItemKind::Struct { fields } = &item.kind {
|
||||||
|
let pat = let_destructure_pat(fields);
|
||||||
|
quote! { let Self #pat = self; }
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
};
|
||||||
|
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||||
|
let ident = &item.ident;
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
const _: () = {
|
||||||
|
use #std::io::Write as _;
|
||||||
|
|
||||||
|
#[allow(non_shorthand_field_pattern)]
|
||||||
|
impl #ig #fmt::Display for #ident #tyg #where_clause {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut #fmt::Formatter<'_>) -> #fmt::Result {
|
||||||
|
#let_assign
|
||||||
|
#std::write!(f, #fmt_string, #(#args),*)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec @ (DisplayImpl::Doc | DisplayImpl::Name) => {
|
||||||
|
let ItemKind::Enum { variants } = &item.kind else {
|
||||||
|
abort!(
|
||||||
|
item.ident,
|
||||||
|
"to use this display setting the type must be enum"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let display = join(&fmt, "Display");
|
||||||
|
let match_arms = variants.iter().fold(quote! {}, |acc, var| {
|
||||||
|
let display_str = match spec {
|
||||||
|
DisplayImpl::Name => var.ident.to_string().to_snake_case(),
|
||||||
|
DisplayImpl::Doc => combine_docs(&var.attrs).unwrap_or_else(|| {
|
||||||
|
abort!(var, "this variant has no docs");
|
||||||
|
}),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let pat = wildcard_arm(&var);
|
||||||
|
let pat = quote! { Self::#pat };
|
||||||
|
quote! { #acc #pat => #display_str, }
|
||||||
|
});
|
||||||
|
|
||||||
|
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||||
|
let ty_name = &item.ident;
|
||||||
|
|
||||||
|
let result = join(&fmt, "Result");
|
||||||
|
let formatter = join(&fmt, "Formatter");
|
||||||
|
|
||||||
|
let doc_match = quote! {
|
||||||
|
match *self {
|
||||||
|
#match_arms
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
const _: () = {
|
||||||
|
#[automatically_derived]
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
impl #ig #display for #ty_name #tyg #where_clause {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut #formatter<'_>) -> #result {
|
||||||
|
let doc_str: &'static str = #doc_match;
|
||||||
|
f.write_str(doc_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let ItemKind::Enum { variants } = &item.kind {
|
||||||
|
let mut arms = quote! {};
|
||||||
|
for var in variants.iter() {
|
||||||
|
let Some(args) = var
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.find(|attr| attr.path().is_ident("display"))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let syn::Meta::List(args) = &args.meta else {
|
||||||
|
abort!(args, "must be meta list");
|
||||||
|
};
|
||||||
|
let args: DisplayFmt = match syn::parse(args.tokens.clone().into()) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return TokenStream::from(e.to_compile_error()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fmt_string = args.fmt;
|
||||||
|
let fmt_args = args.args;
|
||||||
|
let destructure = let_destructure_pat(&var.fields);
|
||||||
|
let ident = &var.ident;
|
||||||
|
arms = quote! {
|
||||||
|
#arms
|
||||||
|
Self::#ident #destructure => {
|
||||||
|
#std::write!(f, #fmt_string, #(#fmt_args),*)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let arms = arms;
|
||||||
|
|
||||||
|
if arms.is_empty() {
|
||||||
|
quote! {}
|
||||||
|
} else {
|
||||||
|
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||||
|
let ident = &item.ident;
|
||||||
|
quote! {
|
||||||
|
const _: () = {
|
||||||
|
use #std::io::Write as _;
|
||||||
|
|
||||||
|
#[automatically_derived]
|
||||||
|
#[allow(non_shorthand_field_patterns)]
|
||||||
|
impl #ig #fmt::Display for #ident #tyg #where_clause {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut #fmt::Formatter<'_>) -> #fmt::Result {
|
||||||
|
match self {
|
||||||
|
#arms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let from_impls = {
|
||||||
|
let mut impls = Vec::<proc_macro2::TokenStream>::new();
|
||||||
|
match &item.kind {
|
||||||
|
ItemKind::Enum { variants } => {
|
||||||
|
for var in variants {
|
||||||
|
let mut generics = item.generics.clone();
|
||||||
|
let mut from_assigned = None;
|
||||||
|
let create = utils::construct(&var.fields, |field| {
|
||||||
|
let ty = &field.ty;
|
||||||
|
if let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("from")) {
|
||||||
|
if from_assigned.is_some() {
|
||||||
|
abort!(attr, "must be only one #[from]");
|
||||||
|
}
|
||||||
|
if !matches!(attr.meta, syn::Meta::Path(..)) {
|
||||||
|
abort!(attr, "must be simple path");
|
||||||
|
}
|
||||||
|
|
||||||
|
from_assigned = Some(ty.clone());
|
||||||
|
quote! { from_value }
|
||||||
|
} else {
|
||||||
|
utils::add_bound(
|
||||||
|
&mut generics.where_clause,
|
||||||
|
syn::parse_quote! {
|
||||||
|
#ty: ::core::default::Default
|
||||||
|
},
|
||||||
|
);
|
||||||
|
quote! { <#ty as ::core::default::Default>::default() }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ty) = from_assigned {
|
||||||
|
let var_ident = &var.ident;
|
||||||
|
let (ig, tyg, where_clause) = generics.split_for_impl();
|
||||||
|
let ident = &item.ident;
|
||||||
|
impls.push(quote! {
|
||||||
|
impl #ig ::core::convert::From<#ty> for #ident #tyg #where_clause {
|
||||||
|
fn from(from_value: #ty) -> Self {
|
||||||
|
Self::#var_ident #create
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemKind::Struct { fields } => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! { #(#impls)* }
|
||||||
|
};
|
||||||
|
let error = maybe(args.error, || {
|
||||||
|
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||||
|
let ident = &item.ident;
|
||||||
|
|
||||||
|
// TODO: make not that stupid.
|
||||||
|
quote! {
|
||||||
|
impl #ig #std::error::Error for #ident #tyg #where_clause {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let debug = maybe(derives.debug, || mk_derive(&join(&fmt, "Debug")));
|
||||||
|
let rename = maybe(derives.serialize || derives.deserialize, || {
|
||||||
|
quote! {
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#debug
|
||||||
|
#clone
|
||||||
|
#copy
|
||||||
|
#partial_eq
|
||||||
|
#eq
|
||||||
|
#partial_ord
|
||||||
|
#ord
|
||||||
|
#ser
|
||||||
|
#de
|
||||||
|
#serde_attr
|
||||||
|
#json_schema
|
||||||
|
#rename
|
||||||
|
#[derive(#eva::_priv::RastGawno)]
|
||||||
|
#orig
|
||||||
|
|
||||||
|
#error
|
||||||
|
|
||||||
|
#from_impls
|
||||||
|
|
||||||
|
#as_static_str
|
||||||
|
|
||||||
|
#display_impl
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn combine_docs(attrs: &[syn::Attribute]) -> Option<String> {
|
||||||
|
let docs = utils::collect_docs(attrs)?;
|
||||||
|
let res = docs.trim().trim_end_matches('.');
|
||||||
|
let mut it = res.chars();
|
||||||
|
// Make first letter lowercase.
|
||||||
|
Some(
|
||||||
|
it.next()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|c| c.to_lowercase())
|
||||||
|
.chain(it)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Derives {
|
||||||
|
serialize: bool,
|
||||||
|
deserialize: bool,
|
||||||
|
json_schema: bool,
|
||||||
|
clone: bool,
|
||||||
|
copy: bool,
|
||||||
|
partial_eq: bool,
|
||||||
|
eq: bool,
|
||||||
|
debug: bool,
|
||||||
|
partial_ord: bool,
|
||||||
|
ord: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DisplayImpl {
|
||||||
|
Fmt(DisplayFmt),
|
||||||
|
|
||||||
|
/// Implement display on enums by parsing doc-comments. Strips
|
||||||
|
/// trailing dot if present.
|
||||||
|
Doc,
|
||||||
|
/// Implement display on enums by variant names.
|
||||||
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DisplayFmt {
|
||||||
|
pub fmt: syn::LitStr,
|
||||||
|
pub args: Vec<syn::Expr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl parse::Parse for DisplayFmt {
|
||||||
|
fn parse(ps: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let fmt: syn::LitStr = ps.parse()?;
|
||||||
|
let mut args = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if ps.peek(Token![,]) {
|
||||||
|
let _: Token![,] = ps.parse()?;
|
||||||
|
if ps.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(ps.parse()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { fmt, args })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Args {
|
||||||
|
eva: Option<syn::Path>,
|
||||||
|
meta_mod: Option<syn::Ident>,
|
||||||
|
error: bool,
|
||||||
|
derives: Derives,
|
||||||
|
as_static_str: bool,
|
||||||
|
display: Option<DisplayImpl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl parse::Parse for Args {
|
||||||
|
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let mut meta_mod: Option<syn::Ident> = None;
|
||||||
|
let mut error = false;
|
||||||
|
let mut eva: Option<syn::Path> = None;
|
||||||
|
|
||||||
|
let mut as_static_str = true;
|
||||||
|
|
||||||
|
let mut display: Option<DisplayImpl> = None;
|
||||||
|
let mut derives = Derives {
|
||||||
|
serialize: true,
|
||||||
|
deserialize: true,
|
||||||
|
json_schema: true,
|
||||||
|
clone: true,
|
||||||
|
debug: true,
|
||||||
|
partial_eq: true,
|
||||||
|
eq: false,
|
||||||
|
copy: false,
|
||||||
|
partial_ord: false,
|
||||||
|
ord: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
utils::comma_separated(input, |ps| {
|
||||||
|
if ps.peek(Token![crate]) {
|
||||||
|
let _: Token![crate] = ps.parse().unwrap();
|
||||||
|
let _: Token![=] = ps.parse()?;
|
||||||
|
eva = Some(ps.parse()?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let param = utils::ident(ps)?;
|
||||||
|
|
||||||
|
if param == "error" {
|
||||||
|
error = true;
|
||||||
|
} else if param == "display" {
|
||||||
|
let content;
|
||||||
|
syn::parenthesized!(content in ps);
|
||||||
|
if let Ok(fmt) = content.parse::<DisplayFmt>() {
|
||||||
|
display = Some(DisplayImpl::Fmt(fmt));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let param = utils::ident(&content)?;
|
||||||
|
let imp = if param == "doc" {
|
||||||
|
DisplayImpl::Doc
|
||||||
|
} else if param == "name" {
|
||||||
|
DisplayImpl::Name
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(param.span(), "doc expected"));
|
||||||
|
};
|
||||||
|
|
||||||
|
display = Some(imp);
|
||||||
|
} else if param == "meta_mod" {
|
||||||
|
let _: Token![=] = ps.parse()?;
|
||||||
|
meta_mod = Some(ps.parse()?);
|
||||||
|
} else if param == "not" {
|
||||||
|
let content;
|
||||||
|
syn::parenthesized!(content in ps);
|
||||||
|
utils::comma_separated(&content, |ps| {
|
||||||
|
let ident = utils::ident(ps)?;
|
||||||
|
|
||||||
|
if ident == "serde" {
|
||||||
|
derives.serialize = false;
|
||||||
|
derives.deserialize = false;
|
||||||
|
} else if ident == "Serialize" {
|
||||||
|
derives.serialize = false;
|
||||||
|
} else if ident == "Deserialize" {
|
||||||
|
derives.deserialize = false;
|
||||||
|
} else if ident == "Debug" {
|
||||||
|
derives.debug = false;
|
||||||
|
} else if ident == "Clone" {
|
||||||
|
derives.clone = false;
|
||||||
|
} else if ident == "schemars" || ident == "JsonSchema" {
|
||||||
|
derives.json_schema = false;
|
||||||
|
} else if ident == "as_static_str" {
|
||||||
|
as_static_str = false;
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(ident.span(), "got unknown `not` attribute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
} else if param == "copy" {
|
||||||
|
derives.clone = true;
|
||||||
|
derives.copy = true;
|
||||||
|
} else if param == "eq" {
|
||||||
|
derives.eq = true;
|
||||||
|
derives.partial_eq = true;
|
||||||
|
} else if param == "ord" {
|
||||||
|
derives.eq = true;
|
||||||
|
derives.partial_eq = true;
|
||||||
|
derives.ord = true;
|
||||||
|
derives.partial_ord = true;
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(param.span(), "got unknown attribute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
meta_mod,
|
||||||
|
error,
|
||||||
|
eva,
|
||||||
|
derives,
|
||||||
|
display,
|
||||||
|
as_static_str,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ItemKind {
|
||||||
|
Enum {
|
||||||
|
variants: Punctuated<syn::Variant, Token![,]>,
|
||||||
|
},
|
||||||
|
Struct {
|
||||||
|
fields: syn::Fields,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Item {
|
||||||
|
attrs: Vec<syn::Attribute>,
|
||||||
|
vis: syn::Visibility,
|
||||||
|
ident: syn::Ident,
|
||||||
|
generics: syn::Generics,
|
||||||
|
|
||||||
|
kind: ItemKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl parse::Parse for Item {
|
||||||
|
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let item: syn::Item = input.parse()?;
|
||||||
|
let span = item.span();
|
||||||
|
|
||||||
|
match item {
|
||||||
|
syn::Item::Enum(e) => Ok(Self {
|
||||||
|
attrs: e.attrs,
|
||||||
|
vis: e.vis,
|
||||||
|
ident: e.ident,
|
||||||
|
generics: e.generics,
|
||||||
|
kind: ItemKind::Enum {
|
||||||
|
variants: e.variants,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
syn::Item::Struct(s) => Ok(Self {
|
||||||
|
attrs: s.attrs,
|
||||||
|
vis: s.vis,
|
||||||
|
ident: s.ident,
|
||||||
|
generics: s.generics,
|
||||||
|
kind: ItemKind::Struct { fields: s.fields },
|
||||||
|
}),
|
||||||
|
_ => Err(syn::Error::new(
|
||||||
|
span,
|
||||||
|
"only structs and enums are supported",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
macros/src/endpoint.rs
Normal file
68
macros/src/endpoint.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
|
||||||
|
use proc_macro_error::abort;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
_ = args;
|
||||||
|
|
||||||
|
let f = syn::parse_macro_input!(body as syn::ItemFn);
|
||||||
|
let syn::ItemFn {
|
||||||
|
attrs,
|
||||||
|
vis,
|
||||||
|
sig,
|
||||||
|
block,
|
||||||
|
} = f;
|
||||||
|
|
||||||
|
if sig.asyncness.is_none() {
|
||||||
|
abort!(sig, "must be async");
|
||||||
|
}
|
||||||
|
|
||||||
|
let generics = &sig.generics;
|
||||||
|
let ident = &sig.ident;
|
||||||
|
|
||||||
|
if sig.inputs.len() != 2 {
|
||||||
|
abort!(sig.inputs, "must contain only state and args");
|
||||||
|
}
|
||||||
|
|
||||||
|
let fn_state = sig.inputs.get(0).unwrap();
|
||||||
|
let fn_args = sig.inputs.get(1).unwrap();
|
||||||
|
|
||||||
|
let syn::FnArg::Typed(fn_state) = fn_state else {
|
||||||
|
abort!(fn_state, "must not be self");
|
||||||
|
};
|
||||||
|
let syn::FnArg::Typed(fn_args) = fn_args else {
|
||||||
|
abort!(fn_args, "must not be self");
|
||||||
|
};
|
||||||
|
|
||||||
|
let fn_args_pat = &*fn_args.pat;
|
||||||
|
let fn_state_pat = &*fn_state.pat;
|
||||||
|
|
||||||
|
let fn_args_ty = &*fn_args.ty;
|
||||||
|
let fn_state_ty = &*fn_state.ty;
|
||||||
|
|
||||||
|
let output = match sig.output {
|
||||||
|
syn::ReturnType::Default => syn::parse_quote!(()),
|
||||||
|
syn::ReturnType::Type(_, t) => t.as_ref().clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (ig, tyg, where_clause) = generics.split_for_impl();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#vis struct #ident;
|
||||||
|
|
||||||
|
impl #ig ::eva::handling::Endpoint<#fn_args_ty, #fn_state_ty> for #ident #where_clause {
|
||||||
|
type Output = #output;
|
||||||
|
|
||||||
|
#(#attrs)*
|
||||||
|
async fn call(
|
||||||
|
&self,
|
||||||
|
#fn_state_pat: #fn_state_ty,
|
||||||
|
#fn_args_pat: #fn_args_ty,
|
||||||
|
) -> Self::Output {
|
||||||
|
#block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
544
macros/src/int.rs
Normal file
544
macros/src/int.rs
Normal file
|
@ -0,0 +1,544 @@
|
||||||
|
use proc_macro::{Span, TokenStream};
|
||||||
|
|
||||||
|
use range_set_blaze::{RangeSetBlaze, UIntPlusOne};
|
||||||
|
|
||||||
|
use crate::utils::{self, join, maybe};
|
||||||
|
|
||||||
|
use quote::{ToTokens, quote};
|
||||||
|
use syn::{Token, parse, spanned::Spanned};
|
||||||
|
|
||||||
|
struct IsInclusive(bool);
|
||||||
|
|
||||||
|
impl parse::Parse for IsInclusive {
|
||||||
|
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
if input.peek(Token![=]) {
|
||||||
|
input.parse::<Token![=]>().expect("checked above");
|
||||||
|
Ok(Self(true))
|
||||||
|
} else {
|
||||||
|
Ok(Self(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SupportedLit(i128);
|
||||||
|
|
||||||
|
impl parse::Parse for SupportedLit {
|
||||||
|
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let lit: syn::Lit = input.parse()?;
|
||||||
|
match lit {
|
||||||
|
syn::Lit::Int(int) => {
|
||||||
|
let res = int.base10_parse::<i128>()?;
|
||||||
|
Ok(Self(res))
|
||||||
|
}
|
||||||
|
syn::Lit::Char(chr) => Ok(Self(chr.value() as i128)),
|
||||||
|
syn::Lit::Byte(b) => Ok(Self(b.value() as i128)),
|
||||||
|
_ => Err(syn::Error::new(
|
||||||
|
Span::call_site().into(),
|
||||||
|
"this literal type is not supported",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SingleRange {
|
||||||
|
start: i128,
|
||||||
|
end: i128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl parse::Parse for SingleRange {
|
||||||
|
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let SupportedLit(start) = input.parse()?;
|
||||||
|
// Allow specifying single values.
|
||||||
|
if !input.peek(Token![..]) {
|
||||||
|
return Ok(Self { start, end: start });
|
||||||
|
}
|
||||||
|
|
||||||
|
let _: Token![..] = input.parse()?;
|
||||||
|
let IsInclusive(inclusive) = input.parse()?;
|
||||||
|
let SupportedLit(mut end) = input.parse()?;
|
||||||
|
|
||||||
|
if !inclusive {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if start > end {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
Span::call_site().into(),
|
||||||
|
"start must be less than end",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { start, end })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum Repr {
|
||||||
|
I8,
|
||||||
|
U8,
|
||||||
|
I16,
|
||||||
|
U16,
|
||||||
|
I32,
|
||||||
|
U32,
|
||||||
|
I64,
|
||||||
|
U64,
|
||||||
|
I128,
|
||||||
|
U128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for Repr {
|
||||||
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
|
tokens.extend(match self {
|
||||||
|
Self::U8 => quote! { u8 },
|
||||||
|
Self::I8 => quote! { i8 },
|
||||||
|
Self::U16 => quote! { u16 },
|
||||||
|
Self::I16 => quote! { i16 },
|
||||||
|
Self::U32 => quote! { u32 },
|
||||||
|
Self::I32 => quote! { i32 },
|
||||||
|
Self::U64 => quote! { u64 },
|
||||||
|
Self::I64 => quote! { i64 },
|
||||||
|
Self::U128 => quote! { u128 },
|
||||||
|
Self::I128 => quote! { i128 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl parse::Parse for Repr {
|
||||||
|
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let tp: syn::TypePath = input.parse()?;
|
||||||
|
let p = tp.path;
|
||||||
|
let span = p.span();
|
||||||
|
|
||||||
|
Ok(if p.is_ident("u8") {
|
||||||
|
Self::U8
|
||||||
|
} else if p.is_ident("u16") {
|
||||||
|
Self::U16
|
||||||
|
} else if p.is_ident("u32") {
|
||||||
|
Self::U32
|
||||||
|
} else if p.is_ident("u64") {
|
||||||
|
Self::U64
|
||||||
|
} else if p.is_ident("u128") {
|
||||||
|
Self::U128
|
||||||
|
} else if p.is_ident("i8") {
|
||||||
|
Self::I8
|
||||||
|
} else if p.is_ident("i16") {
|
||||||
|
Self::I16
|
||||||
|
} else if p.is_ident("i32") {
|
||||||
|
Self::I32
|
||||||
|
} else if p.is_ident("i64") {
|
||||||
|
Self::I64
|
||||||
|
} else if p.is_ident("i128") {
|
||||||
|
Self::I128
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(span, "only u*/i* types are supported"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Args {
|
||||||
|
repr: Repr,
|
||||||
|
range: RangeSetBlaze<i128>,
|
||||||
|
crate_: syn::Path,
|
||||||
|
|
||||||
|
ser: bool,
|
||||||
|
de: bool,
|
||||||
|
schema: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl parse::Parse for Args {
|
||||||
|
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let repr: Repr = input.parse()?;
|
||||||
|
let _: Token![,] = input.parse()?;
|
||||||
|
|
||||||
|
let mut ser = true;
|
||||||
|
let mut de = true;
|
||||||
|
let mut schema = true;
|
||||||
|
|
||||||
|
let range = {
|
||||||
|
let mut set = RangeSetBlaze::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let range: SingleRange = input.parse()?;
|
||||||
|
set.ranges_insert(range.start..=range.end);
|
||||||
|
if input.peek(Token![ | ]) {
|
||||||
|
let _: Token![ | ] = input.parse()?;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut crate_: syn::Path = syn::parse_quote!(eva);
|
||||||
|
if input.peek(Token![,]) {
|
||||||
|
let _: Token![,] = input.parse()?;
|
||||||
|
|
||||||
|
utils::comma_separated(input, |ps| {
|
||||||
|
if ps.peek(Token![crate]) {
|
||||||
|
let _: Token![crate] = ps.parse().unwrap();
|
||||||
|
utils::assign(ps)?;
|
||||||
|
crate_ = ps.parse()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ident: syn::Ident = ps.parse()?;
|
||||||
|
if ident == "not" {
|
||||||
|
let content;
|
||||||
|
syn::parenthesized!(content in ps);
|
||||||
|
|
||||||
|
utils::comma_separated(&content, |ps| {
|
||||||
|
let ident: syn::Ident = ps.parse()?;
|
||||||
|
|
||||||
|
if ident == "serde" {
|
||||||
|
ser = false;
|
||||||
|
de = false;
|
||||||
|
} else if ident == "schemars" || ident == "JsonSchema" {
|
||||||
|
schema = false;
|
||||||
|
} else if ident == "Serialize" {
|
||||||
|
ser = false;
|
||||||
|
} else if ident == "Deserialize" {
|
||||||
|
de = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
repr,
|
||||||
|
range,
|
||||||
|
crate_,
|
||||||
|
|
||||||
|
ser,
|
||||||
|
de,
|
||||||
|
schema,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn int_lit(v: i128) -> syn::LitInt {
|
||||||
|
let v = v.to_string();
|
||||||
|
syn::LitInt::new(&v, Span::call_site().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variant_name(of: i128) -> syn::Ident {
|
||||||
|
quote::format_ident!(
|
||||||
|
"{}{}",
|
||||||
|
if of >= 0 { "POS" } else { "NEG" },
|
||||||
|
of.abs().to_string()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rs_len(rs: &RangeSetBlaze<i128>) -> usize {
|
||||||
|
let UIntPlusOne::UInt(m) = rs.len() else {
|
||||||
|
panic!("Shit happens")
|
||||||
|
};
|
||||||
|
|
||||||
|
m.try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mk_enum(item: &syn::ItemEnum, args: &Args) -> TokenStream {
|
||||||
|
let name = &item.ident;
|
||||||
|
let attrs = &item.attrs;
|
||||||
|
let vis = item.vis.clone();
|
||||||
|
let docs = utils::collect_docs(attrs);
|
||||||
|
|
||||||
|
let cap: usize = rs_len(&args.range);
|
||||||
|
let mut variants: Vec<syn::Variant> = Vec::with_capacity(cap);
|
||||||
|
let repr = args.repr;
|
||||||
|
|
||||||
|
for value in args.range.iter() {
|
||||||
|
let ident = variant_name(value);
|
||||||
|
let value = value.to_string();
|
||||||
|
let lit = syn::LitInt::new(&value, Span::call_site().into());
|
||||||
|
let var: syn::Variant = syn::parse_quote! {
|
||||||
|
#ident = #lit
|
||||||
|
};
|
||||||
|
|
||||||
|
variants.push(var);
|
||||||
|
}
|
||||||
|
|
||||||
|
let range_checks = args.range.ranges().map(|range| {
|
||||||
|
let start = int_lit(*range.start());
|
||||||
|
let end = int_lit(*range.end());
|
||||||
|
quote! {
|
||||||
|
if (repr >= #start) && (repr <= #end) {
|
||||||
|
return Some(unsafe { Self::new_unchecked(repr) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let eva = &args.crate_;
|
||||||
|
let priv_ = join(&args.crate_, "_priv");
|
||||||
|
let serde = join(&priv_, "serde");
|
||||||
|
let schemars = join(&priv_, "schemars");
|
||||||
|
|
||||||
|
let json_schema = maybe(args.schema, || {
|
||||||
|
let schema_id = quote! {
|
||||||
|
::std::borrow::Cow::Borrowed(concat!(module_path!(), "::", stringify!(#name)))
|
||||||
|
};
|
||||||
|
let schema_name = name.to_string();
|
||||||
|
let items: Vec<_> = args
|
||||||
|
.range
|
||||||
|
.ranges()
|
||||||
|
.map(|range| {
|
||||||
|
let minimum = int_lit(*range.start());
|
||||||
|
let maximum = int_lit(*range.end());
|
||||||
|
quote! {
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": #minimum,
|
||||||
|
"maximum": #maximum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let desc = if let Some(desc) = docs.as_deref() {
|
||||||
|
quote! {
|
||||||
|
"description": #desc,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
};
|
||||||
|
let schema = quote! {
|
||||||
|
#schemars::json_schema!({
|
||||||
|
#desc
|
||||||
|
"anyOf": [#(#items),*]
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl #schemars::JsonSchema for #name {
|
||||||
|
fn schema_id() -> ::std::borrow::Cow<'static, str> {
|
||||||
|
#schema_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_name() -> ::std::borrow::Cow<'static, str> {
|
||||||
|
::std::borrow::Cow::Borrowed(#schema_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(_: &mut #schemars::SchemaGenerator) -> #schemars::Schema {
|
||||||
|
#schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let serialize = maybe(args.ser, || {
|
||||||
|
quote! {
|
||||||
|
const _: () = {
|
||||||
|
use ::core::result::Result;
|
||||||
|
|
||||||
|
impl #serde::Serialize for #name {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: #serde::Serializer,
|
||||||
|
{
|
||||||
|
<#repr as #serde::Serialize>::serialize(&self.into_inner(), serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let deserialize = maybe(args.de, || {
|
||||||
|
quote! {
|
||||||
|
const _: () = {
|
||||||
|
use ::core::result::Result;
|
||||||
|
|
||||||
|
impl<'de> #serde::Deserialize<'de> for #name {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: #serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Self::new(<#repr as #serde::Deserialize<'de>>::deserialize(deserializer)?)
|
||||||
|
.ok_or_else(|| #serde::de::Error::custom(#eva::generic::OutOfRange))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ranges = {
|
||||||
|
let ranges_no = args.range.ranges_len();
|
||||||
|
let repr = &args.repr;
|
||||||
|
let ranges = args.range.ranges().map(|r| {
|
||||||
|
let from = int_lit(*r.start());
|
||||||
|
let to = int_lit(*r.end());
|
||||||
|
|
||||||
|
quote! { #from..=#to }
|
||||||
|
});
|
||||||
|
let values_no = rs_len(&args.range);
|
||||||
|
let values = args.range.iter().map(|x| {
|
||||||
|
let lit = int_lit(x);
|
||||||
|
quote! { #lit }
|
||||||
|
});
|
||||||
|
|
||||||
|
let variants = args.range.iter().map(|x| {
|
||||||
|
let variant = variant_name(x);
|
||||||
|
quote! { Self::#variant }
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl #name {
|
||||||
|
#vis const RANGES: [::std::ops::RangeInclusive<#repr>; #ranges_no] = [#(#ranges),*];
|
||||||
|
#vis const VALUES: [#repr; #values_no] = [#(#values),*];
|
||||||
|
#vis const VARIANTS: [Self; #values_no] = [#(#variants),*];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let nth_impl = {
|
||||||
|
let mut skip = 0;
|
||||||
|
let arms = args.range.ranges().fold(quote! {}, |acc, range| {
|
||||||
|
let skip_cur = int_lit(skip);
|
||||||
|
skip += *range.end() - *range.start() + 1;
|
||||||
|
let start = int_lit(*range.start());
|
||||||
|
let end = int_lit(*range.end());
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#acc
|
||||||
|
#start ..= #end => (value - #start + #skip_cur) as usize,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
quote! {
|
||||||
|
impl #name {
|
||||||
|
/// Get index in the [`Self::VARIANTS`] array.
|
||||||
|
pub const fn nth(self) -> usize {
|
||||||
|
let value = self.into_inner();
|
||||||
|
match value {
|
||||||
|
#arms
|
||||||
|
|
||||||
|
_ => unsafe { ::core::hint::unreachable_unchecked() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let rand_impl = {
|
||||||
|
if args.range.is_empty() {
|
||||||
|
quote! {}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
impl #eva::rand::distr::Distribution<#name> for #eva::rand::distr::StandardUniform {
|
||||||
|
fn sample<R: #eva::rand::Rng + ?Sized>(&self, rng: &mut R) -> #name {
|
||||||
|
let idx: usize = <R as #eva::rand::Rng>::random_range(rng, 0..#name::VARIANTS.len());
|
||||||
|
#name::VARIANTS[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#[repr(#repr)]
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
#(#attrs)*
|
||||||
|
#vis enum #name {
|
||||||
|
#(#variants),*
|
||||||
|
}
|
||||||
|
|
||||||
|
#nth_impl
|
||||||
|
#rand_impl
|
||||||
|
#ranges
|
||||||
|
|
||||||
|
#serialize
|
||||||
|
#deserialize
|
||||||
|
|
||||||
|
#json_schema
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
use ::core::{mem, fmt, slice, cmp, result::Result, option::Option};
|
||||||
|
|
||||||
|
impl cmp::PartialEq<#repr> for #name {
|
||||||
|
fn eq(&self, other: &#repr) -> bool {
|
||||||
|
*other == *self as #repr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cmp::PartialOrd for #name {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cmp::Ord for #name {
|
||||||
|
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||||
|
self.into_inner().cmp(&other.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #name {
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The `repr` must be in range.
|
||||||
|
#vis const unsafe fn new_unchecked(repr: #repr) -> Self {
|
||||||
|
unsafe { mem::transmute::<#repr, Self>(repr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create integer from underlying representation.
|
||||||
|
#vis const fn new(repr: #repr) -> Option<Self> {
|
||||||
|
#(#range_checks)*
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert slice into slice of underlying integer
|
||||||
|
/// representation.
|
||||||
|
#vis const fn as_repr_slice(slice: &[Self]) -> &[#repr] {
|
||||||
|
// SAFETY: #repr type domain is wider than ours.
|
||||||
|
unsafe { slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert mutable slice into mutable slice of underlying integer
|
||||||
|
/// representation.
|
||||||
|
#vis const fn as_repr_slice_mut(slice: &mut [Self]) -> &mut [#repr] {
|
||||||
|
// SAFETY: #repr type domain is wider than ours.
|
||||||
|
unsafe { slice::from_raw_parts_mut(slice.as_mut_ptr().cast(), slice.len()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert integer into its representation.
|
||||||
|
#vis const fn into_inner(self) -> #repr {
|
||||||
|
self as #repr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Copy for #name {}
|
||||||
|
impl Clone for #name {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<#name> for #repr {
|
||||||
|
fn from(val: #name) -> Self {
|
||||||
|
val as #repr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for #name {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let underlying: #repr = (*self).into();
|
||||||
|
<#repr as fmt::Debug>::fmt(&underlying, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for #name {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let underlying: #repr = (*self).into();
|
||||||
|
<#repr as fmt::Display>::fmt(&underlying, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
let body = syn::parse_macro_input!(body as syn::ItemEnum);
|
||||||
|
let args: Args = syn::parse_macro_input!(args as Args);
|
||||||
|
|
||||||
|
mk_enum(&body, &args)
|
||||||
|
}
|
69
macros/src/lib.rs
Normal file
69
macros/src/lib.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
#![expect(warnings)]
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro_error::proc_macro_error;
|
||||||
|
|
||||||
|
/// A "property" (trait). Includes set of utilities for working
|
||||||
|
/// with traits.
|
||||||
|
#[proc_macro_error]
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn prop(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_error]
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn endpoint(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
self::endpoint::transform(args, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(RastGawno, attributes(display, from, bounds))]
|
||||||
|
pub fn rast_gawno(body: TokenStream) -> TokenStream {
|
||||||
|
quote::quote! {}.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_error]
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn data(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
self::data::transform(args, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An integer type in a specified range.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// #[int(u8, 0..=100)]
|
||||||
|
/// pub enum Rating {}
|
||||||
|
///
|
||||||
|
/// #[int(u8, 1..=8 | 70..=100)]
|
||||||
|
/// pub enum Age {}
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Extension around [`sum`] macro.
|
||||||
|
#[proc_macro_error]
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn int(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
self::int::transform(args, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A string type.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// #[str(fixed)]
|
||||||
|
/// struct Fixed([ascii::Printable; 4]);
|
||||||
|
///
|
||||||
|
/// #[str(newtype)]
|
||||||
|
/// struct Newtype(Fixed);
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_error]
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn str(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
self::str::transform(args, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
mod int;
|
||||||
|
mod str;
|
||||||
|
|
||||||
|
mod data;
|
||||||
|
mod endpoint;
|
472
macros/src/str.rs
Normal file
472
macros/src/str.rs
Normal file
|
@ -0,0 +1,472 @@
|
||||||
|
use syn::{Token, parse};
|
||||||
|
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro_error::{OptionExt as _, abort};
|
||||||
|
|
||||||
|
pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||||
|
let mut args = syn::parse_macro_input!(args as Args);
|
||||||
|
let mut input = syn::parse_macro_input!(body as syn::DeriveInput);
|
||||||
|
|
||||||
|
let eva = &args.crate_;
|
||||||
|
let ident = input.ident.clone();
|
||||||
|
let mut impls = quote! {};
|
||||||
|
|
||||||
|
let docs = utils::collect_docs(&input.attrs);
|
||||||
|
|
||||||
|
let priv_ = utils::join(&eva, "_priv");
|
||||||
|
let serde = utils::join(&priv_, "serde");
|
||||||
|
let schemars = utils::join(&priv_, "schemars");
|
||||||
|
|
||||||
|
let (ig, tyg, where_clause) = input.generics.split_for_impl();
|
||||||
|
let mut json_schema = quote! {};
|
||||||
|
|
||||||
|
match args.kind {
|
||||||
|
Kind::Fixed { error_ty } => {
|
||||||
|
input.attrs.push(syn::parse_quote!(#[repr(C)]));
|
||||||
|
|
||||||
|
let mut generics = input.generics.clone();
|
||||||
|
let syn::Data::Struct(s) = &input.data else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
let fields = &s.fields;
|
||||||
|
|
||||||
|
args.copy = true;
|
||||||
|
args.clone = true;
|
||||||
|
|
||||||
|
let mut pats = quote! {};
|
||||||
|
|
||||||
|
for field in fields {
|
||||||
|
let ty = &field.ty;
|
||||||
|
pats = quote! {
|
||||||
|
#pats
|
||||||
|
<#ty as #eva::str::HasPattern>::pat_into(buf);
|
||||||
|
};
|
||||||
|
let predicate = syn::PredicateType {
|
||||||
|
lifetimes: None,
|
||||||
|
bounded_ty: ty.clone(),
|
||||||
|
colon_token: Token),
|
||||||
|
bounds: syn::parse_quote!(#eva::str::FixedUtf8),
|
||||||
|
};
|
||||||
|
let predicate = syn::WherePredicate::Type(predicate);
|
||||||
|
if let Some(ref mut clause) = generics.where_clause {
|
||||||
|
clause.predicates.push(predicate);
|
||||||
|
} else {
|
||||||
|
generics.where_clause = Some(syn::parse_quote! {
|
||||||
|
where #predicate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ig, tyg, where_clause) = generics.split_for_impl();
|
||||||
|
let schema_id = quote! {
|
||||||
|
concat!(module_path!(), "::", stringify!(#ident))
|
||||||
|
};
|
||||||
|
let desc = if let Some(desc) = docs.as_deref() {
|
||||||
|
quote! { "description": #desc, }
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let check = {
|
||||||
|
let tys = fields.iter().map(|f| &f.ty);
|
||||||
|
let expected_size = quote! {
|
||||||
|
0_usize #(+ ::std::mem::size_of::<#tys>())*
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
let expected_size = #expected_size;
|
||||||
|
if s.len() != expected_size {
|
||||||
|
return Err(<#error_ty as #eva::str::FixedParseError>::length(expected_size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let parse = utils::construct(&fields, |field| {
|
||||||
|
let ty = &field.ty;
|
||||||
|
quote! {{
|
||||||
|
let ty_size = ::std::mem::size_of::<#ty>();
|
||||||
|
let chunk = unsafe { s.get_unchecked(..ty_size) };
|
||||||
|
let res = chunk.parse()?;
|
||||||
|
s = unsafe { s.get_unchecked(ty_size..) };
|
||||||
|
res
|
||||||
|
}}
|
||||||
|
});
|
||||||
|
impls = quote! {
|
||||||
|
#impls
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
use ::core::{primitive::str, result::Result, str::FromStr};
|
||||||
|
use ::std::{borrow::Cow, string::String};
|
||||||
|
|
||||||
|
impl #ig #schemars::JsonSchema for #ident #tyg #where_clause {
|
||||||
|
#[inline]
|
||||||
|
fn schema_id() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed(#schema_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn schema_name() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed(stringify!(#ident))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn json_schema(_: &mut #schemars::SchemaGenerator) -> #schemars::Schema {
|
||||||
|
let len = std::mem::size_of::<Self>();
|
||||||
|
let mut pat = String::from("^");
|
||||||
|
<Self as #eva::str::HasPattern>::pat_into(&mut pat);
|
||||||
|
pat.push('$');
|
||||||
|
|
||||||
|
#schemars::json_schema!({
|
||||||
|
"type": "string",
|
||||||
|
#desc
|
||||||
|
"minLength": len,
|
||||||
|
"maxLength": len,
|
||||||
|
"pattern": pat,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig #eva::str::HasPattern for #ident #tyg #where_clause {
|
||||||
|
#[inline]
|
||||||
|
fn pat_into(buf: &mut String) {
|
||||||
|
#pats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl #ig #eva::str::FixedUtf8 for #ident #tyg #where_clause {}
|
||||||
|
impl #ig #ident #tyg #where_clause {
|
||||||
|
/// Get str representation.
|
||||||
|
pub const fn as_str(&self) -> &str {
|
||||||
|
#eva::str::reinterpret(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig FromStr for #ident #tyg #where_clause {
|
||||||
|
type Err = #error_ty;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
|
||||||
|
#check
|
||||||
|
Ok(Self #parse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Kind::Custom => {}
|
||||||
|
|
||||||
|
Kind::Newtype => {
|
||||||
|
let syn::Data::Struct(s) = &input.data else {
|
||||||
|
abort!(input, "this must be struct");
|
||||||
|
};
|
||||||
|
let field = s
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.expect_or_abort("struct must have at least one field");
|
||||||
|
let ty = &field.ty;
|
||||||
|
let access = if let Some(name) = &field.ident {
|
||||||
|
quote! { #name }
|
||||||
|
} else {
|
||||||
|
quote! { 0 }
|
||||||
|
};
|
||||||
|
let schemars_path = quote! {#schemars}.to_string();
|
||||||
|
json_schema = quote! {
|
||||||
|
#[derive(#schemars::JsonSchema)]
|
||||||
|
#[schemars(crate = #schemars_path)]
|
||||||
|
};
|
||||||
|
|
||||||
|
if args.pat {
|
||||||
|
impls = quote! {
|
||||||
|
#impls
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
use ::std::string::String;
|
||||||
|
|
||||||
|
impl #ig #eva::str::HasPattern for #ident #tyg #where_clause {
|
||||||
|
#[inline(always)]
|
||||||
|
fn pat_into(buf: &mut String) {
|
||||||
|
<#ty as #eva::str::HasPattern>::pat_into(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut first = true;
|
||||||
|
let parse = utils::construct(&s.fields, |field| {
|
||||||
|
if first {
|
||||||
|
first = false;
|
||||||
|
let ty = &field.ty;
|
||||||
|
quote! {
|
||||||
|
<#ty as FromStr>::from_str(s)?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! { ::core::default::Default::default() }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
impls = quote! {
|
||||||
|
#impls
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
use ::core::{str::FromStr, result::Result};
|
||||||
|
|
||||||
|
impl #ig FromStr for #ident #tyg #where_clause {
|
||||||
|
type Err = <#ty as FromStr>::Err;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self #parse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig #ident #tyg #where_clause {
|
||||||
|
#[inline]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.#access.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ser = utils::maybe(args.ser, || {
|
||||||
|
quote! {
|
||||||
|
const _: () = {
|
||||||
|
use ::core::result::Result;
|
||||||
|
|
||||||
|
impl #ig #serde::Serialize for #ident #tyg #where_clause {
|
||||||
|
#[inline]
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: #serde::Serializer,
|
||||||
|
{
|
||||||
|
<str as #serde::Serialize>::serialize(self.as_str(), serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let de = utils::maybe(args.de, || {
|
||||||
|
let mut generics = input.generics.clone();
|
||||||
|
let lt: syn::Lifetime = syn::parse_quote!('__de);
|
||||||
|
generics.params.insert(
|
||||||
|
0,
|
||||||
|
syn::GenericParam::Lifetime(syn::LifetimeParam::new(lt.clone())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (ig, ..) = generics.split_for_impl();
|
||||||
|
let (_, tyg, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
const _: () = {
|
||||||
|
use ::core::{result::Result, str::FromStr};
|
||||||
|
|
||||||
|
impl #ig #serde::Deserialize<#lt> for #ident #tyg #where_clause {
|
||||||
|
#[inline]
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: #serde::Deserializer<#lt>,
|
||||||
|
{
|
||||||
|
let s = <#eva::str::CompactString as #serde::Deserialize<#lt>>::deserialize(deserializer)?;
|
||||||
|
<Self as FromStr>::from_str(&s)
|
||||||
|
.map_err(#serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
impls = quote! {
|
||||||
|
#impls
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
use ::core::{
|
||||||
|
ops::Deref,
|
||||||
|
borrow::Borrow,
|
||||||
|
convert::AsRef,
|
||||||
|
cmp::{PartialEq, Eq},
|
||||||
|
primitive::str,
|
||||||
|
hash::{Hasher, Hash},
|
||||||
|
fmt,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl #ig fmt::Debug for #ident #tyg #where_clause {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
<str as fmt::Debug>::fmt(self.as_str(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig fmt::Display for #ident #tyg #where_clause {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig Hash for #ident #tyg #where_clause {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.as_str().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig PartialEq<str> for #ident #tyg #where_clause {
|
||||||
|
fn eq(&self, other: &str) -> bool {
|
||||||
|
self.as_str() == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig Borrow<str> for #ident #tyg #where_clause {
|
||||||
|
fn borrow(&self) -> &str {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig AsRef<str> for #ident #tyg #where_clause {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #ig Deref for #ident #tyg #where_clause {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let clone = utils::maybe(args.clone, || {
|
||||||
|
utils::mk_derive(&syn::parse_quote!(::core::clone::Clone))
|
||||||
|
});
|
||||||
|
let copy = utils::maybe(args.copy, || {
|
||||||
|
utils::mk_derive(&syn::parse_quote!(::core::marker::Copy))
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#clone
|
||||||
|
#copy
|
||||||
|
#json_schema
|
||||||
|
#input
|
||||||
|
#impls
|
||||||
|
|
||||||
|
#ser
|
||||||
|
#de
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Args {
|
||||||
|
kind: Kind,
|
||||||
|
ser: bool,
|
||||||
|
de: bool,
|
||||||
|
clone: bool,
|
||||||
|
pat: bool,
|
||||||
|
copy: bool,
|
||||||
|
schema: bool,
|
||||||
|
crate_: syn::Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl parse::Parse for Args {
|
||||||
|
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
let ser = true;
|
||||||
|
let de = true;
|
||||||
|
let schema = true;
|
||||||
|
let clone = true;
|
||||||
|
let mut copy = false;
|
||||||
|
let mut pat = true;
|
||||||
|
|
||||||
|
let kind = utils::ident(input)?;
|
||||||
|
let kind = if kind == "newtype" {
|
||||||
|
Kind::Newtype
|
||||||
|
} else if kind == "fixed" {
|
||||||
|
let content;
|
||||||
|
syn::parenthesized!(content in input);
|
||||||
|
let mut error_ty = None::<syn::Type>;
|
||||||
|
utils::comma_separated(&content, |ps| {
|
||||||
|
let ident = utils::ident(ps)?;
|
||||||
|
if ident == "error" {
|
||||||
|
utils::assign(ps)?;
|
||||||
|
error_ty = Some(ps.parse()?);
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(ident.span(), "unexpected parameter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
Kind::Fixed {
|
||||||
|
error_ty: error_ty.expect_or_abort("there must be error type"),
|
||||||
|
}
|
||||||
|
} else if kind == "custom" {
|
||||||
|
Kind::Custom
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
kind.span(),
|
||||||
|
"expected `newtype`, `fixed` or `custom`",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let mut crate_: syn::Path = syn::parse_quote!(::eva);
|
||||||
|
|
||||||
|
if utils::token_is(input, Token![,]) {
|
||||||
|
utils::comma_separated(input, |ps| {
|
||||||
|
if utils::token_is(ps, Token![crate]) {
|
||||||
|
utils::assign(ps)?;
|
||||||
|
crate_ = ps.parse()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ident = utils::ident(ps)?;
|
||||||
|
if ident == "copy" {
|
||||||
|
copy = true;
|
||||||
|
} else if ident == "not" {
|
||||||
|
let content;
|
||||||
|
syn::parenthesized!(content in ps);
|
||||||
|
utils::comma_separated(&content, |ps| {
|
||||||
|
let key = utils::ident(ps)?;
|
||||||
|
|
||||||
|
if key == "pat" {
|
||||||
|
pat = false;
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(key.span(), "invalid key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(ident.span(), "unexpected parameter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
kind,
|
||||||
|
pat,
|
||||||
|
crate_,
|
||||||
|
clone: clone || copy,
|
||||||
|
copy,
|
||||||
|
ser,
|
||||||
|
de,
|
||||||
|
schema,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Kind {
|
||||||
|
/// Newtype over some other string.
|
||||||
|
Newtype,
|
||||||
|
|
||||||
|
/// Fixed size string combined from other fixed
|
||||||
|
/// size strings.
|
||||||
|
Fixed { error_ty: syn::Type },
|
||||||
|
|
||||||
|
/// Custom string, must provide `as_str` and `FromStr`.
|
||||||
|
Custom,
|
||||||
|
}
|
239
macros/src/utils.rs
Normal file
239
macros/src/utils.rs
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
use syn::{Token, parse, punctuated::Punctuated, spanned::Spanned as _};
|
||||||
|
|
||||||
|
use std::{fmt, ops::Deref};
|
||||||
|
|
||||||
|
use proc_macro_error::abort;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
type PunctFields = Punctuated<syn::Field, Token![,]>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum RefOrOwned<'a, T> {
|
||||||
|
Ref(&'a T),
|
||||||
|
Owned(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Deref for RefOrOwned<'a, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self {
|
||||||
|
Self::Ref(r) => *r,
|
||||||
|
Self::Owned(r) => r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn once<F, S, I, O>(fst: F, mut snd: S) -> impl FnMut(I) -> O
|
||||||
|
where
|
||||||
|
F: FnOnce(I) -> O,
|
||||||
|
S: FnMut(I) -> O,
|
||||||
|
{
|
||||||
|
let mut fst = Some(fst);
|
||||||
|
move |x| {
|
||||||
|
if let Some(fst) = fst.take() {
|
||||||
|
fst(x)
|
||||||
|
} else {
|
||||||
|
snd(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_bound(to: &mut Option<syn::WhereClause>, pred: syn::WherePredicate) {
|
||||||
|
if let Some(clause) = to {
|
||||||
|
clause.predicates.push(pred);
|
||||||
|
} else {
|
||||||
|
*to = Some(syn::WhereClause {
|
||||||
|
where_token: syn::Token),
|
||||||
|
predicates: {
|
||||||
|
let mut preds = syn::punctuated::Punctuated::new();
|
||||||
|
preds.push(pred);
|
||||||
|
|
||||||
|
preds
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn construct_wrap(
|
||||||
|
fields: &syn::Fields,
|
||||||
|
) -> fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream {
|
||||||
|
match fields {
|
||||||
|
syn::Fields::Named(..) => |x| quote! { { #x } },
|
||||||
|
syn::Fields::Unnamed(..) => |x| quote! { (#x) },
|
||||||
|
syn::Fields::Unit => |_| quote! {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_punct() -> Punctuated<syn::Field, Token![,]> {
|
||||||
|
Punctuated::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fields_of<'a>(fields: &'a syn::Fields) -> RefOrOwned<'a, PunctFields> {
|
||||||
|
match fields {
|
||||||
|
syn::Fields::Named(fields) => RefOrOwned::Ref(&fields.named),
|
||||||
|
syn::Fields::Unnamed(fields) => RefOrOwned::Ref(&fields.unnamed),
|
||||||
|
syn::Fields::Unit => RefOrOwned::Owned(PunctFields::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ConstructPat {
|
||||||
|
Wildcard(syn::Ident),
|
||||||
|
Other(proc_macro2::TokenStream),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConstructPat {
|
||||||
|
fn to_ts(&self) -> proc_macro2::TokenStream {
|
||||||
|
match self {
|
||||||
|
ConstructPat::Wildcard(i) => quote! { #i },
|
||||||
|
ConstructPat::Other(othr) => othr.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn construct_pat<F>(fields: &syn::Fields, mut mk_pat: F) -> proc_macro2::TokenStream
|
||||||
|
where
|
||||||
|
F: FnMut(&syn::Field) -> ConstructPat,
|
||||||
|
{
|
||||||
|
let wrap = construct_wrap(fields);
|
||||||
|
let fields = fields_of(fields);
|
||||||
|
let inside = fields.iter().fold(quote! {}, |acc, field| {
|
||||||
|
let pat = mk_pat(field);
|
||||||
|
if let Some(ident) = &field.ident {
|
||||||
|
'res: {
|
||||||
|
if let ConstructPat::Wildcard(ref wc) = pat {
|
||||||
|
if wc == ident {
|
||||||
|
break 'res quote! { #acc #wc, };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ts = pat.to_ts();
|
||||||
|
quote! { #acc #ident : #ts, }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let value = pat.to_ts();
|
||||||
|
quote! { #acc #value, }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wrap(inside)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn construct<F>(fields: &syn::Fields, mut mk_value: F) -> proc_macro2::TokenStream
|
||||||
|
where
|
||||||
|
F: FnMut(&syn::Field) -> proc_macro2::TokenStream,
|
||||||
|
{
|
||||||
|
let wrap = construct_wrap(fields);
|
||||||
|
let fields = fields_of(fields);
|
||||||
|
let inside = fields.iter().fold(quote! {}, |acc, field| {
|
||||||
|
let value = mk_value(field);
|
||||||
|
if let Some(ident) = &field.ident {
|
||||||
|
quote! { #acc #ident : #value, }
|
||||||
|
} else {
|
||||||
|
quote! { #acc #value, }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wrap(inside)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token_is<F, I, T>(ps: parse::ParseStream, tok: F) -> bool
|
||||||
|
where
|
||||||
|
F: parse::Peek + Copy + FnOnce(I) -> T,
|
||||||
|
T: syn::token::Token + parse::Parse,
|
||||||
|
{
|
||||||
|
if ps.peek(tok) {
|
||||||
|
let _: T = ps.parse().unwrap();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_docs(attrs: &[syn::Attribute]) -> Option<String> {
|
||||||
|
let mut docs = String::new();
|
||||||
|
let mut had = false;
|
||||||
|
|
||||||
|
for attr in attrs {
|
||||||
|
if !attr.path().is_ident("doc") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let syn::Meta::NameValue(nv) = &attr.meta else {
|
||||||
|
abort!(attr, r##"must be in form #[doc = "string literal"]"##);
|
||||||
|
};
|
||||||
|
|
||||||
|
let syn::Expr::Lit(syn::ExprLit {
|
||||||
|
attrs: _,
|
||||||
|
lit: syn::Lit::Str(litstr),
|
||||||
|
}) = &nv.value
|
||||||
|
else {
|
||||||
|
abort!(nv, "must be string literal");
|
||||||
|
};
|
||||||
|
had = true;
|
||||||
|
docs.push_str(&litstr.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
had.then_some(docs.trim().to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maybe<F>(b: bool, f: F) -> proc_macro2::TokenStream
|
||||||
|
where
|
||||||
|
F: FnOnce() -> proc_macro2::TokenStream,
|
||||||
|
{
|
||||||
|
if b {
|
||||||
|
f()
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mk_derive(path: &syn::Path) -> proc_macro2::TokenStream {
|
||||||
|
quote! { #[derive(#path)] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_next(ts: parse::ParseStream) -> bool {
|
||||||
|
if ts.peek(Token![,]) {
|
||||||
|
let _: Token![,] = ts.parse().unwrap();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assign(ts: parse::ParseStream) -> syn::Result<()> {
|
||||||
|
let _: Token![=] = ts.parse()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ident(ts: parse::ParseStream) -> syn::Result<syn::Ident> {
|
||||||
|
ts.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn err<T: quote::ToTokens, U: fmt::Display>(tokens: T, msg: U) -> syn::Error {
|
||||||
|
syn::Error::new_spanned(tokens, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn join(p: &syn::Path, next: &str) -> syn::Path {
|
||||||
|
let ident = quote::format_ident!("{next}");
|
||||||
|
syn::parse_quote!(#p :: #ident)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn comma_separated(
|
||||||
|
ts: parse::ParseStream,
|
||||||
|
mut parse: impl FnMut(parse::ParseStream) -> syn::Result<()>,
|
||||||
|
) -> syn::Result<()> {
|
||||||
|
loop {
|
||||||
|
if ts.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(ts)?;
|
||||||
|
|
||||||
|
if !try_next(ts) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
146
src/array.rs
Normal file
146
src/array.rs
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
//! # Array utilities and types.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
borrow::{Borrow, Cow},
|
||||||
|
fmt,
|
||||||
|
ops::Deref,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use eva_macros::data;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize, de};
|
||||||
|
|
||||||
|
#[data(copy, error, display("too big, expected array no bigger than {LIMIT}"), crate = crate)]
|
||||||
|
pub struct TooBig<const LIMIT: usize>;
|
||||||
|
|
||||||
|
#[crate::perfect_derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ImmutableHeap<T, const MAX: usize>(Arc<[T]>);
|
||||||
|
|
||||||
|
impl<T, const MAX: usize> Borrow<[T]> for ImmutableHeap<T, MAX> {
|
||||||
|
fn borrow(&self) -> &[T] {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const MAX: usize> AsRef<[T]> for ImmutableHeap<T, MAX> {
|
||||||
|
fn as_ref(&self) -> &[T] {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const MAX: usize> Deref for ImmutableHeap<T, MAX> {
|
||||||
|
type Target = [T];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, const MAX: usize> IntoIterator for &'a ImmutableHeap<T, MAX> {
|
||||||
|
type IntoIter = <&'a [T] as IntoIterator>::IntoIter;
|
||||||
|
type Item = &'a T;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const MAX: usize> TryFrom<Vec<T>> for ImmutableHeap<T, MAX> {
|
||||||
|
type Error = TooBig<MAX>;
|
||||||
|
|
||||||
|
fn try_from(value: Vec<T>) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() > MAX {
|
||||||
|
return Err(TooBig::<MAX>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(value.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const MAX: usize, const N: usize> TryFrom<[T; N]> for ImmutableHeap<T, MAX> {
|
||||||
|
type Error = TooBig<MAX>;
|
||||||
|
|
||||||
|
fn try_from(value: [T; N]) -> Result<Self, Self::Error> {
|
||||||
|
if N > MAX {
|
||||||
|
return Err(TooBig::<MAX>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(Arc::new(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, T: Deserialize<'de>, const MAX: usize> Deserialize<'de> for ImmutableHeap<T, MAX> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct Visitor<T, const MAX: usize> {
|
||||||
|
v: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d, T: Deserialize<'d>, const MAX: usize> de::Visitor<'d> for Visitor<T, MAX> {
|
||||||
|
type Value = Vec<T>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(formatter, "array no bigger than {MAX}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: de::SeqAccess<'d>,
|
||||||
|
{
|
||||||
|
let mut dst = self.v;
|
||||||
|
let mut fuel = MAX;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if fuel <= 0 {
|
||||||
|
return Err(de::Error::custom(TooBig::<MAX>));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(element) = seq.next_element()? else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
dst.push(element);
|
||||||
|
fuel -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = deserializer.deserialize_seq(Visitor::<T, MAX> {
|
||||||
|
v: Vec::with_capacity(MAX),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self(v.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize, const MAX: usize> Serialize for ImmutableHeap<T, MAX> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.0.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: JsonSchema, const MAX: usize> JsonSchema for ImmutableHeap<T, MAX> {
|
||||||
|
fn schema_id() -> Cow<'static, str> {
|
||||||
|
<Vec<T> as JsonSchema>::schema_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_name() -> Cow<'static, str> {
|
||||||
|
<Vec<T> as JsonSchema>::schema_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
|
schemars::json_schema!({
|
||||||
|
"type": "array",
|
||||||
|
"items": T::json_schema(generator),
|
||||||
|
"maxItems": MAX,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
6
src/collections.rs
Normal file
6
src/collections.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub use hashbrown::{Equivalent, HashTable, hash_map, hash_table};
|
||||||
|
|
||||||
|
use std::hash::BuildHasherDefault;
|
||||||
|
|
||||||
|
pub type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<crate::hash::Hasher>>;
|
||||||
|
pub type HashSet<V> = hashbrown::HashSet<V, BuildHasherDefault<crate::hash::Hasher>>;
|
1
src/encoding.rs
Normal file
1
src/encoding.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod dict;
|
28
src/encoding/dict.rs
Normal file
28
src/encoding/dict.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use crate::data;
|
||||||
|
|
||||||
|
#[data(copy, ord, not(serde), crate = crate)]
|
||||||
|
pub struct Dict<'a, T>(pub &'a [T]);
|
||||||
|
|
||||||
|
impl<'a, T> Dict<'a, T> {
|
||||||
|
pub const fn get_encoded_size(&self, of: u64) -> usize {
|
||||||
|
if of == 0 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
of.ilog(self.0.len() as u64) as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self, mut value: u64, mut put: impl FnMut(&T)) {
|
||||||
|
let base = self.0.len() as u64;
|
||||||
|
loop {
|
||||||
|
let rem = value % base;
|
||||||
|
value /= base;
|
||||||
|
let idx = rem as usize;
|
||||||
|
put(&self.0[idx]);
|
||||||
|
|
||||||
|
if value == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
src/error.rs
Normal file
69
src/error.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
//! # Error utilities.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
mod seal {
|
||||||
|
pub trait Seal {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use crate::_combined as combined;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
#[doc(hidden)]
|
||||||
|
macro_rules! _combined {
|
||||||
|
($($(#[$outer_meta:meta])* $vis:vis enum $name:ident {
|
||||||
|
$( $VarName:ident($ty:ty) ),* $(,)?
|
||||||
|
})*) => {$(
|
||||||
|
$(#[$outer_meta])*
|
||||||
|
$vis enum $name {$(
|
||||||
|
$VarName($ty)
|
||||||
|
),*}
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
pub fn transmogrify<T>(self) -> T
|
||||||
|
where
|
||||||
|
T: $crate::Anything $(+ std::convert::From<$ty>)*
|
||||||
|
{
|
||||||
|
match self {$(
|
||||||
|
Self::$VarName(v) => v.into()
|
||||||
|
),*}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(
|
||||||
|
impl std::convert::From<$ty> for $name {
|
||||||
|
fn from(v: $ty) -> Self {
|
||||||
|
Self::$VarName(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
)*};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indicate that error is highly unlikely.
|
||||||
|
#[track_caller]
|
||||||
|
pub const fn shit_happens() -> ! {
|
||||||
|
panic!("shit happens")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ShitHappens<T>: seal::Seal {
|
||||||
|
/// Same as [`shit_happens`], but for unwrapping errors.
|
||||||
|
fn shit_happens(self) -> T;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O> seal::Seal for Option<O> {}
|
||||||
|
impl<T> ShitHappens<T> for Option<T> {
|
||||||
|
#[track_caller]
|
||||||
|
fn shit_happens(self) -> T {
|
||||||
|
self.unwrap_or_else(|| shit_happens())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O, E> seal::Seal for Result<O, E> {}
|
||||||
|
impl<O, E: fmt::Debug> ShitHappens<O> for Result<O, E> {
|
||||||
|
#[track_caller]
|
||||||
|
fn shit_happens(self) -> O {
|
||||||
|
self.expect("shit happens")
|
||||||
|
}
|
||||||
|
}
|
22
src/fut.rs
Normal file
22
src/fut.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use std::{
|
||||||
|
marker::PhantomData,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Future which is never ready.
|
||||||
|
#[crate::perfect_derive(Debug, Clone, Default, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Never<T>(PhantomData<T>);
|
||||||
|
|
||||||
|
impl<T> Future for Never<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::trait_set! {
|
||||||
|
/// Future + Send.
|
||||||
|
pub trait Fut = Future + Send;
|
||||||
|
}
|
22
src/generic.rs
Normal file
22
src/generic.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use crate::data;
|
||||||
|
|
||||||
|
pub trait Anything {}
|
||||||
|
impl<T: ?Sized> Anything for T {}
|
||||||
|
|
||||||
|
// TODO: include all possible ranges.
|
||||||
|
#[data(
|
||||||
|
copy,
|
||||||
|
error,
|
||||||
|
display("integer out of range"),
|
||||||
|
crate = crate
|
||||||
|
)]
|
||||||
|
pub struct OutOfRange;
|
||||||
|
|
||||||
|
/// Text case.
|
||||||
|
#[data(copy, crate = crate, display(name))]
|
||||||
|
pub enum Case {
|
||||||
|
Snake,
|
||||||
|
Pascal,
|
||||||
|
Kebab,
|
||||||
|
Camel,
|
||||||
|
}
|
13
src/handling/and_then.rs
Normal file
13
src/handling/and_then.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::data;
|
||||||
|
|
||||||
|
#[data(copy, ord, crate = crate)]
|
||||||
|
pub struct AndThen<L, R> {
|
||||||
|
pub lhs: L,
|
||||||
|
pub rhs: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> AndThen<L, R> {
|
||||||
|
pub const fn new(lhs: L, rhs: R) -> Self {
|
||||||
|
Self { lhs, rhs }
|
||||||
|
}
|
||||||
|
}
|
29
src/handling/apply.rs
Normal file
29
src/handling/apply.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use crate::{
|
||||||
|
data,
|
||||||
|
fut::Fut,
|
||||||
|
handling::{Endpoint, Handler},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[data(copy, ord, crate = crate)]
|
||||||
|
pub struct Apply<L, R> {
|
||||||
|
pub lhs: L,
|
||||||
|
pub rhs: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> Apply<L, R> {
|
||||||
|
pub const fn new(lhs: L, rhs: R) -> Self {
|
||||||
|
Self { lhs, rhs }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, S, L, R, Output> Endpoint<I, S> for Apply<L, R>
|
||||||
|
where
|
||||||
|
L: for<'a> Handler<I, S, &'a R, Output = Output>,
|
||||||
|
R: Send + Sync,
|
||||||
|
{
|
||||||
|
type Output = Output;
|
||||||
|
|
||||||
|
fn call(&self, state: S, in_: I) -> impl Fut<Output = Self::Output> {
|
||||||
|
self.lhs.call(state, in_, &self.rhs)
|
||||||
|
}
|
||||||
|
}
|
37
src/handling/mod.rs
Normal file
37
src/handling/mod.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::{auto_impl, fut::Fut};
|
||||||
|
|
||||||
|
pub use self::{and_then::AndThen, apply::Apply, then::Then};
|
||||||
|
|
||||||
|
mod and_then;
|
||||||
|
mod apply;
|
||||||
|
mod then;
|
||||||
|
|
||||||
|
pub trait HandlerExt: Sized {
|
||||||
|
fn then<R>(self, rhs: R) -> Then<Self, R> {
|
||||||
|
Then::new(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> HandlerExt for T {}
|
||||||
|
|
||||||
|
pub trait EndpointExt: Sized {
|
||||||
|
fn apply<R>(self, rhs: R) -> Apply<Self, R> {
|
||||||
|
Apply::new(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> EndpointExt for T {}
|
||||||
|
|
||||||
|
#[auto_impl(&, &mut, Arc, Box)]
|
||||||
|
pub trait Endpoint<I, S>: Send + Sync {
|
||||||
|
type Output;
|
||||||
|
|
||||||
|
fn call(&self, state: S, in_: I) -> impl Fut<Output = Self::Output>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_impl(&, &mut, Arc, Box)]
|
||||||
|
pub trait Handler<I, S, N>: Send + Sync {
|
||||||
|
type Output;
|
||||||
|
|
||||||
|
fn call(&self, state: S, in_: I, next: N) -> impl Fut<Output = Self::Output>;
|
||||||
|
}
|
1
src/handling/provide_state.rs
Normal file
1
src/handling/provide_state.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
29
src/handling/then.rs
Normal file
29
src/handling/then.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use crate::{
|
||||||
|
data,
|
||||||
|
fut::Fut,
|
||||||
|
handling::{Apply, Handler},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[data(copy, ord, crate = crate)]
|
||||||
|
pub struct Then<L, R> {
|
||||||
|
pub lhs: L,
|
||||||
|
pub rhs: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, S, L, R, N, Output> Handler<I, S, N> for Then<L, R>
|
||||||
|
where
|
||||||
|
R: Send + Sync,
|
||||||
|
L: for<'a> Handler<I, S, Apply<&'a R, N>, Output = Output>,
|
||||||
|
{
|
||||||
|
type Output = Output;
|
||||||
|
|
||||||
|
fn call(&self, state: S, in_: I, next: N) -> impl Fut<Output = Self::Output> {
|
||||||
|
self.lhs.call(state, in_, Apply::new(&self.rhs, next))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> Then<L, R> {
|
||||||
|
pub const fn new(lhs: L, rhs: R) -> Self {
|
||||||
|
Self { lhs, rhs }
|
||||||
|
}
|
||||||
|
}
|
1
src/hash.rs
Normal file
1
src/hash.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub type Hasher = ahash::AHasher;
|
59
src/lib.rs
Normal file
59
src/lib.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! zst_error {
|
||||||
|
($($tt:tt)*) => {{
|
||||||
|
#[$crate::data(
|
||||||
|
copy,
|
||||||
|
crate = $crate,
|
||||||
|
not(serde, schemars),
|
||||||
|
display($($tt)*),
|
||||||
|
)]
|
||||||
|
struct E;
|
||||||
|
|
||||||
|
E
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The trait that is implemented for everything.
|
||||||
|
pub trait Anything {}
|
||||||
|
impl<T: ?Sized> Anything for T {}
|
||||||
|
|
||||||
|
pub use bytes;
|
||||||
|
pub use bytesize;
|
||||||
|
pub use url;
|
||||||
|
|
||||||
|
pub use eva_macros::{data, endpoint, int, str};
|
||||||
|
pub use seq_macro::seq;
|
||||||
|
|
||||||
|
pub use auto_impl::auto_impl;
|
||||||
|
pub use perfect_derive::perfect_derive;
|
||||||
|
pub use trait_set::trait_set;
|
||||||
|
|
||||||
|
pub mod array;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub mod fut;
|
||||||
|
pub mod time;
|
||||||
|
|
||||||
|
pub mod sync;
|
||||||
|
pub mod trace_id;
|
||||||
|
|
||||||
|
pub mod generic;
|
||||||
|
pub mod slab;
|
||||||
|
pub mod str;
|
||||||
|
|
||||||
|
pub mod encoding;
|
||||||
|
pub mod rand;
|
||||||
|
|
||||||
|
pub mod collections;
|
||||||
|
pub mod handling;
|
||||||
|
pub mod hash;
|
||||||
|
|
||||||
|
pub use paste::paste;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod _priv {
|
||||||
|
pub use schemars;
|
||||||
|
pub use serde;
|
||||||
|
|
||||||
|
pub use eva_macros::RastGawno;
|
||||||
|
}
|
4
src/rand.rs
Normal file
4
src/rand.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
///! # Random utilities, refer to [`::rand`] crate docs.
|
||||||
|
pub use ::rand::*;
|
||||||
|
|
||||||
|
pub use rand_xoshiro as xoshiro;
|
1
src/slab.rs
Normal file
1
src/slab.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
212
src/str.rs
Normal file
212
src/str.rs
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
use std::{fmt, mem::MaybeUninit, slice, str::FromStr};
|
||||||
|
|
||||||
|
pub use compact_str::{
|
||||||
|
CompactString, CompactStringExt, ToCompactString, ToCompactStringError, format_compact,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::data;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! single_ascii_char {
|
||||||
|
($ty:ty) => {
|
||||||
|
const _: () = {
|
||||||
|
use ::core::{result::Result, str::FromStr};
|
||||||
|
use ::std::string::String;
|
||||||
|
|
||||||
|
use $crate::str::{HasPattern, ParseError};
|
||||||
|
|
||||||
|
impl HasPattern for $ty {
|
||||||
|
#[inline]
|
||||||
|
fn pat_into(buf: &mut String) {
|
||||||
|
$crate::push_ascii_pat!(Self, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for $ty {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let [c]: [u8; 1] = s.as_bytes().try_into().map_err(|_| ParseError::Length)?;
|
||||||
|
Self::new(c).ok_or(ParseError::Char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple parse error.
|
||||||
|
#[data(copy, error, display(doc), crate = crate)]
|
||||||
|
pub enum ParseError {
|
||||||
|
/// Unexpected char.
|
||||||
|
Char,
|
||||||
|
/// Invalid length of string.
|
||||||
|
Length,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FixedParseError {
|
||||||
|
fn length(expected: usize) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedParseError for ParseError {
|
||||||
|
#[inline]
|
||||||
|
fn length(expected: usize) -> Self {
|
||||||
|
_ = expected;
|
||||||
|
Self::Length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasPattern {
|
||||||
|
fn pat_into(buf: &mut String);
|
||||||
|
#[inline]
|
||||||
|
fn regex_pat() -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
Self::pat_into(&mut s);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn regex_pat_fullmatch() -> String {
|
||||||
|
let mut s = String::with_capacity(8);
|
||||||
|
s.push('^');
|
||||||
|
Self::pat_into(&mut s);
|
||||||
|
s.push('$');
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasPattern for String {
|
||||||
|
#[inline]
|
||||||
|
fn pat_into(buf: &mut String) {
|
||||||
|
buf.push_str(".*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasPattern for CompactString {
|
||||||
|
#[inline]
|
||||||
|
fn pat_into(buf: &mut String) {
|
||||||
|
buf.push_str(".*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// Implementation of this trait implies that reinterpreting the reference to the type as &[u8] and then as valid utf8 sequence
|
||||||
|
/// is sound and defined.
|
||||||
|
pub unsafe trait FixedUtf8
|
||||||
|
where
|
||||||
|
Self: Sized + Copy + FromStr<Err: FixedParseError + fmt::Display> + HasPattern,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reinterpret fixed size string as a standard library string slice.
|
||||||
|
pub const fn reinterpret<'a, T: FixedUtf8>(val: &'a T) -> &'a str {
|
||||||
|
let ts = size_of::<T>();
|
||||||
|
let slice: &'a [u8] = unsafe { slice::from_raw_parts(val as *const T as *const u8, ts) };
|
||||||
|
unsafe { std::str::from_utf8_unchecked(slice) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
union EitherUnion<L: FixedUtf8, R: FixedUtf8> {
|
||||||
|
lhs: L,
|
||||||
|
rhs: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Either<L: FixedUtf8, R: FixedUtf8>(EitherUnion<L, R>);
|
||||||
|
|
||||||
|
impl<L: FixedUtf8, R: FixedUtf8> FromStr for Either<L, R> {
|
||||||
|
type Err = R::Err;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(res) = L::from_str(s) {
|
||||||
|
Ok(Self::left(res))
|
||||||
|
} else {
|
||||||
|
R::from_str(s).map(Self::right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<L: FixedUtf8, R: FixedUtf8> FixedUtf8 for Either<L, R> {}
|
||||||
|
|
||||||
|
impl<L: FixedUtf8, R: FixedUtf8> HasPattern for Either<L, R> {
|
||||||
|
fn pat_into(buf: &mut String) {
|
||||||
|
buf.push('(');
|
||||||
|
L::pat_into(buf);
|
||||||
|
buf.push('|');
|
||||||
|
R::pat_into(buf);
|
||||||
|
buf.push(')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L: FixedUtf8, R: FixedUtf8> Either<L, R> {
|
||||||
|
const fn new(un: EitherUnion<L, R>) -> Self {
|
||||||
|
const {
|
||||||
|
if size_of::<L>() != size_of::<R>() {
|
||||||
|
panic!("Could not make string `Either` of differently sized strings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(un)
|
||||||
|
}
|
||||||
|
pub const fn left(lhs: L) -> Self {
|
||||||
|
Self::new(EitherUnion { lhs })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn right(rhs: R) -> Self {
|
||||||
|
Self::new(EitherUnion { rhs })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_str(&self) -> &str {
|
||||||
|
reinterpret(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sequence of fixed size string.
|
||||||
|
#[crate::str(custom, copy, crate = crate)]
|
||||||
|
pub struct Seq<const N: usize, T: FixedUtf8>(pub [T; N]);
|
||||||
|
|
||||||
|
impl<const N: usize, T: FixedUtf8> Seq<N, T> {
|
||||||
|
pub const fn as_str(&self) -> &str {
|
||||||
|
reinterpret(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize, T: FixedUtf8> HasPattern for Seq<N, T> {
|
||||||
|
fn pat_into(buf: &mut String) {
|
||||||
|
T::pat_into(buf);
|
||||||
|
buf.push_str("{");
|
||||||
|
buf.push_str(&N.to_string());
|
||||||
|
buf.push_str("}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<const N: usize, T: FixedUtf8> FixedUtf8 for Seq<N, T> {}
|
||||||
|
|
||||||
|
impl<const N: usize, T> FromStr for Seq<N, T>
|
||||||
|
where
|
||||||
|
T: FixedUtf8,
|
||||||
|
{
|
||||||
|
type Err = T::Err;
|
||||||
|
|
||||||
|
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let expected_size = size_of::<T>() * N;
|
||||||
|
if s.len() != expected_size {
|
||||||
|
return Err(T::Err::length(expected_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut arr: [MaybeUninit<T>; N] = unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
|
for idx in 0..N {
|
||||||
|
let len = size_of::<T>();
|
||||||
|
let chunk = unsafe { s.get_unchecked(..len) };
|
||||||
|
let res = T::from_str(chunk)?;
|
||||||
|
unsafe { arr.get_unchecked_mut(idx) }.write(res);
|
||||||
|
|
||||||
|
s = unsafe { s.get_unchecked(len..) };
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(unsafe { std::ptr::read((&raw const arr).cast()) }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod ascii;
|
109
src/str/ascii.rs
Normal file
109
src/str/ascii.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use crate::int;
|
||||||
|
|
||||||
|
pub fn push_ascii_pat(u: u8, to: &mut String) {
|
||||||
|
let push = match u {
|
||||||
|
b'\r' => r"\r",
|
||||||
|
b'\n' => r"\n",
|
||||||
|
b'-' => r"\-",
|
||||||
|
b'^' => r"\^",
|
||||||
|
b'$' => r"\$",
|
||||||
|
b'[' => r"\[",
|
||||||
|
b']' => r"\]",
|
||||||
|
b'\\' => r"\",
|
||||||
|
b'.' => r"\.",
|
||||||
|
b'*' => r"\*",
|
||||||
|
b'+' => r"\+",
|
||||||
|
b'?' => r"\?",
|
||||||
|
b'{' => r"\{",
|
||||||
|
b'}' => r"\}",
|
||||||
|
b'|' => r"\|",
|
||||||
|
b'(' => r"\(",
|
||||||
|
b')' => r"\)",
|
||||||
|
_ => {
|
||||||
|
to.push(u as char);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
to.push_str(push);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! push_ascii_pat {
|
||||||
|
($e:ident, $to:expr) => {{
|
||||||
|
let buf = $to;
|
||||||
|
let requires_brackets = match $e::RANGES.as_slice() {
|
||||||
|
[r] => r.len() > 1,
|
||||||
|
[] => false,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
if requires_brackets {
|
||||||
|
buf.push('[');
|
||||||
|
}
|
||||||
|
for range in $e::RANGES.iter().cloned() {
|
||||||
|
let start = *range.start();
|
||||||
|
let end = *range.end();
|
||||||
|
$crate::str::ascii::push_ascii_pat(start, buf);
|
||||||
|
if start != end {
|
||||||
|
buf.push('-');
|
||||||
|
$crate::str::ascii::push_ascii_pat(end, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requires_brackets {
|
||||||
|
buf.push(']');
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! valid {
|
||||||
|
($e:ident) => {
|
||||||
|
$crate::single_ascii_char!($e);
|
||||||
|
|
||||||
|
unsafe impl $crate::str::FixedUtf8 for $e {}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl $e {
|
||||||
|
pub const fn as_str(&self) -> &str {
|
||||||
|
$crate::str::reinterpret(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use valid;
|
||||||
|
|
||||||
|
#[int(u8, b'.', crate = crate)]
|
||||||
|
pub enum Dot {}
|
||||||
|
valid!(Dot);
|
||||||
|
|
||||||
|
#[int(u8, b':', crate = crate)]
|
||||||
|
pub enum Colon {}
|
||||||
|
valid!(Colon);
|
||||||
|
|
||||||
|
#[int(u8, b' ', crate = crate)]
|
||||||
|
pub enum Space {}
|
||||||
|
valid!(Space);
|
||||||
|
|
||||||
|
#[int(u8, b'0'..=b'9', crate = crate)]
|
||||||
|
pub enum Digit {}
|
||||||
|
valid!(Digit);
|
||||||
|
|
||||||
|
impl Digit {
|
||||||
|
pub const fn parse(self) -> u8 {
|
||||||
|
self as u8 - b'0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A valid ASCII character.
|
||||||
|
#[int(u8, 0..=177, crate = crate)]
|
||||||
|
pub enum Char {}
|
||||||
|
valid!(Char);
|
||||||
|
|
||||||
|
/// Printable ASCII character.
|
||||||
|
#[int(u8, 32..=126, crate = crate)]
|
||||||
|
pub enum Printable {}
|
||||||
|
valid!(Printable);
|
||||||
|
|
||||||
|
/// ASCII control character.
|
||||||
|
#[int(u8, 0..=31, crate = crate)]
|
||||||
|
pub enum Control {}
|
||||||
|
valid!(Control);
|
1
src/sync.rs
Normal file
1
src/sync.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
62
src/time/clock.rs
Normal file
62
src/time/clock.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use std::{
|
||||||
|
num::NonZeroU64,
|
||||||
|
sync::{Arc, atomic},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::time::Timestamp;
|
||||||
|
|
||||||
|
#[crate::auto_impl(&, &mut)]
|
||||||
|
pub trait Clock {
|
||||||
|
fn get(&self) -> Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Mock(Arc<atomic::AtomicU64>);
|
||||||
|
|
||||||
|
impl Default for Mock {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Arc::new(atomic::AtomicU64::new(
|
||||||
|
Timestamp::TEST_ORIGIN.as_nanos().get(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mock {
|
||||||
|
pub fn advance(&mut self, dur: Duration) {
|
||||||
|
self.0
|
||||||
|
.fetch_add(dur.as_nanos() as u64, atomic::Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, ts: Timestamp) {
|
||||||
|
self.0.store(ts.as_nanos().get(), atomic::Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn back(&self, dur: Duration) {
|
||||||
|
_ = dur;
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clock for Mock {
|
||||||
|
fn get(&self) -> Timestamp {
|
||||||
|
let time = self.0.load(atomic::Ordering::Acquire);
|
||||||
|
Timestamp::from_nanos(time.try_into().expect("invalid time set"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Real time clock.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct RealTime(());
|
||||||
|
|
||||||
|
impl Clock for RealTime {
|
||||||
|
fn get(&self) -> Timestamp {
|
||||||
|
// TODO: get time with a fixed time zone.
|
||||||
|
let dur = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap();
|
||||||
|
let nanos = NonZeroU64::new(dur.as_nanos() as u64).unwrap();
|
||||||
|
|
||||||
|
Timestamp::from_nanos(nanos)
|
||||||
|
}
|
||||||
|
}
|
526
src/time/date.rs
Normal file
526
src/time/date.rs
Normal file
|
@ -0,0 +1,526 @@
|
||||||
|
use std::{borrow::Cow, mem, str::FromStr};
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use seq_macro::seq;
|
||||||
|
use serde::{Deserialize, Serialize, de};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data, int,
|
||||||
|
str::{HasPattern, ParseError},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::str;
|
||||||
|
|
||||||
|
#[data(ord, copy, display(name), crate = crate)]
|
||||||
|
pub enum Leapness {
|
||||||
|
Leap = 1,
|
||||||
|
Ordinary = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Leapness {
|
||||||
|
pub const fn is_leap(self) -> bool {
|
||||||
|
matches!(self, Self::Leap)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_ordinary(self) -> bool {
|
||||||
|
matches!(self, Self::Ordinary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[data(copy, ord, not(serde, schemars), crate = crate)]
|
||||||
|
pub struct LooseDate {
|
||||||
|
day: Day,
|
||||||
|
month: Month,
|
||||||
|
year: Year,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonSchema for LooseDate {
|
||||||
|
fn schema_id() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed(concat!(module_path!(), "::LooseDate"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_name() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed("LooseDate")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
|
schemars::json_schema!({
|
||||||
|
"type": "string",
|
||||||
|
"pattern": str::DateStr::regex_pat_fullmatch(),
|
||||||
|
"description": "Day, month and year"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for LooseDate {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = <&'de str as Deserialize<'de>>::deserialize(deserializer)?;
|
||||||
|
let date: str::DateStr = s.parse().map_err(de::Error::custom)?;
|
||||||
|
|
||||||
|
date.parse().ok_or(de::Error::custom(zst_error!(
|
||||||
|
"invalid day, month and year combination"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for LooseDate {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.to_str().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LooseDate {
|
||||||
|
pub const fn from_dmy(day: Day, month: Month, year: Year) -> Option<Self> {
|
||||||
|
let last_day = month.last_day(year.leapness());
|
||||||
|
if day as u8 > last_day as u8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self { day, month, year })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get day of month.
|
||||||
|
pub const fn day_of_month(self) -> Day {
|
||||||
|
self.day
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current month.
|
||||||
|
pub const fn month(self) -> Month {
|
||||||
|
self.month
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get year.
|
||||||
|
pub const fn year(self) -> Year {
|
||||||
|
self.year
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert date to more compact representation.
|
||||||
|
pub const fn compact(self) -> Date {
|
||||||
|
let year = self.year();
|
||||||
|
let leapness = year.leapness();
|
||||||
|
|
||||||
|
let first_day_of_year = year.first_day().days();
|
||||||
|
let year_offset = self.month().days_from_year_start(leapness);
|
||||||
|
let month_offset = self.day_of_month() as u16 - 1;
|
||||||
|
|
||||||
|
Date::from_days(first_day_of_year + year_offset + month_offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert date to string.
|
||||||
|
pub const fn to_str(self) -> str::DateStr {
|
||||||
|
str::DateStr::new(
|
||||||
|
self.day_of_month().to_str(),
|
||||||
|
self.month().to_str(),
|
||||||
|
self.year().to_str(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[data(copy, ord, not(serde, schemars), crate = crate)]
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub struct Date(u16);
|
||||||
|
|
||||||
|
impl JsonSchema for Date {
|
||||||
|
fn schema_id() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed(concat!(module_path!(), "::Date"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_name() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed("Date")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
|
schemars::json_schema!({
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 65535,
|
||||||
|
"description": "number of days since 1970, only in binary formats"
|
||||||
|
},
|
||||||
|
LooseDate::json_schema(generator)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Date {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
str::DateStr::from_str(s)?
|
||||||
|
.parse()
|
||||||
|
.map(|d| d.compact())
|
||||||
|
.ok_or(ParseError::Char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Date {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
if deserializer.is_human_readable() {
|
||||||
|
Ok(Date::from_days(u16::deserialize(deserializer)?))
|
||||||
|
} else {
|
||||||
|
let loose = LooseDate::deserialize(deserializer)?;
|
||||||
|
Ok(loose.compact())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Date {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
if serializer.is_human_readable() {
|
||||||
|
self.to_str().serialize(serializer)
|
||||||
|
} else {
|
||||||
|
self.days().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Date {
|
||||||
|
pub const MIN: Self = Self(0);
|
||||||
|
pub const MAX: Self = Self(u16::MAX);
|
||||||
|
|
||||||
|
pub const fn from_dmy(day: Day, month: Month, year: Year) -> Option<Self> {
|
||||||
|
let Some(loosen) = LooseDate::from_dmy(day, month, year) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(loosen.compact())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_secs(self) -> u64 {
|
||||||
|
let hours = (self.0 as u64) * 24;
|
||||||
|
let mins = hours * 60;
|
||||||
|
|
||||||
|
mins * 60
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create date from number of days.
|
||||||
|
pub const fn from_days(days: u16) -> Self {
|
||||||
|
Self(days)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of days.
|
||||||
|
pub const fn days(self) -> u16 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loosen up layout to less compact one.
|
||||||
|
pub const fn loose(self) -> LooseDate {
|
||||||
|
let year = self.year();
|
||||||
|
let total_days = self.days();
|
||||||
|
let leapness = year.leapness();
|
||||||
|
let days_since_year = total_days - year.first_day().days();
|
||||||
|
|
||||||
|
let month = unsafe {
|
||||||
|
Month::from_days_since_year_start(days_since_year, leapness).unwrap_unchecked()
|
||||||
|
};
|
||||||
|
let day_of_month = days_since_year - month.days_from_year_start(leapness);
|
||||||
|
|
||||||
|
LooseDate {
|
||||||
|
// SAFETY: safe, since condition for exiting loop is falling
|
||||||
|
// into days range.
|
||||||
|
day: unsafe { Day::new_unchecked(day_of_month as u8 + 1) },
|
||||||
|
month,
|
||||||
|
year,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current year.
|
||||||
|
pub const fn year(self) -> Year {
|
||||||
|
let days = self.days();
|
||||||
|
if days == 0 {
|
||||||
|
return Year::MIN;
|
||||||
|
}
|
||||||
|
let naive_year = days / 365;
|
||||||
|
let leap_days = super::utils::leap_days_after((naive_year + Year::ORIGIN) - 1)
|
||||||
|
- Year::LEAP_DAYS_BEFORE1970;
|
||||||
|
// naive_year includes leap days from the previous years! Skewing
|
||||||
|
// the calculations! This nigger should be rape-fixed!
|
||||||
|
let fixed = days - leap_days;
|
||||||
|
|
||||||
|
debug_assert!((fixed / 365) < 179);
|
||||||
|
unsafe { Year::new_unchecked((fixed / 365) as u8) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of days since the current year. I.e. if it's
|
||||||
|
/// January 1st -> result would be 0.
|
||||||
|
pub const fn days_since_year(self) -> u16 {
|
||||||
|
self.0 - self.year().first_day().days()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current month.
|
||||||
|
pub const fn month(self) -> Month {
|
||||||
|
self.loose().month()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get day of month.
|
||||||
|
pub const fn day_of_month(self) -> Day {
|
||||||
|
self.loose().day_of_month()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert date to string.
|
||||||
|
pub const fn to_str(self) -> str::DateStr {
|
||||||
|
self.loose().to_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[int(u8, 0..179, crate = crate)]
|
||||||
|
pub enum Year {}
|
||||||
|
|
||||||
|
impl Year {
|
||||||
|
pub const MIN: Self = Self::new(0).unwrap();
|
||||||
|
pub const MAX: Self = Self::new(178).unwrap();
|
||||||
|
|
||||||
|
const LEAP_DAYS_BEFORE1970: u16 = super::utils::leap_days_after(1970 - 1);
|
||||||
|
pub const ORIGIN: u16 = 1970;
|
||||||
|
|
||||||
|
pub const fn from_abs(abs: u16) -> Option<Self> {
|
||||||
|
if matches!(abs, 0..Self::ORIGIN) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shifted = abs - Self::ORIGIN;
|
||||||
|
if shifted > Self::MAX as u16 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(unsafe { Year::new_unchecked(shifted as u8) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn leapness(self) -> Leapness {
|
||||||
|
if super::utils::is_leap_year(self.abs()) {
|
||||||
|
Leapness::Leap
|
||||||
|
} else {
|
||||||
|
Leapness::Ordinary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get date of the first day in a year.
|
||||||
|
pub const fn first_day(self) -> Date {
|
||||||
|
let leap_days = super::utils::leap_days_after(self.abs() - 1) - Self::LEAP_DAYS_BEFORE1970;
|
||||||
|
let naive_days = (self as u16) * 365;
|
||||||
|
|
||||||
|
Date::from_days(naive_days + leap_days)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert relative year to absolute year.
|
||||||
|
pub const fn abs(self) -> u16 {
|
||||||
|
self as u16 + Self::ORIGIN
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert year to fixed size string.
|
||||||
|
pub const fn to_str(self) -> str::YearStr {
|
||||||
|
let v = self.abs();
|
||||||
|
let buf = [
|
||||||
|
(v / 1000) as u8 + b'0',
|
||||||
|
(v / 100 % 10) as u8 + b'0',
|
||||||
|
(v / 10 % 10) as u8 + b'0',
|
||||||
|
(v % 10) as u8 + b'0',
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe { mem::transmute::<[u8; 4], str::YearStr>(buf) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[int(u8, 1..=32, crate = crate)]
|
||||||
|
pub enum Day {}
|
||||||
|
|
||||||
|
impl Day {
|
||||||
|
pub const fn first() -> Self {
|
||||||
|
Self::VARIANTS[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get index inside [`Day::VARIANTS`].
|
||||||
|
pub const fn index(self) -> usize {
|
||||||
|
self as usize - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert day to fixed size string.
|
||||||
|
pub const fn to_str(self) -> str::DayStr {
|
||||||
|
let d = self as u8;
|
||||||
|
unsafe { mem::transmute::<[u8; 2], str::DayStr>([d / 10 + b'0', d % 10 + b'0']) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[data(copy, ord, crate = crate, display(name))]
|
||||||
|
pub enum Month {
|
||||||
|
Jan = 0,
|
||||||
|
Feb = 1,
|
||||||
|
Mar = 2,
|
||||||
|
Apr = 3,
|
||||||
|
May = 4,
|
||||||
|
Jun = 5,
|
||||||
|
Jul = 6,
|
||||||
|
Aug = 7,
|
||||||
|
Sep = 8,
|
||||||
|
Oct = 9,
|
||||||
|
Nov = 10,
|
||||||
|
Dec = 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Month {
|
||||||
|
pub const MIN: Self = Self::Jan;
|
||||||
|
pub const MAX: Self = Self::Dec;
|
||||||
|
pub const VARIANTS: [Self; 12] = {
|
||||||
|
use Month::*;
|
||||||
|
|
||||||
|
[Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fn prev(self) -> Self {
|
||||||
|
let Some(prev) = self.prev_checked() else {
|
||||||
|
return Self::MIN;
|
||||||
|
};
|
||||||
|
|
||||||
|
prev
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn prev_checked(self) -> Option<Self> {
|
||||||
|
let Some(prev) = (self as u8).checked_sub(1) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(unsafe { Month::from_repr_unchecked(prev) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn next(self) -> Self {
|
||||||
|
let Some(next) = self.next_checked() else {
|
||||||
|
return Self::MAX;
|
||||||
|
};
|
||||||
|
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn next_checked(self) -> Option<Self> {
|
||||||
|
let repr = self as u8 + 1;
|
||||||
|
Self::from_repr(repr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn from_days_since_year_start_impl(mut days: u16, leapness: Leapness) -> Self {
|
||||||
|
let mut month = Month::Jan;
|
||||||
|
loop {
|
||||||
|
let last_day = month.last_day(leapness);
|
||||||
|
if days < last_day as u16 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
days -= last_day as u16;
|
||||||
|
month = month.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
month
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_days_since_year_start(days: u16, leapness: Leapness) -> Option<Self> {
|
||||||
|
use Leapness::*;
|
||||||
|
const ORDINARY: [Month; 365] = seq!(N in 0..365 {
|
||||||
|
[#(Month::from_days_since_year_start_impl(N, Ordinary),)*]
|
||||||
|
});
|
||||||
|
const LEAP: [Month; 366] = seq!(N in 0..366 {
|
||||||
|
[#(Month::from_days_since_year_start_impl(N, Leap),)*]
|
||||||
|
});
|
||||||
|
|
||||||
|
match leapness {
|
||||||
|
Leap => {
|
||||||
|
if days >= 366 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(LEAP[days as usize])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ordinary => {
|
||||||
|
if days >= 365 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ORDINARY[days as usize])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn days_from_year_start(self, leapness: Leapness) -> u16 {
|
||||||
|
const fn l(m: Month) -> u16 {
|
||||||
|
m.calc_days_from_year_start(Leapness::Leap)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn o(m: Month) -> u16 {
|
||||||
|
m.calc_days_from_year_start(Leapness::Ordinary)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEAP: [u16; 12] = seq!(N in 0..12 {
|
||||||
|
[#(l(Month::VARIANTS[N]),)*]
|
||||||
|
});
|
||||||
|
const ORDINARY: [u16; 12] = seq!(N in 0..12 {
|
||||||
|
[#(o(Month::VARIANTS[N]),)*]
|
||||||
|
});
|
||||||
|
|
||||||
|
match leapness {
|
||||||
|
Leapness::Leap => LEAP[self as usize],
|
||||||
|
Leapness::Ordinary => ORDINARY[self as usize],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn calc_days_from_year_start(self, leapness: Leapness) -> u16 {
|
||||||
|
let mut cur = Self::Jan;
|
||||||
|
let mut days = 0_u16;
|
||||||
|
|
||||||
|
// Sigh, recursion would be better.
|
||||||
|
while cur as u8 != self as u8 {
|
||||||
|
let last_day = cur.last_day(leapness);
|
||||||
|
days += last_day as u16;
|
||||||
|
cur = cur.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
days
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn last_day_naive(self) -> Day {
|
||||||
|
self.last_day(Leapness::Ordinary)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get last day of month with respect to the leapness of year.
|
||||||
|
pub const fn last_day(self, leapness: Leapness) -> Day {
|
||||||
|
use Day::*;
|
||||||
|
const LAST_DAY: [Day; 12] = [
|
||||||
|
POS31, POS28, POS31, POS30, POS31, POS30, POS31, POS31, POS30, POS31, POS30, POS31,
|
||||||
|
];
|
||||||
|
const LAST_LEAP_DAY: [Day; 12] = {
|
||||||
|
let mut src = LAST_DAY;
|
||||||
|
src[1] = POS29;
|
||||||
|
src
|
||||||
|
};
|
||||||
|
const TBL: [[Day; 12]; 2] = [LAST_DAY, LAST_LEAP_DAY];
|
||||||
|
|
||||||
|
TBL[leapness.is_leap() as usize][self as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const unsafe fn from_repr_unchecked(repr: u8) -> Self {
|
||||||
|
unsafe { mem::transmute::<u8, Self>(repr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_repr(repr: u8) -> Option<Self> {
|
||||||
|
match repr {
|
||||||
|
0..12 => Some(unsafe { Self::from_repr_unchecked(repr) }),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert month to fixed size string.
|
||||||
|
pub const fn to_str(self) -> str::MonthStr {
|
||||||
|
let v = self as u8 + 1;
|
||||||
|
unsafe { mem::transmute::<[u8; 2], str::MonthStr>([v / 10 + b'0', v % 10 + b'0']) }
|
||||||
|
}
|
||||||
|
}
|
49
src/time/mod.rs
Normal file
49
src/time/mod.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
//! # Date and time
|
||||||
|
//!
|
||||||
|
//! Contains heavily and precisely typed utilities for working
|
||||||
|
//! with types.
|
||||||
|
//!
|
||||||
|
//! # Glossary
|
||||||
|
//!
|
||||||
|
//! ### [`Clock`]
|
||||||
|
//!
|
||||||
|
//! Trait which is responsible for getting current time.
|
||||||
|
//!
|
||||||
|
//! - [`Mock`] - Mock [`Clock`], can be used for testing purposes
|
||||||
|
//! - [`RealTime`] - realtime clock
|
||||||
|
//!
|
||||||
|
//! ### Date and time types
|
||||||
|
//!
|
||||||
|
//! Main types:
|
||||||
|
//! - [`Date`] - day precise time, contains [`Day`], [`Month`] and [`Year`]
|
||||||
|
//! - [`Time`] - time during day, contains [`Hours`] and [`Mins`]
|
||||||
|
//! - [`SecsTime`] - same as [`Time`], but contains seconds
|
||||||
|
//! - [`PreciseTime`] - same as [`Time`], but contains seconds and nanoseconds
|
||||||
|
//! - [`Timestamp`] - maximum precision timestamp, nanoseconds precision
|
||||||
|
//!
|
||||||
|
//! Every timestamp here return timestamp in the UTC timezone.
|
||||||
|
//!
|
||||||
|
//! ### String representation
|
||||||
|
//!
|
||||||
|
//! Every type presented here have string representation, see [`self::str`] module.
|
||||||
|
|
||||||
|
pub use self::{
|
||||||
|
clock::{Clock, Mock, RealTime},
|
||||||
|
date::{Date, Day, Leapness, LooseDate, Month, Year},
|
||||||
|
time::{Hours, Mins, PreciseTime, Secs, SecsTime, SubsecNanos, Time},
|
||||||
|
timestamp::Timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod ser;
|
||||||
|
pub mod str;
|
||||||
|
pub mod tz;
|
||||||
|
|
||||||
|
mod clock;
|
||||||
|
mod date;
|
||||||
|
mod time;
|
||||||
|
mod timestamp;
|
||||||
|
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
3
src/time/ser.rs
Normal file
3
src/time/ser.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod tz {
|
||||||
|
pub mod msk {}
|
||||||
|
}
|
278
src/time/str.rs
Normal file
278
src/time/str.rs
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
//! # Strings that represent time
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
int, str,
|
||||||
|
str::{ParseError, Seq, ascii},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
Day, Hours, LooseDate, Mins, Month, PreciseTime, Secs, SecsTime, Time, Timestamp, Year,
|
||||||
|
time::SubsecNanos, timestamp::LooseTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
// == TimestampStr ==
|
||||||
|
|
||||||
|
/// Precise string timestamp representation.
|
||||||
|
///
|
||||||
|
/// format: `dd.mm.YYYY HH:MM:SS.NNNNNNNNN`.
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct TimestampStr(DateStr, ascii::Space, PreciseTimeStr);
|
||||||
|
|
||||||
|
impl TimestampStr {
|
||||||
|
pub const fn new(date: DateStr, time: PreciseTimeStr) -> Self {
|
||||||
|
Self(date, ascii::Space::POS32, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn parse_compact(self) -> Option<Timestamp> {
|
||||||
|
let Some(loose) = self.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(loose.compact_loose().compact())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn parse(self) -> Option<LooseTimestamp<LooseDate>> {
|
||||||
|
let Some(date) = self.0.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(time) = self.2.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(LooseTimestamp::<_>::new(date, time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == PreciseTimeStr ==
|
||||||
|
|
||||||
|
/// [`TimeStr`] with seconds an nanoseconds.
|
||||||
|
///
|
||||||
|
/// format: `HH:MM:SS.NNNNNNNNN`, where N is nanosecond digit.
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct PreciseTimeStr(SecsTimeStr, ascii::Dot, SubsecNanosStr);
|
||||||
|
|
||||||
|
impl PreciseTimeStr {
|
||||||
|
pub const fn new(secs_time: SecsTimeStr, subsec_nanos: SubsecNanosStr) -> Self {
|
||||||
|
Self(secs_time, ascii::Dot::POS46, subsec_nanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn parse(self) -> Option<PreciseTime> {
|
||||||
|
let Some(time) = self.0.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nanos = self.2.parse();
|
||||||
|
|
||||||
|
Some(PreciseTime::new(time, nanos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == SecsTimeStr ==
|
||||||
|
|
||||||
|
/// [`TimeStr`] with seconds part.
|
||||||
|
///
|
||||||
|
/// format: `HH:MM:SS`.
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct SecsTimeStr(TimeStr, ascii::Colon, SecsStr);
|
||||||
|
|
||||||
|
impl SecsTimeStr {
|
||||||
|
pub const fn new(time: TimeStr, secs: SecsStr) -> Self {
|
||||||
|
Self(time, ascii::Colon::POS58, secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn parse(self) -> Option<SecsTime> {
|
||||||
|
let Some(time) = self.0.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let secs = self.2.parse();
|
||||||
|
|
||||||
|
Some(SecsTime::new(time, secs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == DateStr ==
|
||||||
|
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct DateStr(DayStr, ascii::Dot, MonthStr, ascii::Dot, YearStr);
|
||||||
|
|
||||||
|
impl DateStr {
|
||||||
|
pub const fn new(day: DayStr, month: MonthStr, year: YearStr) -> Self {
|
||||||
|
Self(day, ascii::Dot::POS46, month, ascii::Dot::POS46, year)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn parse(self) -> Option<LooseDate> {
|
||||||
|
let Some(day) = self.0.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(month) = self.2.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(year) = self.4.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
LooseDate::from_dmy(day, month, year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == TimeStr ==
|
||||||
|
|
||||||
|
/// Partly valid time string. Contains hours and minutes.
|
||||||
|
///
|
||||||
|
/// Format: `HH:MM`.
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct TimeStr(HoursStr, ascii::Colon, MinsStr);
|
||||||
|
|
||||||
|
impl TimeStr {
|
||||||
|
pub const fn new(hours: HoursStr, minutes: MinsStr) -> Self {
|
||||||
|
Self(hours, ascii::Colon::POS58, minutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn parse(self) -> Option<Time> {
|
||||||
|
let Some(hours) = self.0.parse() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let mins = self.2.parse();
|
||||||
|
|
||||||
|
Some(Time::new(hours, mins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == YearStr ==
|
||||||
|
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct YearStr(FirstYearChar, Seq<3, ascii::Digit>);
|
||||||
|
|
||||||
|
impl YearStr {
|
||||||
|
pub const fn parse(self) -> Option<Year> {
|
||||||
|
let mut num = digit(self.0 as u8) as u16 * 1000;
|
||||||
|
let i = self.1.0;
|
||||||
|
num += digit(i[0] as u8) as u16 * 100;
|
||||||
|
num += digit(i[1] as u8) as u16 * 10;
|
||||||
|
num += digit(i[2] as u8) as u16;
|
||||||
|
|
||||||
|
let Some(relative) = num.checked_sub(1970) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Year::new(relative as u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[int(u8, b'1' | b'2', crate = crate)]
|
||||||
|
enum FirstYearChar {}
|
||||||
|
ascii::valid!(FirstYearChar);
|
||||||
|
|
||||||
|
// == MonthStr ==
|
||||||
|
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct MonthStr(FirstMonthChar, ascii::Digit);
|
||||||
|
|
||||||
|
impl MonthStr {
|
||||||
|
pub const fn parse(self) -> Option<Month> {
|
||||||
|
let num = digit(self.0 as u8) * 10 + digit(self.1 as u8);
|
||||||
|
let Some(index) = num.checked_sub(1) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Month::from_repr(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[int(u8, b'0'..=b'1', crate = crate)]
|
||||||
|
enum FirstMonthChar {}
|
||||||
|
ascii::valid!(FirstMonthChar);
|
||||||
|
|
||||||
|
// == DayStr ==
|
||||||
|
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct DayStr(FirstDayChar, ascii::Digit);
|
||||||
|
|
||||||
|
impl DayStr {
|
||||||
|
pub const fn parse(self) -> Option<Day> {
|
||||||
|
let num = digit(self.0 as u8) * 10 + digit(self.1 as u8);
|
||||||
|
Day::new(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[int(u8, b'0'..=b'3', crate = crate)]
|
||||||
|
enum FirstDayChar {}
|
||||||
|
ascii::valid!(FirstDayChar);
|
||||||
|
|
||||||
|
// == SubsecNanosStr ==
|
||||||
|
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct SubsecNanosStr(Seq<9, ascii::Digit>);
|
||||||
|
|
||||||
|
impl SubsecNanosStr {
|
||||||
|
pub const fn parse(self) -> SubsecNanos {
|
||||||
|
let i = self.0.0;
|
||||||
|
let mut n = 0;
|
||||||
|
|
||||||
|
n += digit(i[8] as u8) as u32 * 1_000_000_00;
|
||||||
|
n += digit(i[7] as u8) as u32 * 1_000_000_0;
|
||||||
|
n += digit(i[6] as u8) as u32 * 1_000_000;
|
||||||
|
n += digit(i[5] as u8) as u32 * 1_000_00;
|
||||||
|
n += digit(i[4] as u8) as u32 * 1_000_0;
|
||||||
|
n += digit(i[3] as u8) as u32 * 1_000;
|
||||||
|
n += digit(i[2] as u8) as u32 * 1_00;
|
||||||
|
n += digit(i[1] as u8) as u32 * 10;
|
||||||
|
n += digit(i[0] as u8) as u32;
|
||||||
|
|
||||||
|
unsafe { SubsecNanos::new_unchecked(n) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == SecsStr ==
|
||||||
|
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct SecsStr(FirstSecsChar, ascii::Digit);
|
||||||
|
|
||||||
|
impl SecsStr {
|
||||||
|
pub const fn parse(self) -> Secs {
|
||||||
|
let num = digit(self.0 as u8) * 10 + digit(self.1 as u8);
|
||||||
|
unsafe { Secs::new_unchecked(num) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[int(u8, b'0'..=b'5', crate = crate)]
|
||||||
|
enum FirstSecsChar {}
|
||||||
|
ascii::valid!(FirstSecsChar);
|
||||||
|
|
||||||
|
// == HoursStr ==
|
||||||
|
|
||||||
|
/// Number of hours, two ascii digits.
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct HoursStr(FirstHoursChar, ascii::Digit);
|
||||||
|
|
||||||
|
impl HoursStr {
|
||||||
|
pub const fn parse(self) -> Option<Hours> {
|
||||||
|
let num = digit(self.0 as u8) * 10 + digit(self.1 as u8);
|
||||||
|
Hours::new(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[int(u8, b'0' | b'1' | b'2', crate = crate)]
|
||||||
|
enum FirstHoursChar {}
|
||||||
|
ascii::valid!(FirstHoursChar);
|
||||||
|
|
||||||
|
// == MinsStr ==
|
||||||
|
|
||||||
|
/// Number of minutes, two ascii digits.
|
||||||
|
#[str(fixed(error = ParseError), crate = crate)]
|
||||||
|
pub struct MinsStr(FirstMinsChar, ascii::Digit);
|
||||||
|
|
||||||
|
impl MinsStr {
|
||||||
|
pub const fn parse(self) -> Mins {
|
||||||
|
unsafe { Mins::new_unchecked(digit(self.0 as u8) * 10 + digit(self.1 as u8)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[int(u8, b'0'..=b'5', crate = crate)]
|
||||||
|
enum FirstMinsChar {}
|
||||||
|
ascii::valid!(FirstMinsChar);
|
||||||
|
|
||||||
|
// == Utils ==
|
||||||
|
|
||||||
|
const fn digit(n: u8) -> u8 {
|
||||||
|
n - b'0'
|
||||||
|
}
|
306
src/time/tests.rs
Normal file
306
src/time/tests.rs
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
use super::{
|
||||||
|
Date, Day, Hours, Leapness, LooseDate, Mins, Month, PreciseTime, Secs, SecsTime, SubsecNanos,
|
||||||
|
Time, Timestamp, Year, str,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn each_date(mut f: impl FnMut(LooseDate)) {
|
||||||
|
for year in Year::VARIANTS {
|
||||||
|
for month in Month::VARIANTS {
|
||||||
|
let last_day = month.last_day(year.leapness());
|
||||||
|
for day in Day::VARIANTS.into_iter().take(last_day as usize) {
|
||||||
|
f(LooseDate::from_dmy(day, month, year).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn each_time(mut f: impl FnMut(Time)) {
|
||||||
|
for hours in Hours::VARIANTS {
|
||||||
|
for mins in Mins::VARIANTS {
|
||||||
|
let time = Time::new(hours, mins);
|
||||||
|
f(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod timestamp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "get_time_test")]
|
||||||
|
#[test]
|
||||||
|
fn get_time() {
|
||||||
|
each_date(|date| {
|
||||||
|
let date = date.compact();
|
||||||
|
each_time(|time| {
|
||||||
|
let ts = date.to_secs() + time.to_secs();
|
||||||
|
if ts == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ts = Timestamp::from_secs_checked(ts).unwrap();
|
||||||
|
|
||||||
|
let actual_date = ts.date();
|
||||||
|
let actual_time = ts.time().secs_time().time();
|
||||||
|
|
||||||
|
assert_eq!(actual_date, date);
|
||||||
|
assert_eq!(time, actual_time);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_date_and_back() {
|
||||||
|
each_date(|date| {
|
||||||
|
let date = date.compact();
|
||||||
|
let secs = date.to_secs();
|
||||||
|
let ts = Timestamp::from_secs(secs);
|
||||||
|
|
||||||
|
assert_eq!(ts.date(), date);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod time {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn time(hours: u8, mins: u8) -> Time {
|
||||||
|
Time::new(Hours::new(hours).unwrap(), Mins::new(mins).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_precise() {
|
||||||
|
#[track_caller]
|
||||||
|
fn case(res: &str, t: Time, subsecs: u8, nanos: u32) {
|
||||||
|
let expected = PreciseTime::new(
|
||||||
|
SecsTime::new(t, Secs::new(subsecs).unwrap()),
|
||||||
|
SubsecNanos::new(nanos).unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!(expected.to_str().as_str(), res);
|
||||||
|
}
|
||||||
|
|
||||||
|
case("15:00:00.000000000", time(15, 00), 0, 0);
|
||||||
|
case("15:00:00.148814880", time(15, 00), 0, 148814880);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_secs() {
|
||||||
|
#[track_caller]
|
||||||
|
fn case(res: &str, t: Time, subsecs: u8) {
|
||||||
|
let expected = SecsTime::new(t, Secs::new(subsecs).unwrap());
|
||||||
|
assert_eq!(expected.to_str().as_str(), res);
|
||||||
|
}
|
||||||
|
|
||||||
|
case("15:00:00", time(15, 00), 00);
|
||||||
|
case("15:15:30", time(15, 15), 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_basic() {
|
||||||
|
#[track_caller]
|
||||||
|
fn case(res: &str, t: Time) {
|
||||||
|
let actual = t.to_str();
|
||||||
|
let expected: Time = res.parse().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected.to_str(), actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
case("15:00", time(15, 00));
|
||||||
|
case("23:59", time(23, 59));
|
||||||
|
case("00:00", time(00, 00));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod date {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn year(abs: u16) -> Year {
|
||||||
|
Year::from_abs(abs).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn year_parse() {
|
||||||
|
let year: str::YearStr = "2025".parse().unwrap();
|
||||||
|
let parsed = year.parse().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(parsed.abs(), 2025);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_day_of_year() {
|
||||||
|
for y in Year::VARIANTS {
|
||||||
|
let date = y.first_day();
|
||||||
|
|
||||||
|
assert_eq!(date.day_of_month(), 1);
|
||||||
|
assert_eq!(date.month(), Month::Jan);
|
||||||
|
assert_eq!(date.year(), y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leapness_works_well() {
|
||||||
|
const fn is_leap(y: u16) -> bool {
|
||||||
|
(y % 400 == 0) || (y % 100 != 0 && y % 4 == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for y in Year::VARIANTS {
|
||||||
|
let expected = is_leap(y.abs());
|
||||||
|
let actual = y.leapness().is_leap();
|
||||||
|
|
||||||
|
assert_eq!(expected, actual, "{y} wrongly calculated leapness");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_date() {
|
||||||
|
let date = Date::from_days(0);
|
||||||
|
let y = year(1970);
|
||||||
|
|
||||||
|
assert_eq!(date.to_str().as_str(), "01.01.1970");
|
||||||
|
assert_eq!(y.first_day(), date);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loose_and_compact_identity() {
|
||||||
|
for y in Year::VARIANTS {
|
||||||
|
let leapness = y.leapness();
|
||||||
|
for month in Month::VARIANTS {
|
||||||
|
let last_day = month.last_day(leapness);
|
||||||
|
for day in Day::VARIANTS
|
||||||
|
.into_iter()
|
||||||
|
.take_while(|d| *d as u8 <= last_day as u8)
|
||||||
|
{
|
||||||
|
eprintln!("{}.{}.{}", day, month, y.abs());
|
||||||
|
let dmy = Date::from_dmy(day, month, y).unwrap();
|
||||||
|
assert_eq!(dmy, dmy.loose().compact());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn days_from_year_start() {
|
||||||
|
// TODO: extend tests.
|
||||||
|
assert_eq!(Month::Jan.last_day(Leapness::Ordinary), 31);
|
||||||
|
assert_eq!(Month::Feb.days_from_year_start(Leapness::Ordinary), 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some problematic cases.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dmy_28feb_1970() {
|
||||||
|
let y = year(1970);
|
||||||
|
let m = Month::Feb;
|
||||||
|
let d = Day::new(1).unwrap();
|
||||||
|
|
||||||
|
let date = Date::from_dmy(d, m, y).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(date.year(), y);
|
||||||
|
assert_eq!(date.month(), m);
|
||||||
|
assert_eq!(date.day_of_month(), d);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dmy_1jan_1970() {
|
||||||
|
let y = year(1970);
|
||||||
|
let m = Month::Jan;
|
||||||
|
let d = Day::new(1).unwrap();
|
||||||
|
|
||||||
|
let date = Date::from_dmy(d, m, y).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(date.year(), y);
|
||||||
|
assert_eq!(date.month(), m);
|
||||||
|
assert_eq!(date.day_of_month(), d);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dmy_31dec_1972() {
|
||||||
|
let y = year(1972);
|
||||||
|
let m = Month::Dec;
|
||||||
|
let d = Day::new(31).unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let date = Date::from_dmy(Day::first(), Month::Jan, year(1972)).unwrap();
|
||||||
|
assert_eq!(date.year().abs(), 1972);
|
||||||
|
}
|
||||||
|
|
||||||
|
let date = Date::from_dmy(d, m, y).unwrap();
|
||||||
|
assert_eq!(date.year().abs(), y.abs());
|
||||||
|
assert_eq!(date.month(), m);
|
||||||
|
assert_eq!(date.day_of_month(), d);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dmy_31dec_2000() {
|
||||||
|
let y = year(2000);
|
||||||
|
let m = Month::Dec;
|
||||||
|
let d = Day::new(31).unwrap();
|
||||||
|
|
||||||
|
let date = Date::from_dmy(d, m, y).unwrap();
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"{}.{}.{} = {}.{}.{}",
|
||||||
|
date.day_of_month(),
|
||||||
|
date.month(),
|
||||||
|
date.year().abs(),
|
||||||
|
d,
|
||||||
|
m,
|
||||||
|
y.abs()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(date.year(), y);
|
||||||
|
assert_eq!(date.month(), m);
|
||||||
|
assert_eq!(date.day_of_month(), d);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_dmy() {
|
||||||
|
for y in Year::VARIANTS {
|
||||||
|
for month in Month::VARIANTS {
|
||||||
|
let last_day = month.last_day(y.leapness());
|
||||||
|
for day in Day::VARIANTS
|
||||||
|
.into_iter()
|
||||||
|
.take_while(|d| *d as u8 >= last_day as u8)
|
||||||
|
{
|
||||||
|
let date = Date::from_dmy(day, month, y).expect("got invalid date");
|
||||||
|
|
||||||
|
let real_dom = date.day_of_month();
|
||||||
|
let real_year = date.year();
|
||||||
|
let real_month = date.month();
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"{real_dom} {real_month} {} = {day}.{month}.{}",
|
||||||
|
real_year.abs(),
|
||||||
|
y.abs()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(real_dom, day);
|
||||||
|
assert_eq!(real_month, month);
|
||||||
|
assert_eq!(real_year.abs(), y.abs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format() {
|
||||||
|
use Month::*;
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn case(fmt: &str, dmy: (u8, Month, u16)) {
|
||||||
|
let (d, m, y) = dmy;
|
||||||
|
let d = Day::new(d).unwrap();
|
||||||
|
let y = year(y);
|
||||||
|
let date = Date::from_dmy(d, m, y).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(date.to_str().as_str(), fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
case("01.01.1970", (1, Jan, 1970));
|
||||||
|
case("01.01.2024", (1, Jan, 2024));
|
||||||
|
case("29.09.2005", (29, Sep, 2005));
|
||||||
|
}
|
||||||
|
}
|
234
src/time/time.rs
Normal file
234
src/time/time.rs
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
use std::{mem, str::FromStr};
|
||||||
|
|
||||||
|
use crate::{data, int, str::ParseError, time::str};
|
||||||
|
|
||||||
|
// == PreciseTime ==
|
||||||
|
|
||||||
|
#[data(copy, ord, not(serde, schemars), crate = crate)]
|
||||||
|
pub struct PreciseTime {
|
||||||
|
time: SecsTime,
|
||||||
|
nanos: SubsecNanos,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PreciseTime {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
str::PreciseTimeStr::from_str(s).and_then(|s| s.parse().ok_or(ParseError::Char))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreciseTime {
|
||||||
|
pub const fn new(time: SecsTime, nanos: SubsecNanos) -> Self {
|
||||||
|
Self { time, nanos }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_nanos(self) -> u64 {
|
||||||
|
let secs = self.time.to_secs();
|
||||||
|
self.nanos.into_inner() as u64 + secs * 1_000_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn secs(self) -> Secs {
|
||||||
|
self.secs_time().secs()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn mins(self) -> Mins {
|
||||||
|
self.secs_time().mins()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn hours(self) -> Hours {
|
||||||
|
self.secs_time().hours()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn subsecs_nanos(self) -> SubsecNanos {
|
||||||
|
self.nanos
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn secs_time(self) -> SecsTime {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_str(self) -> str::PreciseTimeStr {
|
||||||
|
str::PreciseTimeStr::new(self.time.to_str(), self.nanos.to_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == SecsTime ==
|
||||||
|
|
||||||
|
#[data(copy, ord, not(serde, schemars), crate = crate)]
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub struct SecsTime {
|
||||||
|
time: Time,
|
||||||
|
secs: Secs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for SecsTime {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
str::SecsTimeStr::from_str(s).and_then(|s| s.parse().ok_or(ParseError::Char))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecsTime {
|
||||||
|
pub const fn to_secs(self) -> u64 {
|
||||||
|
self.time.to_secs() + self.secs.into_inner() as u64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecsTime {
|
||||||
|
pub const fn new(time: Time, secs: Secs) -> Self {
|
||||||
|
Self { time, secs }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn hours(self) -> Hours {
|
||||||
|
self.time().hours()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn mins(self) -> Mins {
|
||||||
|
self.time().mins()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn secs(self) -> Secs {
|
||||||
|
self.secs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn time(self) -> Time {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_str(self) -> str::SecsTimeStr {
|
||||||
|
str::SecsTimeStr::new(self.time.to_str(), self.secs.to_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Time ==
|
||||||
|
|
||||||
|
#[data(copy, ord, not(serde, schemars), crate = crate)]
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub struct Time {
|
||||||
|
hours: Hours,
|
||||||
|
mins: Mins,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Time {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
str::TimeStr::from_str(s).and_then(|s| s.parse().ok_or(ParseError::Char))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Time {
|
||||||
|
pub const fn new(hours: Hours, mins: Mins) -> Self {
|
||||||
|
Self { hours, mins }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_secs(self) -> u64 {
|
||||||
|
(self.hours as u64 * 60 + self.mins as u64) * 60
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn hours(self) -> Hours {
|
||||||
|
self.hours
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn mins(self) -> Mins {
|
||||||
|
self.mins
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_str(self) -> str::TimeStr {
|
||||||
|
let hours = self.hours().to_str();
|
||||||
|
let mins = self.mins().to_str();
|
||||||
|
|
||||||
|
str::TimeStr::new(hours, mins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hours.
|
||||||
|
#[int(u8, 0..24, crate = crate)]
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub enum Hours {}
|
||||||
|
|
||||||
|
impl Hours {
|
||||||
|
pub const fn to_str(self) -> str::HoursStr {
|
||||||
|
let last = (self as u8) % 10;
|
||||||
|
let first = (self as u8) / 10;
|
||||||
|
|
||||||
|
unsafe { mem::transmute::<[u8; 2], str::HoursStr>([b'0' + first, b'0' + last]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minutes.
|
||||||
|
#[int(u8, 0..60, crate = crate)]
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub enum Mins {}
|
||||||
|
|
||||||
|
impl Mins {
|
||||||
|
pub const fn to_str(self) -> str::MinsStr {
|
||||||
|
let first = (self as u8) / 10;
|
||||||
|
let last = (self as u8) % 10;
|
||||||
|
|
||||||
|
unsafe { mem::transmute::<[u8; 2], str::MinsStr>([b'0' + first, b'0' + last]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seconds.
|
||||||
|
#[int(u8, 0..60, crate = crate)]
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub enum Secs {}
|
||||||
|
|
||||||
|
impl Secs {
|
||||||
|
pub const fn to_str(self) -> str::SecsStr {
|
||||||
|
let first = (self as u8) / 10;
|
||||||
|
let last = (self as u8) % 10;
|
||||||
|
|
||||||
|
unsafe { mem::transmute::<[u8; 2], str::SecsStr>([b'0' + first, b'0' + last]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[data(copy, ord, not(serde), crate = crate)]
|
||||||
|
#[derive(Hash, Default)]
|
||||||
|
pub struct SubsecNanos(#[schemars(range(min = Self::MIN.0, max = Self::MAX.0))] u32);
|
||||||
|
|
||||||
|
impl SubsecNanos {
|
||||||
|
pub const MIN: Self = Self(0);
|
||||||
|
pub const MAX: Self = Self(999999999);
|
||||||
|
|
||||||
|
pub const fn into_inner(self) -> u32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_str(self) -> str::SubsecNanosStr {
|
||||||
|
const fn digit(v: u32) -> u8 {
|
||||||
|
(v % 10) as u8 + b'0'
|
||||||
|
}
|
||||||
|
let val = self.0;
|
||||||
|
|
||||||
|
let arr = [
|
||||||
|
digit(val / 1_000_000_00),
|
||||||
|
digit(val / 1_000_000_0),
|
||||||
|
digit(val / 1_000_000),
|
||||||
|
digit(val / 1_000_00),
|
||||||
|
digit(val / 1_000_0),
|
||||||
|
digit(val / 1_000),
|
||||||
|
digit(val / 100),
|
||||||
|
digit(val / 10),
|
||||||
|
digit(val),
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe { mem::transmute::<[u8; 9], str::SubsecNanosStr>(arr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create subsecs without checking the range.
|
||||||
|
pub const unsafe fn new_unchecked(value: u32) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn new(value: u32) -> Option<Self> {
|
||||||
|
if value > Self::MAX.0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Self(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
321
src/time/timestamp.rs
Normal file
321
src/time/timestamp.rs
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
use std::{borrow::Cow, num::NonZeroU64, ops::Add, str::FromStr, time::Duration};
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize, de};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data,
|
||||||
|
str::{HasPattern, ParseError},
|
||||||
|
time::{Hours, Mins, Secs, SecsTime, SubsecNanos, Time},
|
||||||
|
zst_error,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Date, LooseDate, PreciseTime, str, utils::divmod};
|
||||||
|
|
||||||
|
#[data(copy, ord, not(serde, schemars), crate = crate)]
|
||||||
|
pub struct LooseTimestamp<D = Date> {
|
||||||
|
date: D,
|
||||||
|
time: PreciseTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> JsonSchema for LooseTimestamp<D> {
|
||||||
|
fn schema_id() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed(concat!(module_path!(), "::LooseTimestamp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_name() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed("LooseTimestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
|
schemars::json_schema!({
|
||||||
|
"type": "string",
|
||||||
|
"pattern": str::TimestampStr::regex_pat_fullmatch(),
|
||||||
|
"description": "precise timestamp representation (UTC)"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for LooseTimestamp<LooseDate> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.to_str().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for LooseTimestamp<LooseDate> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = str::TimestampStr::deserialize(deserializer)?;
|
||||||
|
let ts = s
|
||||||
|
.parse()
|
||||||
|
.ok_or(de::Error::custom(zst_error!("malformed timestamp")))?;
|
||||||
|
Ok(ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for LooseTimestamp {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.loose().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for LooseTimestamp {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let loose = LooseTimestamp::<LooseDate>::deserialize(deserializer)?;
|
||||||
|
Ok(loose.compact_loose())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LooseTimestamp<LooseDate> {
|
||||||
|
pub const fn to_str(self) -> str::TimestampStr {
|
||||||
|
let date = self.date.to_str();
|
||||||
|
let time = self.time.to_str();
|
||||||
|
|
||||||
|
str::TimestampStr::new(date, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn compact_loose(self) -> LooseTimestamp {
|
||||||
|
LooseTimestamp {
|
||||||
|
date: self.date.compact(),
|
||||||
|
time: self.time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LooseTimestamp {
|
||||||
|
pub const fn compact(self) -> Timestamp {
|
||||||
|
let secs = self.date().to_secs() as u64;
|
||||||
|
let nanos =
|
||||||
|
unsafe { NonZeroU64::new_unchecked(self.time().to_nanos() + secs * 1_000_000_000) };
|
||||||
|
|
||||||
|
Timestamp::from_nanos(nanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn loose(self) -> LooseTimestamp<LooseDate> {
|
||||||
|
LooseTimestamp {
|
||||||
|
date: self.date.loose(),
|
||||||
|
time: self.time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Copy> LooseTimestamp<D> {
|
||||||
|
pub const fn new(date: D, time: PreciseTime) -> Self {
|
||||||
|
Self { date, time }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn date(self) -> D {
|
||||||
|
self.date
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn time(self) -> PreciseTime {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timestamp in nanoseconds, since UTC.
|
||||||
|
#[data(copy, ord, not(serde, schemars), display("{}", self.to_str()), crate = crate)]
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub struct Timestamp(NonZeroU64);
|
||||||
|
|
||||||
|
impl FromStr for Timestamp {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
str::TimestampStr::from_str(s).and_then(|t| {
|
||||||
|
let loose = t.parse().ok_or(ParseError::Char)?;
|
||||||
|
Ok(loose.compact_loose().compact())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonSchema for Timestamp {
|
||||||
|
fn schema_id() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed(concat!(module_path!(), "::Timestamp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_name() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed("Timestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
|
schemars::json_schema!({
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Number of nanoseconds since UTC unix epoch, only in binary formats"
|
||||||
|
},
|
||||||
|
LooseTimestamp::<LooseDate>::json_schema(generator)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Timestamp {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
if serializer.is_human_readable() {
|
||||||
|
self.loose().serialize(serializer)
|
||||||
|
} else {
|
||||||
|
self.0.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Timestamp {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
if deserializer.is_human_readable() {
|
||||||
|
Ok(LooseTimestamp::deserialize(deserializer)?.compact())
|
||||||
|
} else {
|
||||||
|
let nanos = NonZeroU64::deserialize(deserializer)?;
|
||||||
|
Ok(Self::from_nanos(nanos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add<Duration> for Timestamp {
|
||||||
|
type Output = Timestamp;
|
||||||
|
|
||||||
|
fn add(self, rhs: Duration) -> Self::Output {
|
||||||
|
Self(
|
||||||
|
self.0
|
||||||
|
.checked_add(rhs.as_nanos() as u64)
|
||||||
|
.expect("duration addition overflown"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
macro_rules! checked {
|
||||||
|
($lhs:expr) => {'ret: {
|
||||||
|
let Some(value) = NonZeroU64::new($lhs) else {
|
||||||
|
break 'ret None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Timestamp::from_nanos(value))
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! saturated {
|
||||||
|
($lhs:expr) => {
|
||||||
|
if let Some(val) = checked!($lhs) {
|
||||||
|
val
|
||||||
|
} else {
|
||||||
|
Timestamp::MIN
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timestamp {
|
||||||
|
pub const fn from_nanos(nanos: NonZeroU64) -> Self {
|
||||||
|
Self(nanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_secs(secs: u64) -> Self {
|
||||||
|
saturated!(secs * 1_000_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_secs_checked(secs: u64) -> Option<Self> {
|
||||||
|
checked!(secs * 1_000_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_millis(millis: u64) -> Self {
|
||||||
|
saturated!(millis * 1_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_millis_checked(millis: u64) -> Option<Self> {
|
||||||
|
checked!(millis * 1_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loose(self) -> LooseTimestamp<LooseDate> {
|
||||||
|
let date = self.date();
|
||||||
|
let time = {
|
||||||
|
let nanos = self.0.get();
|
||||||
|
let (secs, subsec_nanos) = divmod(nanos, 1_000_000_000);
|
||||||
|
let (mins, subsecs) = divmod(secs, 60);
|
||||||
|
let (hours, submins) = divmod(mins, 60);
|
||||||
|
let subhours = hours % 24;
|
||||||
|
|
||||||
|
let time = Time::new(unsafe { Hours::new_unchecked(subhours as u8) }, unsafe {
|
||||||
|
Mins::new_unchecked(submins as u8)
|
||||||
|
});
|
||||||
|
let secs_time = SecsTime::new(time, unsafe { Secs::new_unchecked(subsecs as u8) });
|
||||||
|
|
||||||
|
let subsec_nanos = unsafe { SubsecNanos::new_unchecked(subsec_nanos as u32) };
|
||||||
|
PreciseTime::new(secs_time, subsec_nanos)
|
||||||
|
};
|
||||||
|
|
||||||
|
LooseTimestamp { date, time }.loose()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert timestamp to string.
|
||||||
|
pub fn to_str(self) -> str::TimestampStr {
|
||||||
|
self.loose().to_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timestamp {
|
||||||
|
/// Get date-precision time.
|
||||||
|
pub const fn date(self) -> Date {
|
||||||
|
let days = self.as_hours() / 24;
|
||||||
|
Date::from_days(days as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get time of current day.
|
||||||
|
pub fn time(self) -> PreciseTime {
|
||||||
|
self.loose().time()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get time in hours.
|
||||||
|
pub const fn as_hours(self) -> u32 {
|
||||||
|
(self.as_mins() / 60) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get time in minutes.
|
||||||
|
pub const fn as_mins(self) -> u64 {
|
||||||
|
self.as_secs() / 60
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get time in seconds.
|
||||||
|
pub const fn as_secs(self) -> u64 {
|
||||||
|
self.as_millis() / 1_000
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get time in milliseconds.
|
||||||
|
pub const fn as_millis(self) -> u64 {
|
||||||
|
self.as_nanos().get() / 1_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get time in nanoseconds.
|
||||||
|
pub const fn as_nanos(self) -> NonZeroU64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timestamp {
|
||||||
|
// 2025.01.01 00:00.00
|
||||||
|
pub const TEST_ORIGIN: Self = Self(NonZeroU64::new(1735689600 * 1_000_000_000).unwrap());
|
||||||
|
|
||||||
|
/// Minimum time that can be represented.
|
||||||
|
pub const MIN: Self = Self(NonZeroU64::MIN);
|
||||||
|
|
||||||
|
/// Maximum time that can be represented.
|
||||||
|
pub const MAX: Self = Self(NonZeroU64::MAX);
|
||||||
|
}
|
23
src/time/tz.rs
Normal file
23
src/time/tz.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
//! # Timezones
|
||||||
|
//!
|
||||||
|
//! Timezone utilities.
|
||||||
|
|
||||||
|
use crate::{data, time::Hours};
|
||||||
|
|
||||||
|
#[data(ord, copy, crate = crate)]
|
||||||
|
pub struct FixedOffset(Hours);
|
||||||
|
|
||||||
|
impl FixedOffset {
|
||||||
|
/// UTC time offset. Basically no offset.
|
||||||
|
pub const UTC: Self = Self(Hours::new(0).unwrap());
|
||||||
|
|
||||||
|
/// Make offset from hours.
|
||||||
|
pub const fn from_hours(hours: Hours) -> Self {
|
||||||
|
Self(hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get offset in hours.
|
||||||
|
pub const fn hours(self) -> Hours {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
25
src/time/utils.rs
Normal file
25
src/time/utils.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
pub const fn is_leap_year(year: u16) -> bool {
|
||||||
|
let ndiv_by4 = (year & 3) != 0;
|
||||||
|
let cycle = ((year & 15) != 0) && (year % 25 == 0);
|
||||||
|
|
||||||
|
!(ndiv_by4 || cycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn divmod(lhs: u64, rhs: u64) -> (u64, u64) {
|
||||||
|
let div = lhs / rhs;
|
||||||
|
let mod_ = lhs - (div * rhs);
|
||||||
|
|
||||||
|
(div, mod_)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns number of additional days in `year`.
|
||||||
|
pub const fn leap_days_after(year: u16) -> u16 {
|
||||||
|
let first_criteria = year / 400;
|
||||||
|
|
||||||
|
let every4 = year >> 2;
|
||||||
|
let every100 = year / 100;
|
||||||
|
|
||||||
|
let second_criteria = every4 - every100;
|
||||||
|
|
||||||
|
first_criteria + second_criteria
|
||||||
|
}
|
13
src/trace_id.rs
Normal file
13
src/trace_id.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::data;
|
||||||
|
|
||||||
|
#[data(copy, ord, display("{_0:X}"), crate = crate)]
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub struct TraceId(u128);
|
||||||
|
|
||||||
|
impl TraceId {
|
||||||
|
pub const fn from_parts(millis: u64, random: u128) -> Self {
|
||||||
|
let mut result = random & ((1 << 80) - 1);
|
||||||
|
result |= (millis as u128) << 80;
|
||||||
|
Self(result)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue