Skip to content

Commit fbc27f7

Browse files
committed
champion.rs build.rs proof of concept
1 parent a571252 commit fbc27f7

File tree

20 files changed

+20994
-1278
lines changed

20 files changed

+20994
-1278
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@
22
**/*.rs.bk
33
/doc
44
apikey.txt
5-

Cargo.lock

Lines changed: 22 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

riven/Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ readme = "../README.md"
88
license = "MIT"
99
edition = "2018"
1010
rust-version = "1.71.1"
11-
include = ["src/**", "../README.md", "/examples"]
11+
include = ["src/**", "schemas", "build", "examples", "../README.md"]
1212
keywords = ["riot-games", "riot", "league", "league-of-legends"]
1313
categories = ["api-bindings", "web-programming::http-client", "wasm"]
14+
build = "build/main.rs"
1415

1516
[lib]
1617
crate-type = ["cdylib", "rlib"]
@@ -90,3 +91,13 @@ console_error_panic_hook = "0.1"
9091
console_log = "1.0"
9192
wasm-bindgen = "0.2.70"
9293
wasm-bindgen-test = "0.3"
94+
95+
[build-dependencies]
96+
heck = "0.5.0"
97+
prettyplease = "0.2.30"
98+
proc-macro2 = "1.0.93"
99+
quote = "1.0.38"
100+
serde = { version = "1.0.85", features = ["derive"] }
101+
serde_derive = "1.0.85"
102+
serde_json = "1.0.1"
103+
syn = "2.0.96"

riven/build/champion.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
use std::collections::BTreeMap;
2+
3+
use heck::ToShoutySnakeCase;
4+
use proc_macro2::{Ident, Literal, Span};
5+
use quote::quote;
6+
use syn::{parse_quote, File};
7+
8+
/// ```json
9+
/// {
10+
/// "id": 910,
11+
/// "name": "Hwei",
12+
/// "alias": "Hwei",
13+
/// "squarePortraitPath": "/lol-game-data/assets/v1/champion-icons/910.png",
14+
/// "roles": [
15+
/// "mage",
16+
/// "support"
17+
/// ]
18+
/// }
19+
/// ```
20+
#[derive(serde::Deserialize)]
21+
#[allow(dead_code)]
22+
pub struct Champion {
23+
pub id: i16,
24+
pub name: String,
25+
pub alias: String,
26+
pub square_portrait_path: Option<String>,
27+
pub roles: Vec<String>,
28+
}
29+
30+
pub fn champion() -> File {
31+
let mut champions: Vec<Champion> =
32+
serde_json::from_str(include_str!("../schemas/.champion.json")).unwrap();
33+
champions.retain(|champion| champion.id >= 0);
34+
35+
let newtype_enum = {
36+
let doc_rows = champions.iter().map(|champion| {
37+
let id = champion.id;
38+
let name = &champion.name;
39+
let alias = &champion.alias;
40+
format!("`{}` | {} | {} | {}", id, name, alias, id)
41+
});
42+
43+
let variants = champions.iter().map(|champion| {
44+
let doc = format!("`{}`", champion.id);
45+
let ident = Ident::new(&champion.alias.to_shouty_snake_case(), Span::call_site());
46+
let id = Literal::i16_unsuffixed(champion.id);
47+
quote! {
48+
#[doc = #doc]
49+
#ident = #id,
50+
}
51+
});
52+
53+
quote! {
54+
newtype_enum! {
55+
/// A League of Legends champion.
56+
///
57+
/// This newtype acts as a C-like enum; each variant corresponds to an
58+
/// integer value. Using a newtype allows _unknown_ variants to be
59+
/// represented. This is important when Riot adds new champions.
60+
///
61+
/// Field | Name | Identifier | Id
62+
/// ---|---|---|---
63+
/// `NONE` | None (no ban) | | -1
64+
#( #[doc = #doc_rows ] )*
65+
pub newtype_enum Champion(i16) {
66+
/// `-1`, none. Appears when a champion ban is not used in champ select.
67+
NONE = -1,
68+
#( #variants )*
69+
}
70+
}
71+
}
72+
};
73+
74+
let champion_impl = {
75+
let id_cases = champions.iter().map(|champion| {
76+
let id = Literal::i16_unsuffixed(champion.id);
77+
let ident = Ident::new(&champion.alias.to_shouty_snake_case(), Span::call_site());
78+
quote! {
79+
Self::#ident => Some(#id),
80+
}
81+
});
82+
let identifier_doc_rows = champions
83+
.iter()
84+
.filter(|champion| {
85+
champion.alias
86+
!= champion
87+
.name
88+
.replace(|c| char::is_ascii_alphanumeric(&c), "")
89+
})
90+
.map(|champion| {
91+
let id = champion.id;
92+
let name = &champion.name;
93+
let alias = &champion.alias;
94+
format!("`{}` | {} | {} | {}", id, name, alias, id)
95+
});
96+
let identifier_cases = champions.iter().map(|champion| {
97+
let ident = Ident::new(&champion.alias.to_shouty_snake_case(), Span::call_site());
98+
let alias = &champion.alias;
99+
quote! {
100+
Self::#ident => Some(#alias),
101+
}
102+
});
103+
104+
quote! {
105+
impl Champion {
106+
/// The champion's name (`en_US` localization).
107+
pub const fn id(self) -> Option<i16> {
108+
match self {
109+
#( #id_cases )*
110+
_ => None,
111+
}
112+
}
113+
114+
/// The champion's identifier key. Somtimes called "key", "identifier", or "alias".
115+
/// This is mainly used in DDragon paths.
116+
///
117+
/// This is generally the `en_US` name with spaces and punctuation removed,
118+
/// capitalization preserved, however the follow are exceptions:
119+
///
120+
/// Field | Name | Identifier | Id
121+
/// ---|---|---|---
122+
#( #[doc = #identifier_doc_rows] )*
123+
pub const fn identifier(self) -> Option<&'static str> {
124+
match self {
125+
#( #identifier_cases )*
126+
_ => None,
127+
}
128+
}
129+
}
130+
}
131+
};
132+
133+
let champion_parse = {
134+
let cases: BTreeMap<String, &Champion> = champions
135+
.iter()
136+
.flat_map(|champion| {
137+
IntoIterator::into_iter([&champion.alias, &champion.name]).flat_map(move |s| {
138+
// e.g. `CHOG` for `Cho'Gath`.
139+
let chars_full = s.chars().filter(|c| c.is_ascii_alphanumeric());
140+
// e.g. `CHO` for `Cho'Gath`.
141+
let chars_first = s.chars().take_while(|c| c.is_ascii_alphanumeric());
142+
143+
IntoIterator::into_iter([
144+
Box::new(chars_full) as Box<dyn Iterator<Item = char>>,
145+
Box::new(chars_first),
146+
])
147+
.map(move |chars| {
148+
let str: String = chars
149+
.map(|c| c.to_ascii_uppercase())
150+
.chain(std::iter::repeat('\0'))
151+
.take(4)
152+
.collect();
153+
(str, champion)
154+
})
155+
})
156+
})
157+
.collect();
158+
let cases = cases.into_iter().map(|(s, champion)| {
159+
let ident = Ident::new(&champion.alias.to_shouty_snake_case(), Span::call_site());
160+
let chars = s.chars().map(|c| Literal::character(c));
161+
quote! {
162+
[#( #chars ),*] => Ok(Champion::#ident),
163+
}
164+
});
165+
166+
quote! {
167+
impl std::str::FromStr for Champion {
168+
type Err = ParseChampionError;
169+
fn from_str(s: &str) -> Result<Self, Self::Err> {
170+
let mut chars = ['\0'; 4];
171+
s.chars()
172+
.take(4)
173+
.filter(|c| c.is_ascii_alphanumeric())
174+
.map(|c| c.to_ascii_uppercase())
175+
.enumerate()
176+
.for_each(|(i, c)| chars[i] = c);
177+
match chars {
178+
#( #cases )*
179+
unknown => Err(ParseChampionError(unknown)),
180+
}
181+
}
182+
}
183+
}
184+
};
185+
186+
parse_quote! {
187+
#newtype_enum
188+
#champion_impl
189+
#champion_parse
190+
}
191+
}

riven/build/main.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use std::iter::FromIterator;
2+
use std::path::PathBuf;
3+
use std::{env, fs};
4+
5+
mod champion;
6+
7+
fn main() {
8+
println!("cargo:rerun-if-changed=schemas");
9+
println!("cargo:rerun-if-changed=build");
10+
11+
fs::write(
12+
PathBuf::from_iter([&env::var("OUT_DIR").unwrap(), "champion.rs"]),
13+
prettyplease::unparse(&champion::champion()),
14+
)
15+
.unwrap();
16+
}

riven/schemas/.champion.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)