use typeshaper::{RequiredError, TypeshaperExt, typeshaper, typex};
use std::convert::TryFrom;
#[typeshaper]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
pub id: u64,
pub name: String,
pub age: u8,
email: String,
}
#[typeshaper]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Badge {
pub score: u32,
pub label: String,
}
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserNoAge = User - [age]
);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserPublic = User & [id, name]
);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserWithBadge = UserNoAge + Badge
);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserDraft = User?
);
typex!(#[derive(Debug, Clone, PartialEq, Eq)] UserComplete = UserDraft!);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserOnly = User % Badge
);
#[test]
fn omit_removes_listed_fields() {
let user = User {
id: 1,
name: "alice".into(),
age: 25,
email: "a@b.com".into(),
};
let no_age: UserNoAge = user.project();
assert_eq!(
no_age,
UserNoAge {
id: 1,
name: "alice".into(),
email: "a@b.com".into()
}
);
}
#[test]
fn pick_keeps_only_listed_fields() {
let user = User {
id: 2,
name: "bob".into(),
age: 30,
email: "b@c.com".into(),
};
let public_user: UserPublic = user.project();
assert_eq!(
public_user,
UserPublic {
id: 2,
name: "bob".into()
}
);
}
#[test]
fn merge_combines_fields_via_from_tuple() {
let no_age = UserNoAge {
id: 3,
name: "carol".into(),
email: "c@d.com".into(),
};
let badge = Badge {
score: 99,
label: "gold".into(),
};
let combined = UserWithBadge::from((no_age, badge));
assert_eq!(combined.id, 3);
assert_eq!(combined.name, "carol");
assert_eq!(combined.email, "c@d.com");
assert_eq!(combined.score, 99);
assert_eq!(combined.label, "gold");
}
#[test]
fn partial_wraps_all_fields_in_option() {
let user = User {
id: 4,
name: "dave".into(),
age: 22,
email: "d@e.com".into(),
};
let draft = UserDraft::from(user);
assert_eq!(draft.id, Some(4));
assert_eq!(draft.name, Some("dave".into()));
assert_eq!(draft.age, Some(22));
assert_eq!(draft.email, Some("d@e.com".into()));
}
#[test]
fn required_unwraps_all_some_fields() {
let draft = UserDraft {
id: Some(5),
name: Some("eve".into()),
age: Some(28),
email: Some("e@f.com".into()),
};
let complete = UserComplete::try_from(draft).expect("all fields are Some");
assert_eq!(complete.id, 5);
assert_eq!(complete.name, "eve");
assert_eq!(complete.age, 28);
assert_eq!(complete.email, "e@f.com");
}
#[test]
fn required_errors_when_a_field_is_none() {
let draft = UserDraft {
id: None,
name: Some("x".into()),
age: Some(1),
email: Some("x@y.com".into()),
};
let err = UserComplete::try_from(draft).unwrap_err();
assert_eq!(err, RequiredError::new("id"));
assert_eq!(err.field, "id");
assert!(err.to_string().contains("id"));
}
#[test]
fn diff_keeps_only_fields_absent_in_second_type() {
let user = User {
id: 10,
name: "hank".into(),
age: 33,
email: "h@i.com".into(),
};
let user_only: UserOnly = user.project();
assert_eq!(
user_only,
UserOnly {
id: 10,
name: "hank".into(),
age: 33,
email: "h@i.com".into()
}
);
}
#[test]
fn diff_excludes_shared_field_names() {
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
OnlyEmail = UserNoAge % UserPublic
);
let src = UserNoAge {
id: 11,
name: "iris".into(),
email: "i@j.com".into(),
};
let only_email: OnlyEmail = src.project();
assert_eq!(
only_email,
OnlyEmail {
email: "i@j.com".into()
}
);
}
#[test]
fn multiple_attrs_forwarded_to_generated_struct() {
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
UserMinimal = User & [id, name]
);
let user = User {
id: 42,
name: "zoe".into(),
age: 20,
email: "z@z.com".into(),
};
let minimal: UserMinimal = user.project();
assert_eq!(
minimal,
UserMinimal {
id: 42,
name: "zoe".into()
}
);
}
#[test]
fn omit_result_can_feed_into_merge() {
let combined = UserWithBadge {
id: 9,
name: "fred".into(),
email: "f@g.com".into(),
score: 7,
label: "bronze".into(),
};
assert_eq!(combined.score, 7);
}
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserDto = User - [age] & [id, name]
);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserFull = User + (Badge - [label])
);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserNoAgeDraft = User - [age]?
);
typex!(#[derive(Debug, Clone, PartialEq, Eq)] UserNoAgeComplete = (User - [age])?!);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserExtraFields = User % (User & [id, name])
);
#[test]
fn chain_omit_then_pick() {
let dto = UserDto {
id: 1,
name: "alice".into(),
};
assert_eq!(dto.id, 1);
assert_eq!(dto.name, "alice");
}
#[test]
fn merge_with_parenthesised_right() {
let user = User {
id: 2,
name: "bob".into(),
age: 30,
email: "b@c.com".into(),
};
let badge = Badge {
score: 88,
label: "silver".into(),
};
let full = UserFull::from((user, badge.project()));
assert_eq!(full.id, 2);
assert_eq!(full.name, "bob");
assert_eq!(full.age, 30);
assert_eq!(full.email, "b@c.com");
assert_eq!(full.score, 88);
}
#[test]
fn chain_omit_then_partial() {
let draft = UserNoAgeDraft {
id: Some(3),
name: Some("carol".into()),
email: Some("c@d.com".into()),
};
assert_eq!(draft.id, Some(3));
assert_eq!(draft.name, Some("carol".into()));
assert_eq!(draft.email, Some("c@d.com".into()));
}
#[test]
fn nested_parens_omit_partial_required() {
let complete = UserNoAgeComplete {
id: 4,
name: "dave".into(),
email: "d@e.com".into(),
};
assert_eq!(complete.id, 4);
assert_eq!(complete.name, "dave");
assert_eq!(complete.email, "d@e.com");
}
#[test]
fn diff_with_parenthesised_right() {
let user = User {
id: 5,
name: "eve".into(),
age: 33,
email: "e@f.com".into(),
};
let extra: UserExtraFields = user.project();
assert_eq!(extra.age, 33);
assert_eq!(extra.email, "e@f.com");
}
#[typeshaper(export)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Product {
pub id: u64,
pub title: String,
pub price_cents: u64,
pub hidden: bool,
}
typeshaper_import_Product!();
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
ProductPublic = Product - [hidden]
);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
ProductSummary = Product & [id, title]
);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
ProductPartial = Product?
);
#[test]
fn export_import_omit() {
let p = Product {
id: 1,
title: "Widget".into(),
price_cents: 499,
hidden: false,
};
let public: ProductPublic = p.project();
assert_eq!(
public,
ProductPublic {
id: 1,
title: "Widget".into(),
price_cents: 499
}
);
}
#[test]
fn export_import_pick() {
let p = Product {
id: 2,
title: "Gadget".into(),
price_cents: 999,
hidden: true,
};
let summary: ProductSummary = p.project();
assert_eq!(
summary,
ProductSummary {
id: 2,
title: "Gadget".into()
}
);
}
#[test]
fn export_import_partial() {
let p = Product {
id: 3,
title: "Thing".into(),
price_cents: 199,
hidden: false,
};
let draft = ProductPartial::from(p);
assert_eq!(draft.id, Some(3));
assert_eq!(draft.title, Some("Thing".into()));
assert_eq!(draft.price_cents, Some(199));
assert_eq!(draft.hidden, Some(false));
}
#[typeshaper]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportedDraft {
pub id: Option<u64>,
pub name: Option<String>,
pub score: Option<u32>,
}
typex!(#[derive(Debug, Clone, PartialEq, Eq)] ImportedComplete = ImportedDraft!);
#[test]
fn required_works_on_imported_option_fields() {
let draft = ImportedDraft {
id: Some(42),
name: Some("alice".into()),
score: Some(99),
};
let complete = ImportedComplete::try_from(draft).expect("all fields are Some");
assert_eq!(complete.id, 42);
assert_eq!(complete.name, "alice");
assert_eq!(complete.score, 99);
}
#[test]
fn required_errors_on_imported_none_field() {
let draft = ImportedDraft { id: None, name: Some("bob".into()), score: Some(0) };
let err = ImportedComplete::try_from(draft).unwrap_err();
assert_eq!(err, RequiredError::new("id"));
}
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserCopy = User
);
typex!(
#[derive(Debug, Clone, PartialEq, Eq)]
UserCopyPublic = UserCopy & [id, name]
);
#[test]
fn rebuild_produces_identical_fields() {
use typeshaper::TypeshaperExt;
let user = User {
id: 99,
name: "zara".into(),
age: 21,
email: "z@z.com".into(),
};
let copy: UserCopy = user.project();
assert_eq!(copy.id, 99);
assert_eq!(copy.name, "zara");
assert_eq!(copy.age, 21);
assert_eq!(copy.email, "z@z.com");
}
#[test]
fn rebuild_result_is_registered_for_downstream_ops() {
use typeshaper::TypeshaperExt;
let copy = UserCopy { id: 7, name: "kai".into(), age: 30, email: "k@k.com".into() };
let public: UserCopyPublic = copy.project();
assert_eq!(public.id, 7);
assert_eq!(public.name, "kai");
}
#[test]
fn rebuild_private_field_visibility_is_preserved() {
let copy = UserCopy {
id: 1,
name: "test".into(),
age: 0,
email: "t@t.com".into(),
};
assert_eq!(copy.email, "t@t.com");
}
#[test]
fn merge_supports_project_via_tuple() {
let a = UserNoAge { id: 1, name: "x".into(), email: "x@y.com".into() };
let b = Badge { score: 7, label: "s".into() };
let combined: UserWithBadge = (a, b).project();
assert_eq!(combined.id, 1);
assert_eq!(combined.score, 7);
}