initialize spec macro and standards

Signed-off-by: Onur Özkan <work@onurozkan.dev>
This commit is contained in:
Onur Özkan committed 2026-07-16 22:16:45 +03:00
1 parent bac05a4562
commit ceba320a96
11 files changed
+471 -1

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
[package]
name = "kaptan-spec"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[lib]
proc-macro = true
[dev-dependencies]
trybuild = "1.0"
+70
View File
@@ -0,0 +1,70 @@
use proc_macro::{Literal, Punct, Spacing, TokenStream, TokenTree};
#[proc_macro_attribute]
pub fn spec(attr: TokenStream, item: TokenStream) -> TokenStream {
match parse_spec_path(attr) {
Ok(path) => expand_spec_guard(&path, item),
Err(message) => compile_error(&message),
}
}
fn parse_spec_path(attr: TokenStream) -> Result<String, String> {
let tokens: Vec<TokenTree> = attr.into_iter().collect();
match tokens.as_slice() {
[TokenTree::Literal(path)] if is_string_literal(path) => Ok(path.to_string()),
[
TokenTree::Ident(name),
TokenTree::Punct(eq),
TokenTree::Literal(path),
] if name.to_string() == "path" && is_equals(eq) && is_string_literal(path) => {
Ok(path.to_string())
}
[] => Err("missing spec path; use #[spec(\"file.v0.1.0.spec.md\")]".into()),
_ => Err("invalid spec attribute; use #[spec(\"file.v0.1.0.spec.md\")]".into()),
}
}
fn is_string_literal(literal: &Literal) -> bool {
let value = literal.to_string();
value.starts_with('"') || value.starts_with("r\"") || value.starts_with("r#")
}
fn is_equals(punct: &Punct) -> bool {
punct.as_char() == '=' && punct.spacing() == Spacing::Alone
}
fn expand_spec_guard(path: &str, item: TokenStream) -> TokenStream {
let guard = format!("const _: &str = include_str!({path});");
let mut output = TokenStream::new();
output.extend(
guard
.parse::<TokenStream>()
.expect("spec macro must generate valid Rust tokens"),
);
output.extend(item);
output
}
fn compile_error(message: &str) -> TokenStream {
let escaped = escape_string(message);
format!("compile_error!(\"{escaped}\");")
.parse::<TokenStream>()
.expect("spec macro must generate valid compile_error tokens")
}
fn escape_string(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'\\' => escaped.push_str("\\\\"),
'"' => escaped.push_str("\\\""),
_ => escaped.push(ch),
}
}
escaped
}
+11
View File
@@ -0,0 +1,11 @@
#[test]
fn accepts_existing_sidecar_spec() {
let tests = trybuild::TestCases::new();
tests.pass("tests/ui/flight_core_contract.rs");
}
#[test]
fn rejects_missing_sidecar_spec() {
let tests = trybuild::TestCases::new();
tests.compile_fail("tests/ui/fail_missing_sidecar.rs");
}
@@ -0,0 +1,8 @@
use kaptan_spec::spec;
#[spec("fail_missing_sidecar.v0.1.0.spec.md")]
struct MissingSpec;
fn main() {
let _ = core::mem::size_of::<MissingSpec>();
}
@@ -0,0 +1,5 @@
error: couldn't read `$DIR/tests/ui/fail_missing_sidecar.v0.1.0.spec.md`: No such file or directory (os error 2)
--> tests/ui/fail_missing_sidecar.rs:3:1
|
3 | #[spec("fail_missing_sidecar.v0.1.0.spec.md")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -0,0 +1,9 @@
use kaptan_spec::spec;
#[spec("flight_core_contract.v0.1.0.spec.md")]
struct FlightCore;
fn main() {
let size = core::mem::size_of::<FlightCore>();
assert_eq!(size, 0);
}
@@ -0,0 +1,10 @@
---
title: Flight core contract
status: in-progress
owners:
- Test Name <name@test.com>
---
# Summary
Minimal spec for compile-time linkage testing.