Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## Next

#### 💥 Breaking

- Removed `#[config(serde(...))]` on containers. Use `#[serde(...)]` instead.

#### 🚀 Updates

##### Config

- Added support for unnamed tuple and newtype structs. Unnamed fields within the struct support
`#[setting]`.
- Added support for `#[setting(nested = NestedConfig)]` on struct fields and enum variants, where
the nested config name can be explicitly defined if we fail to detect it. This is useful for
extremely complex/composed types.
- Added support for `#[setting(transform)]` on enum variants.
- Added support for env prefixes at the field level when the field is also nested. This will
override the env prefix defined on the nested container:
`#[setting(nested, env_prefix = "OVERRIDE_")]`.
- Updated `#[setting(extend)]` settings to support `Option` wrapped values.
- Updated the methods of `PartialConfig` to all have a default implementation. This helps to greatly
reduce the amount of macro generated code.
- Improved the parse, handling, and validation of container and field attributes.

##### Serde

- Added support for explicit deserialize and serialize renaming on containers and fields:
`#[serde(rename(deserialize = "de_name", serialize = "ser_name"))]`.
- Added support for `skip_deserializing_if` and `skip_serializing_if` on fields.
- Updated `alias` to support multiple aliases: `#[serde(alias = "alias1", alias = "alias2")]`

## 0.19.7

#### ⚙️ Internal
Expand Down
32 changes: 32 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ members = ["crates/*"]
[workspace.dependencies]
chrono = "0.4.42"
convert_case = "0.10.0"
darling = "0.23.0"
indexmap = "2.13.0"
miette = "7.6.0"
proc-macro2 = "1.0.103"
quote = "1.0.42"
regex = "1.12.2"
relative-path = "2.0.1"
reqwest = { version = "0.13.1", default-features = false }
Expand All @@ -19,6 +22,7 @@ serde_json = "1.0.149"
serde_yaml = "0.9.33"
serde_norway = "0.9.42"
starbase_sandbox = "0.10.7"
syn = "2.0.111"
toml = "1.1.2"
tracing = "0.1.44"
url = "2.5.8"
Expand Down
40 changes: 40 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[package]
name = "schematic_core"
version = "0.18.7"
edition = "2024"
license = "MIT"
description = "Core building blocks for schematic macros."
homepage = "https://moonrepo.github.io/schematic"
repository = "https://github.com/moonrepo/schematic"

[package.metadata.docs.rs]
all-features = true

[dependencies]
# convert_case = { workspace = true }
darling = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true, features = ["full"] }

[dev-dependencies]
schematic_core = { path = ".", features = [
"config",
"env",
"extends",
"schema",
"tracing",
"validate",
] }
prettyplease = "0.2.37"
syn = { workspace = true, features = ["full", "extra-traits"] }
starbase_sandbox = { workspace = true }

[features]
default = []
config = []
env = []
extends = []
schema = []
tracing = []
validate = []
218 changes: 218 additions & 0 deletions crates/core/src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
use crate::utils::to_type_string;
use darling::ast::NestedMeta;
use darling::{FromAttributes, FromDeriveInput, FromMeta};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use std::ops::Deref;
use syn::{Expr, Ident};

#[derive(Clone, Copy, Debug)]
pub enum SerdeIoDirection {
From, // read / deserialize
To, // write / serialize
}

#[derive(Clone, Debug)]
pub enum SerdeTagFormat {
Untagged,
External,
Internal(String),
Adjacent(String, String),
// Special case for unit only enums
Unit,
}

// #[serde(rename = "name")]
// #[serde(rename(deserialize = "de_name", serialize = "ser_name"))]
#[derive(Debug, Default, PartialEq)]
pub struct SerdeRenameArg {
pub deserialize: Option<String>,
pub serialize: Option<String>,
}

impl FromMeta for SerdeRenameArg {
fn from_string(value: &str) -> darling::Result<Self> {
Ok(Self {
deserialize: Some(value.into()),
serialize: Some(value.into()),
})
}

fn from_list(items: &[NestedMeta]) -> darling::Result<Self> {
#[derive(Default, FromMeta)]
#[darling(default)]
struct Rename {
deserialize: Option<String>,
serialize: Option<String>,
}

impl From<Rename> for SerdeRenameArg {
fn from(value: Rename) -> Self {
Self {
deserialize: value.deserialize,
serialize: value.serialize,
}
}
}

Rename::from_list(items).map(SerdeRenameArg::from)
}
}

