Compare commits

..

2 Commits

Author SHA1 Message Date
72f6bcc5be feat: primitive comment block parser 2026-01-29 16:49:46 +01:00
749dd0dbb2 chore: basic project goal 2026-01-27 17:24:34 +01:00
10 changed files with 267 additions and 2 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

21
Cargo.lock generated Normal file
View File

@@ -0,0 +1,21 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "apidoc"
version = "0.1.0"
dependencies = [
"anyhow",
"apidoc_parser",
]
[[package]]
name = "apidoc_parser"
version = "0.1.0"

11
Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[workspace]
resolver = "3"
members = ["src/apidoc", "src/apidoc_parser"]
[workspace.package]
version = "0.1.0"
edition = "2024"
public = false
[workspace.clippy]
strict = "warn"

View File

@@ -1,3 +1,53 @@
# Musicparty management repository template # Apidoc
This is a template repository, use it to create other repositories. A code utility to manage your API:s, purely derrived from code (doc-comments).
## Trivial example
````ts
// @apidoc TYPE User.Token
// [readonly] [auth]
//
// A JWT token used to verify identity at secured endpoints.
//
// ## Schema
// string.jwt
//
// ## Examples
//
// ```
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
// ```
type Token = string;
// @apidoc POST /auth/register Sign-in as user `User.login`
//
// Using credentials and details, request that a `User` be created and stored,
// returning a `User.token`.
//
// ## Body JSON
// username: string.ascii(max: 32) [unique] [example = `tommylive`]
// email: string.email The email to use when authenticating the user.
// password: string(min: 8) [sendonly]
// A plain-text password to hash and store for authentication.
//
// Password criteria:
// - >8 characters
// - >1 uppercase ascii letter
// - >1 symbol, defined as any non-ascii-alphanumeric character
// display_name?: string(max: 32) [example = `Tommy`]
// An alternative name to show instead of the username
//
// ## Responses
// ### 200 User created JSON
//
// The user passed checks and was created.
//
// token: User.Token Token to identify as the newly created user
//
// ### 400 Invalid payload
// ### 409 Username taken
export async function processRegister(req: Request, res: Response) {
// ...
}
````

8
src/apidoc/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "apidoc"
version.workspace = true
edition.workspace = true
[dependencies]
anyhow = "1.0.100"
apidoc_parser = { version = "0.1.0", path = "../apidoc_parser" }

17
src/apidoc/src/main.rs Normal file
View File

@@ -0,0 +1,17 @@
use std::{env::args, fs};
use anyhow::Context;
use apidoc_parser::parse;
fn main() -> anyhow::Result<()> {
let mut args = args().skip(1);
let file = fs::read_to_string(args.next().context("expected a file path argument")?)
.context("opening file")?;
println!("{file}");
parse(file);
Ok(())
}

View File

@@ -0,0 +1,7 @@
[package]
name = "apidoc_parser"
version.workspace = true
edition.workspace = true
public.workspace = true
[dependencies]

View File

@@ -0,0 +1,20 @@
#[derive(Debug, Clone, Copy)]
pub struct Span {
lo: usize,
hi: usize,
}
impl Span {
pub const fn new(range: std::ops::Range<usize>) -> Self {
Self {
lo: range.start,
hi: range.end,
}
}
}
#[derive(Debug, Clone)]
pub struct Comment {
pub(crate) prefix: String,
pub(crate) span: Span,
}

View File

@@ -0,0 +1,75 @@
#[derive(Clone)]
pub(crate) struct Cursor {
source: String,
offset: usize,
byte_offset: usize,
}
impl Cursor {
pub const fn new(source: String) -> Self {
Self {
source,
offset: 0,
byte_offset: 0,
}
}
pub fn slice(&self) -> &'_ str {
self.source
.get(self.byte_offset..)
.expect("str should be built out of bounded chars")
}
pub fn bump(&mut self) -> &mut Self {
self.offset += 1;
self.byte_offset += self.slice().chars().next().map_or(0, char::len_utf8);
self
}
pub fn bump_newline(&mut self) -> bool {
while !self.slice().starts_with('\n') && !self.bump().is_finished() {}
if self.slice().starts_with('\n') {
self.bump();
return true;
}
false
}
pub fn skip(&mut self, chars: usize) {
let len: usize = self.slice().chars().take(chars).map(char::len_utf8).sum();
self.offset += chars;
self.byte_offset += len;
}
pub fn skip_whitespace(&mut self) {
let mut offset = 0;
let mut byte_offset = 0;
for char in self.slice().chars() {
if !char.is_ascii_whitespace() || char == '\n' {
break;
}
offset += 1;
byte_offset += char.len_utf8();
}
self.offset += offset;
self.byte_offset += byte_offset;
}
pub const fn is_finished(&self) -> bool {
self.byte_offset >= self.source.len()
}
pub const fn offset(&self) -> usize {
self.offset
}
pub fn fmt_offset(&self) -> String {
format!("{} (byte {})", self.offset, self.byte_offset)
}
}

View File

@@ -0,0 +1,55 @@
use crate::comment::{Comment, Span};
pub mod comment;
pub(crate) mod cursor;
use cursor::Cursor;
// TODO: (issue #3) Move these into some variable configuration
const APIDOC_PRAGMA: &str = "@apidoc";
const COMMENT_PREFIX: &str = "//";
pub fn parse(source: String) -> Vec<Comment> {
let mut comments = Vec::new();
let mut cursor = Cursor::new(source);
while !cursor.bump().is_finished() {
let slice = cursor.slice();
if slice.starts_with(COMMENT_PREFIX) {
println!("comment: at offset {}", cursor.fmt_offset());
let start = cursor.offset();
cursor.skip(COMMENT_PREFIX.len());
cursor.skip_whitespace();
if !cursor.slice().starts_with(APIDOC_PRAGMA) {
continue;
}
let prefix = COMMENT_PREFIX;
println!(" pragma! {}", cursor.fmt_offset());
cursor.bump_newline();
let mut end = cursor.offset() - 1;
loop {
cursor.skip_whitespace();
if !cursor.slice().starts_with(prefix) {
break;
}
cursor.bump_newline();
end = cursor.offset() - 1;
}
comments.push(dbg!(Comment {
prefix: COMMENT_PREFIX.to_owned(),
span: Span::new(start..end)
}));
}
}
comments
}