Skip to content

Commit

Permalink
Add icu-locale macros
Browse files Browse the repository at this point in the history
  • Loading branch information
zbraniecki committed Sep 3, 2020
1 parent 3ebc97d commit 81e4597
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 12 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"components/icu4x",
"components/uniset",
"components/locale",
"components/locale/macros",
"components/num-util",
"components/pluralrules",
]
21 changes: 21 additions & 0 deletions components/locale/macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "icu-locale-macros"
description = "proc-macros for icu-locale"
version = "0.0.1"
authors = ["The ICU4X Project Developers"]
edition = "2018"
readme = "README.md"
repository = "https://github.com/unicode-org/icu4x"
license-file = "../../LICENSE"
categories = ["internationalization"]
include = [
"src/**/*",
"Cargo.toml",
"README.md"
]

[lib]
proc_macro = true

[dependencies]
icu-locale = { version = "0.0.1", path = "../" }
109 changes: 109 additions & 0 deletions components/locale/macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
extern crate proc_macro;

use proc_macro::TokenStream;

fn get_value_from_token_stream(input: TokenStream) -> String {
let val = format!("{}", input);
if !val.starts_with('"') || !val.ends_with('"') {
panic!("Argument must be a string literal.");
}
let len = val.len();
(&val[1..len - 1]).to_string()
}

fn language_output(input: Option<u64>) -> String {
let val = if let Some(raw) = input {
format!("Some({})", raw)
} else {
"None".to_string()
};
format!("unsafe {{ icu_locale::subtags::Language::from_raw_unchecked({}) }}", val)
}

fn script_output(input: u32) -> String {
format!("unsafe {{ icu_locale::subtags::Script::from_raw_unchecked({}) }}", input)
}

fn region_output(input: u32) -> String {
format!("unsafe {{ icu_locale::subtags::Region::from_raw_unchecked({}) }}", input)
}

fn variant_output(input: u64) -> String {
format!("unsafe {{ icu_locale::subtags::Variant::from_raw_unchecked({}) }}", input)
}

#[proc_macro]
pub fn language(input: TokenStream) -> TokenStream {
let val = get_value_from_token_stream(input);

let parsed: icu_locale::subtags::Language = val.parse().expect("Malformed Language Subtag");

let lang = language_output(parsed.into_raw());

lang.parse().unwrap()
}

#[proc_macro]
pub fn script(input: TokenStream) -> TokenStream {
let val = get_value_from_token_stream(input);

let parsed: icu_locale::subtags::Script = val.parse().expect("Malformed Script Subtag");

let script = script_output(parsed.into_raw());

script.parse().unwrap()
}

#[proc_macro]
pub fn region(input: TokenStream) -> TokenStream {
let val = get_value_from_token_stream(input);

let parsed: icu_locale::subtags::Region = val.parse().expect("Malformed Region Subtag");

let region = region_output(parsed.into_raw());

region.parse().unwrap()
}

#[proc_macro]
pub fn variant(input: TokenStream) -> TokenStream {
let val = get_value_from_token_stream(input);

let parsed: icu_locale::subtags::Variant = val.parse().expect("Malformed Variant Subtag");

let variant = variant_output(parsed.into_raw());

variant.parse().unwrap()
}

#[proc_macro]
pub fn langid(input: TokenStream) -> TokenStream {
let val = get_value_from_token_stream(input);

let langid: icu_locale::LanguageIdentifier = val.parse().expect("Malformed Language Identifier");

let lang = language_output(langid.language.into_raw());

let script = if let Some(script) = langid.script {
format!("Some({})", script_output(script.into_raw()))
} else {
"None".to_string()
};
let region = if let Some(region) = langid.region {
format!("Some({})", region_output(region.into_raw()))
} else {
"None".to_string()
};
let variants = format!("icu_locale::subtags::Variants::from_vec_unchecked(vec![])");

let output = format!(r#"
icu_locale::LanguageIdentifier {{
language: {},
script: {},
region: {},
variants: {},
}}
"#, lang, script, region, variants);

output.parse().unwrap()
}
54 changes: 54 additions & 0 deletions components/locale/macros/tests/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use icu_locale_macros::*;
use icu_locale::subtags;
use icu_locale::LanguageIdentifier;

const LANG_PL: subtags::Language = language!("pL");
const SCRIPT_LATN: subtags::Script = script!("lAtN");
const REGION_US: subtags::Region = region!("us");
const VARIANT_MACOS: subtags::Variant = variant!("MACOS");
const LANGID: LanguageIdentifier = langid!("de-Arab-AT");

#[test]
fn language() {
let lang = language!("Pl");

assert_eq!(lang, "pl");
assert_eq!(LANG_PL, "pl");
assert_eq!(lang, LANG_PL);
}

#[test]
fn script() {
let script = script!("latn");

assert_eq!(script, "Latn");
assert_eq!(SCRIPT_LATN, "Latn");
assert_eq!(script, SCRIPT_LATN);
}

#[test]
fn region() {
let region = region!("us");

assert_eq!(region, "US");
assert_eq!(REGION_US, "US");
assert_eq!(region, REGION_US);
}

#[test]
fn variant() {
let variant = variant!("macOS");

assert_eq!(variant, "macos");
assert_eq!(VARIANT_MACOS, "macos");
assert_eq!(variant, VARIANT_MACOS);
}

#[test]
fn langid() {
let langid = langid!("de_Arab_aT");

assert_eq!(langid.to_string(), "de-Arab-AT");
assert_eq!(LANGID.to_string(), "de-Arab-AT");
assert_eq!(langid, LANGID);
}
2 changes: 1 addition & 1 deletion components/locale/src/parser/langid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub fn parse_language_identifier_from_iter<'a>(
language,
script,
region,
variants: subtags::Variants::from_vec_unchecked(variants),
variants: subtags::Variants::from_raw_unchecked(Some(variants.into_boxed_slice())),
})
}