impl SerdeRenameArg {
pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> {
match dir {
SerdeIoDirection::From => self.deserialize.as_deref(),
SerdeIoDirection::To => self.serialize.as_deref(),
}
}

pub fn get_meta(&self, key: &str) -> TokenStream {
match (self.deserialize.as_deref(), self.serialize.as_deref()) {
(Some(de), Some(ser)) => {
if de == ser {
quote! { #key = #de }
} else {
quote! { #key(deserialize = #de, serialize = #ser) }
}
}
(None, Some(ser)) => quote! { #key(serialize = #ser) },
(Some(de), None) => quote! { #key(deserialize = #de) },
_ => quote! {},
}
}
}

// #[serde()]
#[derive(Debug, Default, FromDeriveInput)]
#[darling(default, allow_unknown_fields, attributes(serde))]
pub struct SerdeContainerArgs {
pub default: bool,
pub deny_unknown_fields: bool,

// struct
pub rename: Option<SerdeRenameArg>,
pub rename_all: Option<SerdeRenameArg>,
pub rename_all_fields: Option<SerdeRenameArg>,

// enum
pub content: Option<String>,
pub expecting: Option<String>,
pub tag: Option<String>,
pub untagged: bool,
}

// #[serde()]
#[derive(Debug, Default, FromAttributes)]
#[darling(default, allow_unknown_fields, attributes(serde))]
pub struct SerdeFieldArgs {
#[darling(multiple)]
pub alias: Vec<String>,
pub default: bool,
pub flatten: bool,
pub rename: Option<SerdeRenameArg>,
pub skip: bool,
pub skip_deserializing: bool,
pub skip_deserializing_if: Option<String>,
pub skip_serializing: bool,
pub skip_serializing_if: Option<String>,

// variant
pub other: bool,
pub untagged: bool,
}

// #[setting(partial)]
#[derive(Debug, Default)]
pub struct PartialArg {
meta: Vec<NestedMeta>,
}

impl ToTokens for PartialArg {
fn to_tokens(&self, tokens: &mut TokenStream) {
let attrs: Vec<_> = self.meta.iter().map(|m| m.to_token_stream()).collect();

if !attrs.is_empty() {
tokens.extend(quote! {#[#(#attrs),*]});
}
}
}

impl FromMeta for PartialArg {
fn from_list(items: &[NestedMeta]) -> darling::Result<Self> {
Ok(Self {
meta: items.to_vec(),
})
}
}

// #[setting(nested)]
#[derive(Debug)]
pub enum NestedArg {
Detect(bool),
Ident(Ident),
}

impl NestedArg {
pub fn is_nested(&self) -> bool {
match self {
NestedArg::Detect(inner) => *inner,
NestedArg::Ident(_) => true,
}
}
}

impl FromMeta for NestedArg {
// #[setting(nested)]
fn from_word() -> darling::Result<Self> {
Ok(Self::Detect(true))
}

// #[setting(nested = true)]
fn from_bool(value: bool) -> darling::Result<Self> {
Ok(Self::Detect(value))
}

// #[setting(nested = NestedConfig)]
fn from_expr(expr: &Expr) -> darling::Result<Self> {
match expr {
Expr::Lit(lit) => Self::from_value(&lit.lit),
Expr::Path(path) => {
if path.path.segments.len() > 1 {
Err(darling::Error::custom(format!(
"Too many segments for `{}`, only a single identifier is allowed.",
to_type_string(path.to_token_stream())
)))
} else {
Ok(Self::Ident(
path.path.segments.last().unwrap().ident.to_owned(),
))
}
}
_ => Err(darling::Error::unexpected_expr_type(expr)),
}
.map_err(|e| e.with_span(expr))
}
}

// #[setting(validate)]
#[derive(Debug)]
pub struct ValidateArg(Expr);

impl FromMeta for ValidateArg {
fn from_expr(expr: &Expr) -> darling::Result<Self> {
match expr {
Expr::Call(_) | Expr::Path(_) => Ok(Self(expr.to_owned())),
_ => Err(darling::Error::unexpected_expr_type(expr)),
}
.map_err(|e| e.with_span(expr))
}
}

impl Deref for ValidateArg {
type Target = Expr;

fn deref(&self) -> &Self::Target {
&self.0
}
}
Loading
Loading