6 releases

0.9.5 Jul 19, 2024
0.9.4 Jul 19, 2024

#462 in Encoding

Apache-2.0

27KB
430 lines

json-template

json-template is a library that allows you to create JSON templates.

file.json

{
   "personal_info": "{file:additional.json}",
   "info": "{personal_info.name} {personal_info.last_name} is {personal_info.age} years old"
}

additional.json

{
   "name": "Danilo",
   "last_name": "Guanabara",
   "age": 36
}

Can be rendered to

{
   "personal_info": {
      "age": 36,
      "name": "Danilo",
      "last_name": "Guanabara"
   },
   "info": "Danilo Guanabara is 36 years old"
}

Functionalities

Chained resolver

{
   "my": "Danilo",
   "name": "{my}",
   "is": "{name}
}

Every path segment is a placeholder

{
   "data": "{{file:data.json}.property}
}

Functions

Built-in functions

Function Description
{file:path} Loads a file from a relative path. Its base directory is automatically set if you deserialize a file. You can also set it manually using Context::set_directory.
{string:path} Transforms a serde_json::Value to serde_json::Value::String. It's useful if you need to deserialize a Number as a String.
{compose:{a}, {b}, ...} Composes N objects together. If the property doesn't exist, it will be added. If the property is an array, both arrays will be concatenated. Inputs are placeholders.

Check Custom Functions code example to learn how to create a custom function.

Code examples

You can always check the tests :)

From memory

use json_template::*;
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Data {
   name: String,
   age: usize,
   age_str: String,
   info: String,
   time: String
}

let json = r#"{
  "data": {
    "name": "Danilo",
    "age": 36
  },
  "name": "{data.name}",
  "age": "{data.age}",
  "age_str": "{string:data.age}",
  "info": "{data.name} is {data.age} years old.",
  "time": "{data.time}"
}"#;
let context = Context::new()
   .with_data(serde_json::json!({
      "data": {
         "time": "now"
      }
   }));
let data: Data = Deserializer::new().deserialize_with_context(json, &context).unwrap();
assert_eq!(data, Data {
   name: "Danilo".into(),
   age: 36,
   age_str: "36".into(),
   info: "Danilo is 36 years old.".into(),
   time: "now".into()
})

Note that the age_str field is a string, while the age field is a number.

Simple "{age}" placeholders are replaced with the corresponding value in the JSON object.

"{string:age}" placeholders are replaced with the corresponding value in the JSON object as a string.

Formatted placeholders like "{data.name} is {data.age} years old." are replaced with the corresponding values in the JSON object as strings.

Even though "{string:data.name} is {string:data.age} years old." would work, it is not necessary.

From file

use json_template::*;
use serde::{Serialize, Deserialize};
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Data {
   name: String,
   age: usize,
   age_str: String,
   info: String,
   time: String
}

let json = r#"{
  "data": "{file:tests/data.json}",
  "name": "{data.name}",
  "age": "{data.age}",
  "age_str": "{string:data.age}",
  "info": "{data.name} is {data.age} years old.",
  "time": "{data.time}"
}"#;

let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let context = Context::new()
   .with_data(serde_json::json!({
      "data": {
         "time": "now"
      }
   }))
   .with_directory(Some(directory));

let data: Data = Deserializer::new().deserialize_with_context(json, &context).unwrap();

assert_eq!(data, Data {
   name: "Danilo".into(),
   age: 36,
   age_str: "36".into(),
   info: "Danilo is 36 years old.".into(),
   time: "now".into()
})

or simply

use json_template::*;
use serde::{Serialize, Deserialize};
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Data {
   name: String,
   age: usize,
   age_str: String,
   info: String,
   time: String
}

let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
    .join("tests")
    .join("data-from-file.json");
let context = Context::new()
   .with_data(serde_json::json!({
      "data": {
         "time": "now"
      }
   }));

let data: Data = Deserializer::new().deserialize_with_context(file, &context).unwrap();

assert_eq!(data, Data {
   name: "Danilo".into(),
   age: 36,
   age_str: "36".into(),
   info: "Danilo is 36 years old.".into(),
   time: "now".into()
})

Custom functions

use json_template::*;
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Time {
   duration: std::time::Duration
}

let value = serde_json::json!({ "duration": "{time:5}" });

let context = Context::new().with_function("time", |_deserializer, _context, placeholder| {
   let seconds = placeholder
      .path()
      .str()
      .parse::<u64>()
      .map_err(|e| serde::de::Error::custom(e))?;
   let duration = std::time::Duration::from_secs(seconds);
   serde_json::to_value(&duration)
});

let data: Time = Deserializer::new().deserialize_with_context(value, &context).expect("Failed to deserialize");

assert_eq!(data.duration, std::time::Duration::from_secs(5));

Dependencies

~0.7–1.6MB
~35K SLoC