Expand Down
47 changes: 47 additions & 0 deletions components/locale/src/subtags/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,53 @@ impl Language {
}
}

/// Deconstructs the `Language` into raw format to be consumed
/// by `from_raw_unchecked`.
///
/// # Examples
///
/// ```
/// use icu_locale::subtags::Language;
///
/// let lang = Language::from_bytes(b"en")
/// .expect("Parsing failed.");
///
/// let raw = lang.into_raw();
/// let lang = unsafe { Language::from_raw_unchecked(raw) };
/// assert_eq!(lang, "en");
/// ```
pub fn into_raw(self) -> Option<u64> {
self.0.map(|v| v.into())
}

/// Constructor which takes a raw value returned by
/// `into_raw`.
///
/// # Examples
///
/// ```
/// use icu_locale::subtags::Language;
///
/// let lang = Language::from_bytes(b"en")
/// .expect("Parsing failed.");
///
/// let raw = lang.into_raw();
/// let lang = unsafe { Language::from_raw_unchecked(raw) };
/// assert_eq!(lang, "en");
/// ```
///
/// # Safety
///
/// This function accepts any u64 that is exected to be a valid
/// `TinyStr8` and a valid `Language` subtag.
pub const unsafe fn from_raw_unchecked(v: Option<u64>) -> Self {
if let Some(v) = v {
Self(Some(TinyStr8::new_unchecked(v)))
} else {
Self(None)
}
}

/// A helper function for displaying
/// a `Language` subtag as a `&str`.
///
Expand Down
42 changes: 42 additions & 0 deletions components/locale/src/subtags/region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@ impl Region {
}
}

/// Deconstructs the `Region` into raw format to be consumed
/// by `from_raw_unchecked`.
///
/// # Examples
///
/// ```
/// use icu_locale::subtags::Region;
///
/// let region = Region::from_bytes(b"us")
/// .expect("Parsing failed.");
///
/// let raw = region.into_raw();
/// let region = unsafe { Region::from_raw_unchecked(raw) };
/// assert_eq!(region, "US");
/// ```
pub fn into_raw(self) -> u32 {
self.0.into()
}

/// Constructor which takes a raw value returned by
/// `into_raw`.
///
/// # Examples
///
/// ```
/// use icu_locale::subtags::Region;
///
/// let region = Region::from_bytes(b"us")
/// .expect("Parsing failed.");
///
/// let raw = region.into_raw();
/// let region = unsafe { Region::from_raw_unchecked(raw) };
/// assert_eq!(region, "US");
/// ```
/// # Safety
///
/// This function accepts any u32 that is exected to be a valid
/// `TinyStr8` and a valid `Region` subtag.
pub const unsafe fn from_raw_unchecked(v: u32) -> Self {
Self(TinyStr4::new_unchecked(v))
}

/// A helper function for displaying
/// a `Region` subtag as a `&str`.
///
Expand Down
42 changes: 42 additions & 0 deletions components/locale/src/subtags/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,48 @@ impl Script {
Ok(Self(s.to_ascii_titlecase()))
}

/// Deconstructs the `Script` into raw format to be consumed
/// by `from_raw_unchecked`.
///
/// # Examples
///
/// ```
/// use icu_locale::subtags::Script;
///
/// let script = Script::from_bytes(b"Latn")
/// .expect("Parsing failed.");
///
/// let raw = script.into_raw();
/// let script = unsafe { Script::from_raw_unchecked(raw) };
/// assert_eq!(script, "Latn");
/// ```
pub fn into_raw(self) -> u32 {
self.0.into()
}

/// Constructor which takes a raw value returned by
/// `into_raw`.
///
/// # Examples
///
/// ```
/// use icu_locale::subtags::Script;
///
/// let script = Script::from_bytes(b"Latn")
/// .expect("Parsing failed.");
///
/// let raw = script.into_raw();
/// let script = unsafe { Script::from_raw_unchecked(raw) };
/// assert_eq!(script, "Latn");
/// ```
/// # Safety
///
/// This function accepts any u32 that is exected to be a valid
/// `TinyStr8` and a valid `Script` subtag.
pub const unsafe fn from_raw_unchecked(v: u32) -> Self {
Self(TinyStr4::new_unchecked(v))
}

/// A helper function for displaying
/// a `Script` subtag as a `&str`.
///
Expand Down
Loading

0 comments on commit 81e4597

Please sign in to comment.