Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
dist
dist-python
node_modules
rust/target
rust/Cargo.lock
17 changes: 17 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "dicebear-styles"
version = "10.1.0"
edition = "2021"
rust-version = "1.70"
description = "Definition files of the DiceBear avatar styles, shipped as embedded JSON data."
keywords = ["avatar", "dicebear"]
categories = ["multimedia::images"]
homepage = "https://www.dicebear.com"
repository = "https://github.com/dicebear/styles"
# The avatar styles are subject to different licenses; see LICENSE.md.
license-file = "../LICENSE.md"
readme = "README.md"
include = ["src/**/*", "build.rs", "../src/*.json", "README.md"]

[dependencies]
serde_json = "1"
42 changes: 42 additions & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# dicebear-styles

The official [DiceBear](https://www.dicebear.com) avatar style definitions for
Rust, shipped as embedded JSON data.

An avatar style definition is a JSON document that describes how to create an
avatar — its available elements, colors, and metadata. This crate is **pure
data**: it contains no rendering or avatar-generation logic, mirroring how the
npm (`@dicebear/styles`), Composer (`dicebear/styles`), and PyPI
(`dicebear-styles`) packages distribute the same JSON. The JSON schema for the
definitions can be found in the
[`@dicebear/schema`](https://github.com/dicebear/schema) package.

## Usage

```rust
// Look up a style by name (file stem).
let adventurer: &str = dicebear_styles::get("adventurer").unwrap();

// Or use the typed accessor.
let lorelei: &str = dicebear_styles::lorelei();

// List all 37 available style names (sorted).
for name in dicebear_styles::names() {
println!("{name}");
}

// Parse a definition into a `serde_json::Value`.
let value = dicebear_styles::parsed("initials").unwrap();
```

The raw JSON is returned as `&'static str`, embedded at compile time from the
repository's `src/*.json` definition files. The embed table is generated by
`build.rs` in sorted order, so the list of styles always matches the files on
disk.

## License

The avatar styles are licensed under different licenses. More information can be
found in the file
[LICENSE.md](https://github.com/dicebear/definitions/blob/main/LICENSE.md) or in
the definition files themselves.
90 changes: 90 additions & 0 deletions rust/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Build script for `dicebear-styles`.
//!
//! Scans the repository's `src/*.json` style definitions and generates a
//! deterministic, sorted lookup table that embeds each file via `include_str!`.
//! This mirrors how the npm / Composer / PyPI packages ship the same pure JSON
//! data, without hand-maintaining the list of style names.

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
// The style definitions live one level up, in `<repo>/src/*.json`.
let src_dir = manifest_dir.join("..").join("src");

// Re-run when the set of definition files changes.
println!("cargo:rerun-if-changed={}", src_dir.display());

let mut entries: Vec<(String, String)> = Vec::new();
for entry in fs::read_dir(&src_dir)
.unwrap_or_else(|e| panic!("failed to read style directory {}: {e}", src_dir.display()))
{
let path = entry.expect("failed to read directory entry").path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let name = path
.file_stem()
.and_then(|s| s.to_str())
.expect("style file has a non-UTF-8 name")
.to_owned();

// Re-run if any individual definition file changes.
println!("cargo:rerun-if-changed={}", path.display());

entries.push((name.clone(), format!("../src/{name}.json")));
}

// Deterministic, sorted order by style name.
entries.sort_by(|a, b| a.0.cmp(&b.0));

assert!(
!entries.is_empty(),
"no style definitions found in {}",
src_dir.display()
);

let mut generated = String::new();
generated.push_str("// @generated by build.rs — do not edit.\n\n");

// `STYLES` is a sorted slice of (name, raw JSON) pairs.
generated.push_str(
"/// All embedded style definitions as `(name, raw_json)` pairs, sorted by name.\n",
);
generated.push_str("pub(crate) static STYLES: &[(&str, &str)] = &[\n");
for (name, rel_path) in &entries {
// `include_str!` in the generated file resolves relative to OUT_DIR, so
// we anchor each path to `CARGO_MANIFEST_DIR` (resolved at compile time)
// and walk up to the repo `src/` directory. This stays portable across
// machines, unlike an absolute build-host path.
generated.push_str(&format!(
" ({name:?}, include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{rel_path}\"))),\n"
));
}
generated.push_str("];\n\n");

// `NAMES` is the sorted list of style names, derived from the same entries.
generated.push_str("/// All style names, sorted.\n");
generated.push_str("pub(crate) static NAMES: &[&str] = &[\n");
for (name, _) in &entries {
generated.push_str(&format!(" {name:?},\n"));
}
generated.push_str("];\n\n");

// Generate a typed accessor function per style, e.g. `big_ears_neutral()`.
for (name, _) in &entries {
let fn_name = name.replace('-', "_");
generated.push_str(&format!(
"/// Returns the raw JSON definition for the `{name}` style.\n"
));
generated.push_str(&format!(
"pub fn {fn_name}() -> &'static str {{\n get({name:?}).expect(\"embedded style is always present\")\n}}\n\n"
));
}

let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let dest = Path::new(&out_dir).join("styles.rs");
fs::write(&dest, generated).expect("failed to write generated styles.rs");
}
156 changes: 156 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//! # dicebear-styles
//!
//! The official [DiceBear](https://www.dicebear.com) avatar style definitions,
//! shipped as embedded JSON data.
//!
//! A style definition is a JSON document describing how to build an avatar — its
//! available elements, colors, and metadata. This crate is **pure data**: it
//! contains no rendering or avatar-generation logic, mirroring how the npm
//! (`@dicebear/styles`), Composer (`dicebear/styles`), and PyPI
//! (`dicebear-styles`) packages distribute the same JSON. The JSON schema for
//! the definitions lives in the [`@dicebear/schema`](https://github.com/dicebear/schema)
//! package.
//!
//! ## Usage
//!
//! ```
//! // Look up a style by name.
//! let adventurer = dicebear_styles::get("adventurer").unwrap();
//! assert!(!adventurer.is_empty());
//!
//! // Or use the typed accessor.
//! let lorelei = dicebear_styles::lorelei();
//! assert!(!lorelei.is_empty());
//!
//! // List all available style names.
//! assert_eq!(dicebear_styles::names().len(), 37);
//!
//! // Parse a definition into a `serde_json::Value`.
//! let value = dicebear_styles::parsed("initials").unwrap();
//! assert!(value.is_object());
//! ```

// The `STYLES` table and the per-style accessor functions are generated by
// `build.rs` from the repository's `src/*.json` definitions, in sorted order.
include!(concat!(env!("OUT_DIR"), "/styles.rs"));

/// Returns the raw JSON definition for the style with the given `name`, or
/// `None` if no such style exists.
///
/// Names match the definition file stems, e.g. `"adventurer"` or
/// `"big-ears-neutral"`.
pub fn get(name: &str) -> Option<&'static str> {
// `STYLES` is sorted by name, so a binary search is correct and cheap.
STYLES
.binary_search_by(|(candidate, _)| (*candidate).cmp(name))
.ok()
.map(|index| STYLES[index].1)
}

/// Returns the sorted list of all available style names.
pub fn names() -> &'static [&'static str] {
NAMES
}

