Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_Pu7HBuKr1ugKgn0LZfqFb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"Cargo.toml":"Patch"},"note":"Hello","date":"2026-02-16T16:40:01.407925800Z"}
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@
resolver = "2"
members = ["crates/*", "examples/*"]

[workspace.package]
version = "0.1.33"
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/dev-five-git/vespera"
readme = "README.md"

[workspace.dependencies]
vespera_core = { path = "crates/vespera_core", version = "0.1.33" }
vespera_macro = { path = "crates/vespera_macro", version = "0.1.33" }

[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
# cherry-picked nursery lints
collection_is_never_read = "warn"
option_if_let_else = "warn"
redundant_pub_crate = "warn"
use_self = "warn"
# noisy pedantic lints to allow
module_name_repetitions = "allow"
must_use_candidate = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
doc_markdown = "allow"

[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
12 changes: 7 additions & 5 deletions crates/vespera/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
[package]
name = "vespera"
version = "0.1.33"
edition = "2024"
version.workspace = true
edition.workspace = true
description = "A fully automated OpenAPI engine for Axum with zero-config route and schema discovery"
license = "Apache-2.0"
repository = "https://github.com/dev-five-git/vespera"
readme = "../../README.md"
license.workspace = true
repository.workspace = true

[features]
default = ["dep:axum-extra", "axum-extra/typed-header", "axum-extra/form", "axum-extra/query", "axum-extra/multipart", "axum-extra/cookie"]
Expand All @@ -21,3 +20,6 @@ tempfile = "3"
serde_json = "1"
tower-layer = "0.3"
tower-service = "0.3"

[lints]
workspace = true
8 changes: 5 additions & 3 deletions crates/vespera/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Vespera - OpenAPI generation for Rust web frameworks
//! Vespera - `OpenAPI` generation for Rust web frameworks
//!
//! This crate provides macros and utilities for generating OpenAPI documentation
//! This crate provides macros and utilities for generating `OpenAPI` documentation
//! from your route definitions.

// Re-export vespera_core types so users don't need to depend on vespera_core directly
Expand Down Expand Up @@ -66,7 +66,8 @@ impl<S> VesperaRouter<S>
where
S: Clone + Send + Sync + 'static,
{
/// Create a new VesperaRouter with a base router and routers to merge
/// Create a new `VesperaRouter` with a base router and routers to merge
#[must_use]
pub fn new(base: axum::Router<S>, merge_fns: Vec<fn() -> axum::Router<()>>) -> Self {
Self { base, merge_fns }
}
Expand All @@ -92,6 +93,7 @@ where
}

/// Add a layer to the router.
#[must_use]
pub fn layer<L>(self, layer: L) -> Self
where
L: tower_layer::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
Expand Down
14 changes: 8 additions & 6 deletions crates/vespera_core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
[package]
name = "vespera_core"
version = "0.1.33"
edition = "2024"
version.workspace = true
edition.workspace = true
description = "Core types and utilities for Vespera"
license = "Apache-2.0"
repository = "https://github.com/dev-five-git/vespera"
readme = "../../README.md"
license.workspace = true
repository.workspace = true

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[dev-dependencies]
rstest = "0.26"
rstest = "0.26"

[lints]
workspace = true
4 changes: 2 additions & 2 deletions crates/vespera_core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Vespera Core - OpenAPI structure definitions
//! Vespera Core - `OpenAPI` structure definitions
//!
//! Provides structures conforming to the OpenAPI 3.1 specification.
//! Provides structures conforming to the `OpenAPI` 3.1 specification.

pub mod openapi;
pub mod route;
Expand Down
19 changes: 12 additions & 7 deletions crates/vespera_core/src/openapi.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! OpenAPI document structure definitions
//! `OpenAPI` document structure definitions

use crate::route::PathItem;
use crate::schema::{Components, ExternalDocumentation};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};

/// OpenAPI document version
/// `OpenAPI` document version
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OpenApiVersion {
#[serde(rename = "3.0.0")]
Expand Down Expand Up @@ -114,11 +114,11 @@ pub struct Tag {
pub external_docs: Option<ExternalDocumentation>,
}

/// OpenAPI document (root structure)
/// `OpenAPI` document (root structure)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenApi {
/// OpenAPI version
/// `OpenAPI` version
pub openapi: OpenApiVersion,
/// API information
pub info: Info,
Expand All @@ -142,10 +142,10 @@ pub struct OpenApi {
}

impl OpenApi {
/// Merge another OpenAPI document into this one.
/// Merge another `OpenAPI` document into this one.
/// Paths, schemas, and tags from `other` are added to `self`.
/// If there are conflicts, `self` takes precedence.
pub fn merge(&mut self, other: OpenApi) {
pub fn merge(&mut self, other: Self) {
// Merge paths (self takes precedence on conflict)
for (path, item) in other.paths {
self.paths.entry(path).or_insert(item);
Expand Down Expand Up @@ -194,8 +194,13 @@ impl OpenApi {
}

/// Merge from a JSON string. Returns error if parsing fails.
///
/// # Errors
///
/// Returns a `serde_json::Error` when `json_str` is not valid JSON or does not
/// deserialize into an `OpenApi` document.
pub fn merge_from_str(&mut self, json_str: &str) -> Result<(), serde_json::Error> {
let other: OpenApi = serde_json::from_str(json_str)?;
let other: Self = serde_json::from_str(json_str)?;
self.merge(other);
Ok(())
}
Expand Down
68 changes: 49 additions & 19 deletions crates/vespera_core/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::collections::{BTreeMap, HashMap};
use crate::SchemaRef;

/// HTTP method
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Expand All @@ -19,18 +19,35 @@ pub enum HttpMethod {
Trace,
}

impl From<&str> for HttpMethod {
fn from(value: &str) -> Self {
impl std::fmt::Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Get => write!(f, "GET"),
Self::Post => write!(f, "POST"),
Self::Put => write!(f, "PUT"),
Self::Patch => write!(f, "PATCH"),
Self::Delete => write!(f, "DELETE"),
Self::Head => write!(f, "HEAD"),
Self::Options => write!(f, "OPTIONS"),
Self::Trace => write!(f, "TRACE"),
}
}
}

impl TryFrom<&str> for HttpMethod {
type Error = String;

fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.to_uppercase().as_str() {
"GET" => HttpMethod::Get,
"POST" => HttpMethod::Post,
"PUT" => HttpMethod::Put,
"PATCH" => HttpMethod::Patch,
"DELETE" => HttpMethod::Delete,
"HEAD" => HttpMethod::Head,
"OPTIONS" => HttpMethod::Options,
"TRACE" => HttpMethod::Trace,
_ => HttpMethod::Get, // default value
"GET" => Ok(Self::Get),
"POST" => Ok(Self::Post),
"PUT" => Ok(Self::Put),
"PATCH" => Ok(Self::Patch),
"DELETE" => Ok(Self::Delete),
"HEAD" => Ok(Self::Head),
"OPTIONS" => Ok(Self::Options),
"TRACE" => Ok(Self::Trace),
other => Err(format!("unknown HTTP method: {other}")),
}
}
}
Expand Down Expand Up @@ -137,7 +154,7 @@ pub struct Header {
pub schema: Option<SchemaRef>,
}

/// OpenAPI Operation definition
/// `OpenAPI` Operation definition
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Operation {
Expand Down Expand Up @@ -221,7 +238,8 @@ impl PathItem {
}

/// Get an operation for a specific HTTP method
pub fn get_operation(&self, method: &HttpMethod) -> Option<&Operation> {
#[must_use]
pub const fn get_operation(&self, method: &HttpMethod) -> Option<&Operation> {
match method {
HttpMethod::Get => self.get.as_ref(),
HttpMethod::Post => self.post.as_ref(),
Expand Down Expand Up @@ -277,15 +295,14 @@ mod tests {
#[case("trace", HttpMethod::Trace)]
#[case("Trace", HttpMethod::Trace)]
fn test_http_method_from_str(#[case] input: &str, #[case] expected: HttpMethod) {
let result = HttpMethod::from(input);
let result = HttpMethod::try_from(input).unwrap();
assert_eq!(result, expected);
}

#[test]
fn test_http_method_from_invalid_str() {
// Invalid method should default to Get
let result = HttpMethod::from("INVALID");
assert_eq!(result, HttpMethod::Get);
let result = HttpMethod::try_from("INVALID");
assert!(result.is_err());
}

#[test]
Expand Down Expand Up @@ -523,6 +540,19 @@ mod tests {
);
}

#[rstest]
#[case(HttpMethod::Get, "GET")]
#[case(HttpMethod::Post, "POST")]
#[case(HttpMethod::Put, "PUT")]
#[case(HttpMethod::Patch, "PATCH")]
#[case(HttpMethod::Delete, "DELETE")]
#[case(HttpMethod::Head, "HEAD")]
#[case(HttpMethod::Options, "OPTIONS")]
#[case(HttpMethod::Trace, "TRACE")]
fn test_http_method_display(#[case] method: HttpMethod, #[case] expected: &str) {
assert_eq!(method.to_string(), expected);
}

#[test]
fn test_http_method_equality() {
let method1 = HttpMethod::Get;
Expand All @@ -536,7 +566,7 @@ mod tests {
#[test]
fn test_http_method_clone() {
let method = HttpMethod::Get;
let cloned = method.clone();
let cloned = method;
assert_eq!(method, cloned);
}

Expand Down
Loading