Skip to content

Commit

Permalink
Implement replace_segment attribute (#197)
Browse files Browse the repository at this point in the history
* Implement `replace_segment` attribute

This implements the `replace_segment` attribute. This attribute can be
used to rename one or multiple segments returned by `TypeInfo::path`. It
could be for example be used to rename a changed crate name. For more
info see the examples and the docs in this pr.

* Update trybuild

* Directly pass the ident str.

* Update changelog
  • Loading branch information
bkchr authored Oct 20, 2023
1 parent ec8d941 commit 68c85ee
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 35 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.10.0] - 2023-10-20

### Added
- Implement `replace_segment` attribute [(#197)](https://github.com/paritytech/scale-info/pull/197)

## [2.9.0] - 2023-07-03

### Changed
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "scale-info"
version = "2.9.0"
version = "2.10.0"
authors = ["Parity Technologies <[email protected]>"]
edition = "2021"
rust-version = "1.60.0"
Expand All @@ -17,7 +17,7 @@ include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"]
[dependencies]
bitvec = { version = "1", default-features = false, features = ["alloc"], optional = true }
cfg-if = "1.0"
scale-info-derive = { version = "2.9.0", path = "derive", default-features = false, optional = true }
scale-info-derive = { version = "2.10.0", path = "derive", default-features = false, optional = true }
serde = { version = "1", default-features = false, optional = true, features = ["derive", "alloc"] }
derive_more = { version = "0.99.1", default-features = false, features = ["from"] }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
Expand Down
2 changes: 1 addition & 1 deletion derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "scale-info-derive"
version = "2.9.0"
version = "2.10.0"
authors = [
"Parity Technologies <[email protected]>",
"Centrality Developers <[email protected]>",
Expand Down
61 changes: 51 additions & 10 deletions derive/src/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use syn::{
parse::{Parse, ParseBuffer},
punctuated::Punctuated,
spanned::Spanned,
Token,
LitStr, Token,
};

const SCALE_INFO: &str = "scale_info";
Expand All @@ -26,6 +26,7 @@ mod keywords {
syn::custom_keyword!(bounds);
syn::custom_keyword!(skip_type_params);
syn::custom_keyword!(capture_docs);
syn::custom_keyword!(replace_segment);
}

/// Parsed and validated set of `#[scale_info(...)]` attributes for an item.
Expand All @@ -34,6 +35,7 @@ pub struct Attributes {
skip_type_params: Option<SkipTypeParamsAttr>,
capture_docs: Option<CaptureDocsAttr>,
crate_path: Option<CratePathAttr>,
replace_segments: Vec<ReplaceSegment>,
}

impl Attributes {
Expand All @@ -43,6 +45,7 @@ impl Attributes {
let mut skip_type_params = None;
let mut capture_docs = None;
let mut crate_path = None;
let mut replace_segments = Vec::new();

let attributes_parser = |input: &ParseBuffer| {
let attrs: Punctuated<ScaleInfoAttr, Token![,]> =
Expand Down Expand Up @@ -86,7 +89,6 @@ impl Attributes {
}
capture_docs = Some(parsed_capture_docs);
}

ScaleInfoAttr::CratePath(parsed_crate_path) => {
if crate_path.is_some() {
return Err(syn::Error::new(
Expand All @@ -97,6 +99,9 @@ impl Attributes {

crate_path = Some(parsed_crate_path);
}
ScaleInfoAttr::ReplaceSegment(replace_segment) => {
replace_segments.push(replace_segment);
}
}
}
}
Expand Down Expand Up @@ -127,6 +132,7 @@ impl Attributes {
skip_type_params,
capture_docs,
crate_path,
replace_segments,
})
}

Expand All @@ -153,6 +159,11 @@ impl Attributes {
pub fn crate_path(&self) -> Option<&CratePathAttr> {
self.crate_path.as_ref()
}

/// Returns an iterator over the `#[scale_info(replace_segment("Hello", "world"))]` attributes.
pub fn replace_segments(&self) -> impl Iterator<Item = &ReplaceSegment> {
self.replace_segments.iter()
}
}

/// Parsed representation of the `#[scale_info(bounds(...))]` attribute.
Expand Down Expand Up @@ -266,29 +277,59 @@ impl Parse for CratePathAttr {
}
}

/// Parsed representation of the `#[scale_info(replace_segment("Hello", "world"))]` attribute.
#[derive(Clone)]
pub struct ReplaceSegment {
search: LitStr,
replace: LitStr,
}

impl ReplaceSegment {
pub fn search(&self) -> &LitStr {
&self.search
}

pub fn replace(&self) -> &LitStr {
&self.replace
}
}

impl Parse for ReplaceSegment {
fn parse(input: &ParseBuffer) -> syn::Result<Self> {
input.parse::<keywords::replace_segment>()?;
let content;
syn::parenthesized!(content in input);

let search = content.parse::<LitStr>()?;
content.parse::<Token![,]>()?;
let replace = content.parse::<LitStr>()?;

Ok(Self { search, replace })
}
}

/// Parsed representation of one of the `#[scale_info(..)]` attributes.
pub enum ScaleInfoAttr {
Bounds(BoundsAttr),
SkipTypeParams(SkipTypeParamsAttr),
CaptureDocs(CaptureDocsAttr),
CratePath(CratePathAttr),
ReplaceSegment(ReplaceSegment),
}

impl Parse for ScaleInfoAttr {
fn parse(input: &ParseBuffer) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(keywords::bounds) {
let bounds = input.parse()?;
Ok(Self::Bounds(bounds))
Ok(Self::Bounds(input.parse()?))
} else if lookahead.peek(keywords::skip_type_params) {
let skip_type_params = input.parse()?;
Ok(Self::SkipTypeParams(skip_type_params))
Ok(Self::SkipTypeParams(input.parse()?))
} else if lookahead.peek(keywords::capture_docs) {
let capture_docs = input.parse()?;
Ok(Self::CaptureDocs(capture_docs))
Ok(Self::CaptureDocs(input.parse()?))
} else if lookahead.peek(Token![crate]) {
let crate_path = input.parse()?;
Ok(Self::CratePath(crate_path))
Ok(Self::CratePath(input.parse()?))
} else if lookahead.peek(keywords::replace_segment) {
Ok(Self::ReplaceSegment(input.parse()?))
} else {
Err(lookahead.error())
}
Expand Down
14 changes: 13 additions & 1 deletion derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ impl TypeInfoImpl {

fn expand(&self) -> Result<TokenStream2> {
let ident = &self.ast.ident;
let ident_str = ident.to_string();
let scale_info = crate_path(self.attrs.crate_path())?;

let where_clause = trait_bounds::make_where_clause(
Expand Down Expand Up @@ -101,12 +102,23 @@ impl TypeInfoImpl {
};
let docs = self.generate_docs(&self.ast.attrs);

let replaces = self.attrs.replace_segments().map(|r| {
let search = r.search();
let replace = r.replace();

quote!(( #search, #replace ))
});

Ok(quote! {
impl #impl_generics #scale_info::TypeInfo for #ident #ty_generics #where_clause {
type Identity = Self;
fn type_info() -> #scale_info::Type {
#scale_info::Type::builder()
.path(#scale_info::Path::new(::core::stringify!(#ident), ::core::module_path!()))
.path(#scale_info::Path::new_with_replace(
#ident_str,
::core::module_path!(),
&[ #( #replaces ),* ]
))
.type_params(#scale_info::prelude::vec![ #( #type_params ),* ])
#docs
.#build_type
Expand Down
24 changes: 24 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,30 @@
//! }
//! ```
//!
//! #### `#[scale_info(replace_segment("search", "replace"))]`
//!
//! Specify to rename a segment in the `path` returned by the [`TypeInfo::path`] function.
//! Normally the path is generated by using the `module_path!` macro. This path includes
//! the crate name and all the modules up to the declaration of the type. Sometimes it
//! is useful to replace one of these segments to ensure that for example a renaming
//! of the crate isn't propagated to downstream users. Be aware that if a `crate-name`
//! contains an hypen, the actual segment is `crate_name`. The `replace` name needs
//! to be a valid Rust identifier. The attribute is allowed to be passed multiple
//! times to replace multiple segments.
//!
//! Example:
//! ```ignore
//! use scale_info_reexport::info::TypeInfo;
//!
//! #[derive(TypeInfo)]
//! #[scale_info(replace_segment("something", "better_name"))]
//! #[scale_info(replace_segment("TestEnum", "BetterEnumName"))]
//! enum TestEnum {
//! FirstVariant,
//! SecondVariant,
//! }
//! ```
//!
//! # Forms
//!
//! To bridge between compile-time type information and runtime the
Expand Down
4 changes: 3 additions & 1 deletion src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ cfg_if! {
sync,
time,
vec,
rc
rc,
iter,
};
} else {
pub use alloc::{
Expand All @@ -64,6 +65,7 @@ cfg_if! {
num,
ops,
time,
iter,
};
}
}
31 changes: 28 additions & 3 deletions src/ty/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use crate::prelude::{
fmt::{Display, Error as FmtError, Formatter},
iter,
vec::Vec,
};

Expand Down Expand Up @@ -84,9 +85,33 @@ impl Path<MetaForm> {
///
/// - If the type identifier or module path contain invalid Rust identifiers
pub fn new(ident: &'static str, module_path: &'static str) -> Path {
let mut segments = module_path.split("::").collect::<Vec<_>>();
segments.push(ident);
Self::from_segments(segments).expect("All path segments should be valid Rust identifiers")
let segments = module_path.split("::");
Self::from_segments(segments.chain(iter::once(ident)))
.expect("All path segments should be valid Rust identifiers")
}

/// Create a new Path
///
/// The `segment_replace` is a list of `(search, replace)` items. Every
/// `search` item that appears in the `module_path` is replaced by the
/// `replace` item. This can be used for example to replace the crate name
/// or even the name of the type in the final [`Path`].
///
/// # Panics
///
/// - If the type identifier, module path or replace contain invalid Rust identifiers
pub fn new_with_replace(
ident: &'static str,
module_path: &'static str,
segment_replace: &[(&'static str, &'static str)],
) -> Path {
let segments = module_path.split("::");
Self::from_segments(
segments
.chain(iter::once(ident))
.map(|s| segment_replace.iter().find(|r| s == r.0).map_or(s, |r| r.1)),
)
.expect("All path segments should be valid Rust identifiers")
}

/// Create a Path from the given segments
Expand Down
50 changes: 50 additions & 0 deletions test_suite/tests/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -857,3 +857,53 @@ fn strips_invisible_delimiters_from_type_name() {

assert_type!(S, expected);
}

#[test]
fn replace_segments_works() {
#[allow(unused)]
#[derive(TypeInfo)]
#[scale_info(replace_segment("derive", "hey"))]
pub struct S;

let ty = Type::builder()
.path(Path::new("S", "hey"))
.composite(Fields::unit());

assert_type!(S, ty);

mod nested {
#[allow(unused)]
#[derive(info::TypeInfo)]
#[scale_info(replace_segment("derive", "hey"))]
#[scale_info(replace_segment("nested", "what"))]
pub struct S;

#[allow(unused)]
#[derive(info::TypeInfo)]
#[scale_info(replace_segment("nested", "what"))]
pub struct R;
}

let ty = Type::builder()
.path(Path::new("S", "hey::what"))
.composite(Fields::unit());

assert_type!(nested::S, ty);

let ty = Type::builder()
.path(Path::new("R", "derive::what"))
.composite(Fields::unit());

assert_type!(nested::R, ty);

#[allow(unused)]
#[derive(info::TypeInfo)]
#[scale_info(replace_segment("R", "AWESOME"))]
pub struct R;

let ty = Type::builder()
.path(Path::new("AWESOME", "derive"))
.composite(Fields::unit());

assert_type!(R, ty);
}
16 changes: 8 additions & 8 deletions test_suite/tests/ui/fail_missing_derive.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ error[E0277]: the trait bound `PawType<u16>: TypeInfo` is not satisfied
| ^^^^^^^^^^^^^^^^^^ the trait `TypeInfo` is not implemented for `PawType<u16>`
|
= help: the following other types implement trait `TypeInfo`:
&T
&mut T
()
(A, B)
(A, B, C)
(A, B, C, D)
(A, B, C, D, E)
(A, B, C, D, E, F)
bool
char
i8
i16
i32
i64
i128
u8
and $N others
note: required for `Cat<bool, u8, u16>` to implement `TypeInfo`
--> tests/ui/fail_missing_derive.rs:8:10
Expand Down
Loading

0 comments on commit 68c85ee

Please sign in to comment.