/// Returns the style with the given `name` parsed into a [`serde_json::Value`],
/// or `None` if no such style exists.
///
/// # Panics
///
/// Panics if an embedded definition is not valid JSON. This cannot happen with
/// the definitions shipped by this crate, which are validated at build time of
/// the underlying packages, but the panic guards against a corrupted build.
pub fn parsed(name: &str) -> Option<serde_json::Value> {
get(name).map(|raw| serde_json::from_str(raw).expect("embedded style definition is valid JSON"))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn there_are_exactly_37_styles() {
assert_eq!(names().len(), 37);
assert_eq!(STYLES.len(), 37);
}

#[test]
fn names_are_sorted_and_unique() {
let names = names();
let mut sorted = names.to_vec();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(names, sorted.as_slice());
}

#[test]
fn every_style_returns_non_empty_valid_json() {
for &name in names() {
let raw = get(name).unwrap_or_else(|| panic!("missing style: {name}"));
assert!(!raw.trim().is_empty(), "empty definition for: {name}");

let value: serde_json::Value = serde_json::from_str(raw)
.unwrap_or_else(|e| panic!("invalid JSON for {name}: {e}"));
assert!(value.is_object(), "definition for {name} is not an object");
}
}

#[test]
fn parsed_matches_get_for_known_and_unknown() {
assert!(parsed("adventurer").is_some());
assert!(parsed("does-not-exist").is_none());
assert!(get("does-not-exist").is_none());
}

#[test]
fn typed_accessors_match_lookup() {
assert_eq!(adventurer(), get("adventurer").unwrap());
assert_eq!(big_ears_neutral(), get("big-ears-neutral").unwrap());
assert_eq!(pixel_art(), get("pixel-art").unwrap());
}

#[test]
fn expected_names_are_present() {
for name in [
"adventurer",
"adventurer-neutral",
"avataaars",
"avataaars-neutral",
"big-ears",
"big-ears-neutral",
"big-smile",
"bottts",
"bottts-neutral",
"croodles",
"croodles-neutral",
"disco",
"dylan",
"fun-emoji",
"glass",
"glyphs",
"icons",
"identicon",
"initial-face",
"initials",
"lorelei",
"lorelei-neutral",
"micah",
"miniavs",
"notionists",
"notionists-neutral",
"open-peeps",
"personas",
"pixel-art",
"pixel-art-neutral",
"rings",
"shape-grid",
"shapes",
"stripes",
"thumbs",
"toon-head",
"triangles",
] {
assert!(get(name).is_some(), "expected style missing: {name}");
}
}
}
Loading