10 Commits

11 changed files with 323 additions and 178 deletions

View File

@@ -13,7 +13,7 @@ categories.workspace = true
readme.workspace = true readme.workspace = true
[workspace.package] [workspace.package]
version = "0.2.1" version = "0.4.0"
edition = "2021" edition = "2021"
rust-version = "1.81" rust-version = "1.81"
@@ -26,7 +26,7 @@ categories = ["api-bindings"]
readme = "README.md" readme = "README.md"
[dependencies] [dependencies]
aws-macros = { path = "./aws_macros", version = "0.2.*" } aws-macros = { path = "./aws_macros", version = "0.4.*" }
aws-config = { version = "1.*", default-features = false } aws-config = { version = "1.*", default-features = false }
aws-sdk-ec2 = { version = "1.*", default-features = false, features = [ aws-sdk-ec2 = { version = "1.*", default-features = false, features = [
"rustls", "rustls",
@@ -56,8 +56,15 @@ chrono = { version = "0.4.*", default-features = false, features = [
serde = { version = "1.*", default-features = false, features = [ serde = { version = "1.*", default-features = false, features = [
"std", "std",
"derive", "derive",
] } ], optional = true }
serde_json = { version = "1.*", default-features = false, features = ["std"] } serde_json = { version = "1.*", default-features = false, features = [
"std",
], optional = true }
[features]
default = []
serde = ["dep:serde"]
serde-tags = ["dep:serde", "dep:serde_json"]
[workspace] [workspace]
resolver = "2" resolver = "2"

View File

@@ -7,11 +7,12 @@ docs:
.PHONY: test .PHONY: test
test: test:
@cargo test --workspace --color=always cargo hack --feature-powerset --no-dev-deps check
cargo test --workspace --color=always
.PHONY: lint .PHONY: lint
lint: lint:
@cargo clippy --workspace --tests --color=always cargo clippy --workspace --tests --color=always
.PHONY: fmt .PHONY: fmt
fmt: fmt:

View File

@@ -1,5 +1,6 @@
# AWS # AWS
* Helper functions for frequently used AWS operations (create EC2 instance, Route53 record, ...) - Helper functions for frequently used AWS operations (create EC2 instance,
* Type-safe operations on AWS tags (e.g. from an EC2 instance), see [the separate README](./src/tags/README.md) Route53 record, ...)
- Type-safe operations on AWS tags (e.g. from an EC2 instance), see
[the separate README](./src/tags/README.md)

View File

@@ -29,6 +29,7 @@ syn = { version = "2.*", default-features = false, features = [
"parsing", "parsing",
"printing", "printing",
"clone-impls", "clone-impls",
"derive",
] } ] }
[lints] [lints]

View File

@@ -15,8 +15,7 @@ pub fn Tags(attr: TokenStream, item: TokenStream) -> TokenStream {
tags::transform(attr, item) tags::transform(attr, item)
} }
#[proc_macro_attribute] #[proc_macro_derive(Tag, attributes(tag))]
#[expect(non_snake_case, reason = "attribute proc macros should be capitalized")] pub fn tag(input: TokenStream) -> TokenStream {
pub fn Tag(attr: TokenStream, item: TokenStream) -> TokenStream { tag::transform(input)
tag::transform(attr, item)
} }

View File

@@ -18,19 +18,18 @@ enum Translator {
Transparent(TransparentKind), Transparent(TransparentKind),
} }
fn parse_enum_attributes(attrs: &mut Vec<syn::Attribute>) -> Option<syn::LitStr> { fn parse_enum_attributes(attrs: &[syn::Attribute]) -> Option<syn::LitStr> {
let index_of_tag_attribute = attrs let index_of_tag_attribute = attrs
.iter() .iter()
.enumerate() .filter(|attr| attr.style == syn::AttrStyle::Outer)
.filter(|&(_i, attr)| attr.style == syn::AttrStyle::Outer) .find_map(|attr| match attr.meta {
.find_map(|(i, attr)| match attr.meta {
syn::Meta::List(ref meta_list) => { syn::Meta::List(ref meta_list) => {
if let (Some(name), 1) = ( if let (Some(name), 1) = (
meta_list.path.segments.first(), meta_list.path.segments.first(),
meta_list.path.segments.len(), meta_list.path.segments.len(),
) { ) {
if name.ident == "tag" { if name.ident == "tag" {
Some((i, meta_list.clone())) Some(meta_list.clone())
} else { } else {
None None
} }
@@ -42,11 +41,7 @@ fn parse_enum_attributes(attrs: &mut Vec<syn::Attribute>) -> Option<syn::LitStr>
}); });
match index_of_tag_attribute { match index_of_tag_attribute {
Some((i, meta_list)) => { Some(meta_list) => {
// `i` came from `enumerate()` and is guaranteed to be in bounds
let removed_attribute = attrs.remove(i);
drop(removed_attribute);
let expr: syn::Expr = let expr: syn::Expr =
syn::parse(meta_list.tokens.into()).expect("expected expr in macro attribute"); syn::parse(meta_list.tokens.into()).expect("expected expr in macro attribute");
@@ -79,10 +74,10 @@ fn parse_enum_attributes(attrs: &mut Vec<syn::Attribute>) -> Option<syn::LitStr>
} }
} }
fn parse_transparent_enum(mut e: syn::ItemEnum) -> (Translator, syn::Item) { fn parse_transparent_enum(e: &syn::DataEnum) -> Translator {
let variants = e let variants = e
.variants .variants
.iter_mut() .iter()
.map(|variant| { .map(|variant| {
assert!( assert!(
variant.discriminant.is_none(), variant.discriminant.is_none(),
@@ -92,25 +87,16 @@ fn parse_transparent_enum(mut e: syn::ItemEnum) -> (Translator, syn::Item) {
syn::Fields::Unit => (), syn::Fields::Unit => (),
_ => panic!("enum cannot have fields in variants"), _ => panic!("enum cannot have fields in variants"),
} }
let rename = parse_enum_attributes(&mut variant.attrs); let rename = parse_enum_attributes(&variant.attrs);
(variant.ident.clone(), rename) (variant.ident.clone(), rename)
}) })
.collect::<Vec<(syn::Ident, Option<syn::LitStr>)>>(); .collect::<Vec<(syn::Ident, Option<syn::LitStr>)>>();
( Translator::Transparent(TransparentKind::SimpleEnum { variants })
Translator::Transparent(TransparentKind::SimpleEnum { variants }),
e.into(),
)
} }
pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { fn parse_tag_attribute(expr: syn::Expr, elem: &syn::Data) -> Translator {
let root = quote! {::aws_lib};
let expr: syn::Expr = syn::parse(attr).expect("expected expr in macro attribute");
let elem = syn::parse_macro_input!(item as syn::Item);
let syn::Expr::Assign(assign) = expr else { let syn::Expr::Assign(assign) = expr else {
panic!("invalid expression in macro attribute") panic!("invalid expression in macro attribute")
}; };
@@ -127,7 +113,7 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
_ => panic!("invalid expression in tag field attribute, left side"), _ => panic!("invalid expression in tag field attribute, left side"),
} }
let (translator, elem) = match *assign.right { match *assign.right {
syn::Expr::Path(ref exprpath) => { syn::Expr::Path(ref exprpath) => {
let segments = &exprpath.path.segments; let segments = &exprpath.path.segments;
let (Some(segment), 1) = (segments.first(), segments.len()) else { let (Some(segment), 1) = (segments.first(), segments.len()) else {
@@ -135,45 +121,71 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
}; };
match segment.ident.to_string().as_str() { match segment.ident.to_string().as_str() {
"serde" => (Translator::Serde, elem), "serde" => Translator::Serde,
"manual" => (Translator::Manual, elem), "manual" => Translator::Manual,
"transparent" => match elem { "transparent" =>
syn::Item::Struct(ref s) => match s.fields { {
syn::Fields::Unnamed(ref fields) => { #[expect(
let (Some(field), 1) = (fields.unnamed.first(), fields.unnamed.len()) clippy::match_wildcard_for_single_variants,
else { reason = "just by chance is there only one additional variant"
panic!( )]
"transparent translation is only available for newtype-style macros" match *elem {
) syn::Data::Struct(ref s) => match s.fields {
}; syn::Fields::Unnamed(ref fields) => {
( let (Some(field), 1) =
(fields.unnamed.first(), fields.unnamed.len())
else {
panic!(
"transparent translation is only available for newtype-style macros"
)
};
Translator::Transparent(TransparentKind::NewtypeStruct { Translator::Transparent(TransparentKind::NewtypeStruct {
ty: field.ty.clone(), ty: field.ty.clone(),
}), })
elem, }
) _ => panic!(
"transparent translation is only available for newtype-style macros"
),
},
syn::Data::Enum(ref e) => parse_transparent_enum(e),
_ => {
panic!("transparent translation is only available for newtype-style macros")
} }
_ => panic!(
"transparent translation is only available for newtype-style macros"
),
},
syn::Item::Enum(e) => parse_transparent_enum(e),
_ => {
panic!("transparent translation is only available for newtype-style macros")
} }
}, }
t => panic!("invalid translator {t}"), t => panic!("invalid translator {t}"),
} }
} }
_ => panic!("invalid expression in tag field attribute, left side"), _ => panic!("invalid expression in tag field attribute, left side"),
}; }
}
let name = match elem { pub(crate) fn transform(input: TokenStream) -> TokenStream {
syn::Item::Struct(ref s) => &s.ident, let root = quote! {::aws_lib};
syn::Item::Enum(ref e) => &e.ident,
_ => panic!("only applicable to structs/enums"), let input = syn::parse_macro_input!(input as syn::DeriveInput);
};
let expr = input
.attrs
.into_iter()
.find_map(|attr| match attr.meta {
syn::Meta::List(meta_list) => {
if meta_list.path.is_ident("tag") {
Some(
syn::parse2::<syn::Expr>(meta_list.tokens)
.expect("invalid expression in tag attribute"),
)
} else {
None
}
}
_ => None,
})
.expect("Tag derive macro requires a tag attribute");
let translator = parse_tag_attribute(expr, &input.data);
let name = input.ident;
let translator = match translator { let translator = match translator {
Translator::Serde => quote! { Translator::Serde => quote! {
@@ -209,7 +221,7 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
} }
} }
impl From<#name> for RawTagValue { impl From<#name> for #root::tags::RawTagValue {
fn from(value: #name) -> Self { fn from(value: #name) -> Self {
<#ty as #root::tags::TagValue<#ty>>::into_raw_tag(value.0) <#ty as #root::tags::TagValue<#ty>>::into_raw_tag(value.0)
} }
@@ -271,7 +283,6 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
}; };
quote! { quote! {
#elem
#translator #translator
} }
.into() .into()

View File

@@ -21,6 +21,26 @@ struct Element {
ty: syn::Path, ty: syn::Path,
kind: ElementKind, kind: ElementKind,
name: String, name: String,
attrs: Vec<syn::Attribute>,
}
fn is_cfg_attribute(attr: &syn::Attribute) -> bool {
match attr.meta {
syn::Meta::List(ref meta_list) => {
let segments = &meta_list.path.segments;
match (segments.first(), segments.len()) {
(Some(segment), 1) => segment.ident == "cfg",
_ => false,
}
}
_ => false,
}
}
fn cfg_attrs(v: &[syn::Attribute]) -> Vec<&syn::Attribute> {
v.iter()
.filter(|attr: &&syn::Attribute| is_cfg_attribute(attr))
.collect()
} }
fn parse_type(input: syn::Type) -> (syn::Path, ElementKind) { fn parse_type(input: syn::Type) -> (syn::Path, ElementKind) {
@@ -66,68 +86,73 @@ fn parse_type(input: syn::Type) -> (syn::Path, ElementKind) {
} }
} }
fn parse_field_attrs(attrs: &[syn::Attribute]) -> Option<String> { fn parse_field_attrs(attrs: &mut Vec<syn::Attribute>) -> Option<String> {
match (attrs.first(), attrs.len()) { let index_of_tag_attribute = attrs
(Some(attr), 1) => { .iter()
assert!( .enumerate()
attr.style == syn::AttrStyle::Outer, .filter(|&(_i, attr)| attr.style == syn::AttrStyle::Outer)
"field attribute style needs to be an outer attribute" .find_map(|(i, attr)| match attr.meta {
); syn::Meta::List(ref meta_list) => {
match attr.meta { let tag = &meta_list.path;
syn::Meta::List(ref meta_list) => { if let (Some(segment), 1) = (tag.segments.first(), tag.segments.len()) {
let tag = &meta_list.path; if segment.ident == "tag" {
let tag_name = match (tag.segments.first(), tag.segments.len()) { Some((i, meta_list.clone()))
(Some(segment), 1) => segment.ident.to_string(), } else {
(_, 0) => return None, None
_ => panic!("invalid field attribute path"),
};
assert!(tag_name == "tag", "invalid field attribute path {tag_name}");
let expr: syn::Expr = match meta_list.parse_args() {
Ok(expr) => expr,
Err(e) => panic!("failed parsing tag field attribute: {e}"),
};
let syn::Expr::Assign(assign) = expr else {
panic!("invalid expression in tag field attribute")
};
match *assign.left {
syn::Expr::Path(ref exprpath) => {
let segments = &exprpath.path.segments;
let (Some(segment), 1) = (segments.first(), segments.len()) else {
panic!("invalid tag field attribute key")
};
assert!(segment.ident == "key", "invalid tag field attribute key");
}
_ => panic!("invalid expression in tag field attribute, left side"),
}
match *assign.right {
syn::Expr::Lit(ref expr_lit) => match expr_lit.lit {
syn::Lit::Str(ref lit_str) => Some(lit_str.value()),
_ => panic!("right side of tag field not a string literal"),
},
_ => panic!("right side of tag field attribute not a literal"),
} }
} else {
None
} }
_ => panic!("invalid field attribute"), }
_ => None,
});
match index_of_tag_attribute {
Some((i, meta_list)) => {
let removed_attribute = attrs.remove(i);
drop(removed_attribute);
let expr: syn::Expr = match meta_list.parse_args() {
Ok(expr) => expr,
Err(e) => panic!("failed parsing tag field attribute: {e}"),
};
let syn::Expr::Assign(assign) = expr else {
panic!("invalid expression in tag field attribute")
};
match *assign.left {
syn::Expr::Path(ref exprpath) => {
let segments = &exprpath.path.segments;
let (Some(segment), 1) = (segments.first(), segments.len()) else {
panic!("invalid tag field attribute key")
};
assert!(segment.ident == "key", "invalid tag field attribute key");
}
_ => panic!("invalid expression in tag field attribute, left side"),
}
match *assign.right {
syn::Expr::Lit(ref expr_lit) => match expr_lit.lit {
syn::Lit::Str(ref lit_str) => Some(lit_str.value()),
_ => panic!("right side of tag field not a string literal"),
},
_ => panic!("right side of tag field attribute not a literal"),
} }
} }
(_, 0) => None, None => None,
_ => panic!("invalid field attributes"),
} }
} }
fn parse_fields(input: impl IntoIterator<Item = syn::Field>) -> Vec<Element> { fn parse_fields(input: impl IntoIterator<Item = syn::Field>) -> Vec<Element> {
let mut elements = Vec::new(); let mut elements = Vec::new();
for field in input { for mut field in input {
let ident = field.ident.expect("tuple structs not supported"); let ident = field.ident.expect("tuple structs not supported");
let vis = field.vis; let vis = field.vis;
let (ty, kind) = parse_type(field.ty); let (ty, kind) = parse_type(field.ty);
let name = parse_field_attrs(&field.attrs); let name = parse_field_attrs(&mut field.attrs);
elements.push(Element { elements.push(Element {
ident: ident.clone(), ident: ident.clone(),
@@ -135,6 +160,7 @@ fn parse_fields(input: impl IntoIterator<Item = syn::Field>) -> Vec<Element> {
ty, ty,
kind, kind,
name: name.unwrap_or_else(|| ident.to_string()), name: name.unwrap_or_else(|| ident.to_string()),
attrs: field.attrs,
}); });
} }
elements elements
@@ -165,14 +191,19 @@ fn build_output(input: Input) -> TokenStream {
let ident = &element.ident; let ident = &element.ident;
let vis = &element.vis; let vis = &element.vis;
let ty = &element.ty; let ty = &element.ty;
let attrs = &element.attrs;
match element.kind { match element.kind {
ElementKind::Required => { ElementKind::Required => {
quote!( quote!(
#(#attrs)
*
#vis #ident: #ty #vis #ident: #ty
) )
} }
ElementKind::Optional => { ElementKind::Optional => {
quote!( quote!(
#(#attrs)
*
#vis #ident: ::std::option::Option<#ty> #vis #ident: ::std::option::Option<#ty>
) )
} }
@@ -191,9 +222,18 @@ fn build_output(input: Input) -> TokenStream {
let params = input.elements.iter().map(|element| { let params = input.elements.iter().map(|element| {
let ident = &element.ident; let ident = &element.ident;
let ty = &element.ty; let ty = &element.ty;
let attrs = cfg_attrs(&element.attrs);
match element.kind { match element.kind {
ElementKind::Required => quote!(#ident: #ty), ElementKind::Required => quote! {
ElementKind::Optional => quote!(#ident: ::std::option::Option<#ty>), #(#attrs)
*
#ident: #ty
},
ElementKind::Optional => quote! {
#(#attrs)
*
#ident: ::std::option::Option<#ty>
},
} }
}); });
@@ -202,16 +242,22 @@ fn build_output(input: Input) -> TokenStream {
.iter() .iter()
.map(|element| { .map(|element| {
let ident = &element.ident; let ident = &element.ident;
quote! {#ident: #ident} let attrs = cfg_attrs(&element.attrs);
quote! {
#(#attrs)
*
#ident: #ident
}
}) })
.collect(); .collect();
let from_tags_fields: Vec<proc_macro2::TokenStream>= input.elements.iter().map(|element| { let from_tags_fields: Vec<proc_macro2::TokenStream> = input.elements.iter().map(|element| {
let ident = &element.ident; let ident = &element.ident;
let ty = &element.ty; let ty = &element.ty;
let tag_name = &element.name; let tag_name = &element.name;
let attrs = cfg_attrs(&element.attrs);
let try_convert = quote!{ let try_convert = quote! {
let value: ::std::result::Result<#ty, #root::tags::ParseTagsError> = <#ty as #root::tags::TagValue<#ty>>::from_raw_tag(value) let value: ::std::result::Result<#ty, #root::tags::ParseTagsError> = <#ty as #root::tags::TagValue<#ty>>::from_raw_tag(value)
.map_err( .map_err(
|e| #root::tags::ParseTagsError::ParseTag(#root::tags::ParseTagError::InvalidTagValue { |e| #root::tags::ParseTagsError::ParseTag(#root::tags::ParseTagError::InvalidTagValue {
@@ -261,6 +307,8 @@ fn build_output(input: Input) -> TokenStream {
}; };
quote! { quote! {
#(#attrs)
*
#ident: { #ident: {
let key: #root::tags::TagKey = #root::tags::TagKey::new(#tag_name.to_owned()); let key: #root::tags::TagKey = #root::tags::TagKey::new(#tag_name.to_owned());
@@ -286,9 +334,12 @@ fn build_output(input: Input) -> TokenStream {
let ident = &element.ident; let ident = &element.ident;
let ty= &element.ty; let ty= &element.ty;
let tag_name = &element.name; let tag_name = &element.name;
let attrs= &element.attrs;
match element.kind { match element.kind {
ElementKind::Required => { ElementKind::Required => {
quote! { quote! {
#(#attrs)
*
{ {
let key = #root::tags::TagKey::new(#tag_name.to_owned()); let key = #root::tags::TagKey::new(#tag_name.to_owned());
let value: #root::tags::RawTagValue = <#ty as #root::tags::TagValue<#ty>>::into_raw_tag(self.#ident); let value: #root::tags::RawTagValue = <#ty as #root::tags::TagValue<#ty>>::into_raw_tag(self.#ident);
@@ -298,6 +349,8 @@ fn build_output(input: Input) -> TokenStream {
} }
ElementKind::Optional => { ElementKind::Optional => {
quote! { quote! {
#(#attrs)
*
{ {
match self.#ident { match self.#ident {
::std::option::Option::Some(value) => { ::std::option::Option::Some(value) => {

View File

@@ -11,6 +11,7 @@ use std::{
use aws_config::retry::RetryConfig; use aws_config::retry::RetryConfig;
use aws_sdk_ec2::client::Waiters; use aws_sdk_ec2::client::Waiters;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
mod error; mod error;
@@ -40,6 +41,7 @@ macro_rules! wrap_aws_enum {
} }
} }
#[cfg(feature = "serde")]
impl Serialize for $name { impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@@ -49,6 +51,7 @@ macro_rules! wrap_aws_enum {
} }
} }
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for $name { impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
@@ -196,11 +199,12 @@ impl Instance {
} }
} }
#[derive(Serialize, Deserialize, Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Region { pub enum Region {
#[serde(rename = "eu-central-1")] #[cfg_attr(feature = "serde", serde(rename = "eu-central-1"))]
EuCentral1, EuCentral1,
#[serde(rename = "us-east-1")] #[cfg_attr(feature = "serde", serde(rename = "us-east-1"))]
UsEast1, UsEast1,
} }
@@ -269,7 +273,8 @@ pub struct RegionClient {
pub cdn: RegionClientCdn, pub cdn: RegionClientCdn,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct InstanceProfileName(String); pub struct InstanceProfileName(String);
impl InstanceProfileName { impl InstanceProfileName {
@@ -278,7 +283,8 @@ impl InstanceProfileName {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct InstanceKeypairName(String); pub struct InstanceKeypairName(String);
impl InstanceKeypairName { impl InstanceKeypairName {
@@ -287,7 +293,8 @@ impl InstanceKeypairName {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct SecurityGroupId(String); pub struct SecurityGroupId(String);
impl SecurityGroupId { impl SecurityGroupId {
@@ -296,12 +303,14 @@ impl SecurityGroupId {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct SecurityGroup { pub struct SecurityGroup {
id: SecurityGroupId, id: SecurityGroupId,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct SubnetId(String); pub struct SubnetId(String);
impl SubnetId { impl SubnetId {
@@ -328,8 +337,9 @@ impl fmt::Display for SubnetId {
macro_rules! string_newtype { macro_rules! string_newtype {
($name:ident) => { ($name:ident) => {
#[Tag(translate = serde)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq, Tag)]
#[tag(translate = transparent)]
pub struct $name(String); pub struct $name(String);
impl std::fmt::Display for $name { impl std::fmt::Display for $name {
@@ -342,7 +352,8 @@ macro_rules! string_newtype {
string_newtype!(AvailabilityZone); string_newtype!(AvailabilityZone);
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Subnet { pub struct Subnet {
pub id: SubnetId, pub id: SubnetId,
pub availability_zone: AvailabilityZone, pub availability_zone: AvailabilityZone,
@@ -383,7 +394,8 @@ impl AmiId {
} }
} }
#[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug)]
pub struct Ami { pub struct Ami {
pub id: AmiId, pub id: AmiId,
pub tags: TagList, pub tags: TagList,
@@ -410,8 +422,9 @@ impl TryFrom<aws_sdk_ec2::types::Image> for Ami {
} }
} }
#[Tag(translate = manual)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] #[derive(Tag, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
#[tag(translate = manual)]
pub struct Timestamp(DateTime<Utc>); pub struct Timestamp(DateTime<Utc>);
impl Timestamp { impl Timestamp {
@@ -468,7 +481,8 @@ impl TryFrom<RawImageCreationDate> for Timestamp {
} }
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Ip(net::IpAddr); pub struct Ip(net::IpAddr);
impl Ip { impl Ip {
@@ -499,7 +513,8 @@ impl EipAllocationId {
} }
} }
#[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug)]
pub struct Eip { pub struct Eip {
pub allocation_id: EipAllocationId, pub allocation_id: EipAllocationId,
pub ip: Ip, pub ip: Ip,
@@ -568,8 +583,9 @@ impl Eip {
string_newtype!(CloudfrontDistributionId); string_newtype!(CloudfrontDistributionId);
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[serde(rename_all = "lowercase")] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum CloudfrontDistributionStatus { pub enum CloudfrontDistributionStatus {
Deployed, Deployed,
Other(String), Other(String),
@@ -597,10 +613,12 @@ impl From<String> for CloudfrontDistributionStatus {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct EfsId(String); pub struct EfsId(String);
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Efs { pub struct Efs {
id: EfsId, id: EfsId,
region: Region, region: Region,
@@ -633,7 +651,8 @@ impl From<String> for CloudfrontDistributionDomain {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct CloudfrontOrigin { pub struct CloudfrontOrigin {
id: CloudfrontOriginId, id: CloudfrontOriginId,
domain: CloudfrontOriginDomain, domain: CloudfrontOriginDomain,
@@ -643,6 +662,10 @@ impl CloudfrontOrigin {
pub const fn id(&self) -> &CloudfrontOriginId { pub const fn id(&self) -> &CloudfrontOriginId {
&self.id &self.id
} }
pub const fn domain(&self) -> &CloudfrontOriginDomain {
&self.domain
}
} }
string_newtype!(CloudfrontOriginId); string_newtype!(CloudfrontOriginId);
@@ -675,7 +698,8 @@ impl From<aws_sdk_cloudfront::types::Origin> for CloudfrontOrigin {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct CloudfrontDistribution { pub struct CloudfrontDistribution {
pub id: CloudfrontDistributionId, pub id: CloudfrontDistributionId,
pub status: CloudfrontDistributionStatus, pub status: CloudfrontDistributionStatus,
@@ -789,7 +813,8 @@ pub async fn load_sdk_clients<const C: usize>(
region_clients region_clients
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Account { pub struct Account {
id: String, id: String,
} }
@@ -804,7 +829,8 @@ impl Account {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct HostedZoneId(String); pub struct HostedZoneId(String);
impl HostedZoneId { impl HostedZoneId {
@@ -817,7 +843,8 @@ impl HostedZoneId {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Route53Zone { pub struct Route53Zone {
hosted_zone_id: HostedZoneId, hosted_zone_id: HostedZoneId,
name: String, name: String,
@@ -846,6 +873,14 @@ impl Route53Zone {
name, name,
} }
} }
pub const fn hosted_zone_id(&self) -> &HostedZoneId {
&self.hosted_zone_id
}
pub fn name(&self) -> &str {
&self.name
}
} }
impl From<aws_sdk_route53::types::HostedZone> for Route53Zone { impl From<aws_sdk_route53::types::HostedZone> for Route53Zone {

View File

@@ -51,19 +51,15 @@ strategies:
- `impl From<T> for RawTagValue`: How to turn `T` back into the value of a - `impl From<T> for RawTagValue`: How to turn `T` back into the value of a
tag. This cannot fail. tag. This cannot fail.
There is a [`macro@Tag`] macro that selects the strategy, which can then be used There is a [`macro@Tag`] derive macro that selects the strategy, which can then
in a struct that is using `#[Tags]`: be used in a struct that is using `#[Tags]`:
```rust ```rust
use aws_lib::tags::{Tag, Tags}; use aws_lib::tags::{Tag, Tags};
use serde::{Serialize, Deserialize};
#[Tag(translate = serde)] #[derive(Tag, Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[tag(translate = transparent)]
struct MyTag { struct MyTag(String);
foo: String,
bar: bool,
}
#[Tags] #[Tags]
struct MyTags { struct MyTags {

View File

@@ -1,7 +1,12 @@
#![doc = include_str!("README.md")] #![doc = include_str!("README.md")]
use std::fmt::{self, Debug}; use std::fmt::{self, Debug};
use serde::{de::DeserializeOwned, Deserialize, Serialize}; #[cfg(feature = "serde-tags")]
use serde::de::DeserializeOwned;
#[cfg(feature = "serde")]
use serde::Deserialize;
#[cfg(any(feature = "serde-tags", feature = "serde"))]
use serde::Serialize;
mod error; mod error;
mod helpers; mod helpers;
@@ -43,10 +48,12 @@ where
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct RawTagValue(String); pub struct RawTagValue(String);
helpers::impl_string_wrapper!(RawTagValue); helpers::impl_string_wrapper!(RawTagValue);
#[cfg(feature = "serde-tags")]
pub struct TranslateSerde; pub struct TranslateSerde;
pub struct TranslateManual; pub struct TranslateManual;
@@ -57,6 +64,7 @@ pub trait Translator<S: ?Sized, T> {
fn into_raw_tag(value: T) -> RawTagValue; fn into_raw_tag(value: T) -> RawTagValue;
} }
#[cfg(feature = "serde-tags")]
pub trait TranslatableSerde: Serialize + DeserializeOwned {} pub trait TranslatableSerde: Serialize + DeserializeOwned {}
pub trait TranslatableManual: pub trait TranslatableManual:
TryFrom<RawTagValue, Error: Into<ParseTagValueError>> + Into<RawTagValue> TryFrom<RawTagValue, Error: Into<ParseTagValueError>> + Into<RawTagValue>
@@ -76,6 +84,7 @@ pub trait TagValue<V> {
} }
} }
#[cfg(feature = "serde-tags")]
impl<S, T> Translator<S, T> for TranslateSerde impl<S, T> Translator<S, T> for TranslateSerde
where where
T: TranslatableSerde, T: TranslatableSerde,
@@ -118,7 +127,8 @@ where
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RawTag { pub struct RawTag {
key: TagKey, key: TagKey,
value: RawTagValue, value: RawTagValue,
@@ -155,7 +165,8 @@ where
} }
} }
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct TagKey(String); pub struct TagKey(String);
helpers::impl_string_wrapper!(TagKey); helpers::impl_string_wrapper!(TagKey);
@@ -252,7 +263,8 @@ where
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TagList(Vec<RawTag>); pub struct TagList(Vec<RawTag>);
impl TagList { impl TagList {
@@ -292,19 +304,21 @@ impl TagList {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[cfg(feature = "serde-tags")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::*; use super::*;
#[Tag(translate = manual)] #[derive(Tag, Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)] #[tag(translate = manual)]
enum MyTag { enum MyTag {
A, A,
B, B,
} }
#[Tag(translate = serde)] #[cfg(feature = "serde-tags")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Tag, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[tag(translate = serde)]
struct MyStructTag { struct MyStructTag {
foo: String, foo: String,
bar: bool, bar: bool,
@@ -358,7 +372,9 @@ mod tests {
#[tag(key = "anothername")] #[tag(key = "anothername")]
tag6: Option<MyTag>, tag6: Option<MyTag>,
tag7: Option<MyTag>, tag7: Option<MyTag>,
#[cfg(feature = "serde-tags")]
tag8: MyStructTag, tag8: MyStructTag,
#[cfg(feature = "serde-tags")]
tag9: Option<MyStructTag>, tag9: Option<MyStructTag>,
} }
@@ -368,6 +384,7 @@ mod tests {
RawTag::new("tag3".to_owned(), "false".to_owned()), RawTag::new("tag3".to_owned(), "false".to_owned()),
RawTag::new("myname".to_owned(), "A".to_owned()), RawTag::new("myname".to_owned(), "A".to_owned()),
RawTag::new("anothername".to_owned(), "B".to_owned()), RawTag::new("anothername".to_owned(), "B".to_owned()),
#[cfg(feature = "serde-tags")]
RawTag::new("tag8".to_owned(), r#"{"foo":"hi","bar":false}"#.to_owned()), RawTag::new("tag8".to_owned(), r#"{"foo":"hi","bar":false}"#.to_owned()),
]); ]);
@@ -380,6 +397,7 @@ mod tests {
assert!(tags.tag5 == MyTag::A); assert!(tags.tag5 == MyTag::A);
assert!(tags.tag6 == Some(MyTag::B)); assert!(tags.tag6 == Some(MyTag::B));
assert!(tags.tag7.is_none()); assert!(tags.tag7.is_none());
#[cfg(feature = "serde-tags")]
assert!( assert!(
tags.tag8 tags.tag8
== MyStructTag { == MyStructTag {
@@ -387,6 +405,7 @@ mod tests {
bar: false bar: false
} }
); );
#[cfg(feature = "serde-tags")]
assert!(tags.tag9.is_none()); assert!(tags.tag9.is_none());
let into_tags = tags.into_tags(); let into_tags = tags.into_tags();
@@ -399,6 +418,7 @@ mod tests {
RawTag::new("tag3".to_owned(), "false".to_owned()), RawTag::new("tag3".to_owned(), "false".to_owned()),
RawTag::new("myname".to_owned(), "A".to_owned()), RawTag::new("myname".to_owned(), "A".to_owned()),
RawTag::new("anothername".to_owned(), "B".to_owned()), RawTag::new("anothername".to_owned(), "B".to_owned()),
#[cfg(feature = "serde-tags")]
RawTag::new("tag8".to_owned(), r#"{"foo":"hi","bar":false}"#.to_owned(),), RawTag::new("tag8".to_owned(), r#"{"foo":"hi","bar":false}"#.to_owned(),),
]) ])
); );
@@ -406,8 +426,8 @@ mod tests {
#[test] #[test]
fn test_transparent_tag() { fn test_transparent_tag() {
#[Tag(translate = transparent)] #[derive(Tag, PartialEq, Debug)]
#[derive(Serialize, Deserialize, PartialEq, Debug)] #[tag(translate = transparent)]
struct MyTag(String); struct MyTag(String);
assert_eq!( assert_eq!(
@@ -422,8 +442,8 @@ mod tests {
#[test] #[test]
fn test_enums() { fn test_enums() {
#[Tag(translate = transparent)] #[derive(PartialEq, Debug, Tag)]
#[derive(PartialEq, Debug)] #[tag(translate = transparent)]
enum MyCoolioTag { enum MyCoolioTag {
A, A,
#[tag(rename = "C")] #[tag(rename = "C")]

View File

@@ -1,14 +1,35 @@
use super::{ use super::{ParseTagValueError, RawTagValue, TagValue, TranslatableManual, TranslateManual};
ParseTagValueError, TagValue, TranslatableManual, TranslatableSerde, TranslateManual,
TranslateSerde,
};
// Bools can just be handled by serde. impl TranslatableManual for bool {}
impl TranslatableSerde for bool {}
const TRUE_STR: &str = "true";
const FALSE_STR: &str = "false";
impl TagValue<Self> for bool { impl TagValue<Self> for bool {
type Error = ParseTagValueError; type Error = ParseTagValueError;
type Translator = TranslateSerde; type Translator = TranslateManual;
}
impl TryFrom<RawTagValue> for bool {
type Error = ParseTagValueError;
fn try_from(value: RawTagValue) -> Result<Self, Self::Error> {
match value.as_str() {
TRUE_STR => Ok(true),
FALSE_STR => Ok(false),
_ => Err(ParseTagValueError::InvalidBoolValue { value }),
}
}
}
impl From<bool> for RawTagValue {
fn from(value: bool) -> Self {
if value {
Self::new(TRUE_STR.to_owned())
} else {
Self::new(FALSE_STR.to_owned())
}
}
} }
// Due to quoting, we cannot use serde here. It would produce quoted // Due to quoting, we cannot use serde here. It would produce quoted