civ_map_generator 0.1.2

A civilization map generator
Documentation
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;

use serde_json::Value;

fn main() {
    if std::env::var("DOCS_RS").is_ok() {
        return;
    }

    println!("cargo:rerun-if-changed=src/jsons/Civ V - Gods & Kings/TerrainTypes.json");
    println!("cargo:rerun-if-changed=src/jsons/Civ V - Gods & Kings/BaseTerrains.json");
    println!("cargo:rerun-if-changed=src/jsons/Civ V - Gods & Kings/Features.json");
    println!("cargo:rerun-if-changed=src/jsons/Civ V - Gods & Kings/NaturalWonders.json");
    println!("cargo:rerun-if-changed=src/jsons/Civ V - Gods & Kings/TileResources.json");
    println!("cargo:rerun-if-changed=src/jsons/Civ V - Gods & Kings/Nations.json");

    let json_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("src")
        .join("jsons")
        .join("Civ V - Gods & Kings");

    /* Tile Component Rust File Generation */
    let tile_component_path = Path::new("src/tile_component");

    create_enum_from_json(
        json_path.join("TerrainTypes.json").to_str().unwrap(),
        tile_component_path
            .join("terrain_type.rs")
            .to_str()
            .unwrap(),
        "TerrainType",
    );

    create_enum_from_json(
        json_path.join("BaseTerrains.json").to_str().unwrap(),
        tile_component_path
            .join("base_terrain.rs")
            .to_str()
            .unwrap(),
        "BaseTerrain",
    );

    create_enum_from_json(
        json_path.join("Features.json").to_str().unwrap(),
        tile_component_path.join("feature.rs").to_str().unwrap(),
        "Feature",
    );

    create_enum_from_json(
        json_path.join("NaturalWonders.json").to_str().unwrap(),
        tile_component_path
            .join("natural_wonder.rs")
            .to_str()
            .unwrap(),
        "NaturalWonder",
    );

    create_enum_from_json(
        json_path.join("Resources.json").to_str().unwrap(),
        tile_component_path.join("resource.rs").to_str().unwrap(),
        "Resource",
    );
    /* Tile Component Rust File Generation */

    create_enum_from_json(
        json_path.join("Nations.json").to_str().unwrap(),
        Path::new("src/nation.rs").to_str().unwrap(),
        "Nation",
    );
}

fn create_enum_from_json(json_path: &str, dest_path: &str, enum_name: &str) {
    let json_string_without_comment = load_json_file_and_strip_json_comments(json_path);

    let value_list: Vec<Value> = serde_json::from_str(&json_string_without_comment)
        .unwrap_or_else(|_| panic!("{}'{}'", "Can't serde ", json_path));

    let names: Vec<&str> = value_list
        .iter()
        .map(|terrain| terrain.get("name").and_then(|v| v.as_str()).unwrap_or(""))
        .collect();

    let mut output = String::new();
    output.push_str("// Auto-generated by build.rs, DO NOT EDIT\n");
    output.push_str("use enum_map::Enum;\n");
    output.push_str("use serde::{Deserialize, Serialize};\n");
    output.push('\n');
    output.push_str(
        "#[derive(Enum, PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize, Debug)]\n",
    );
    output.push_str(&format!("pub enum {} {{\n", enum_name));

    let enum_variants: Vec<String> = names
        .iter()
        .map(|name| {
            let variant: String = name
                .split_whitespace()
                .map(|word| {
                    let mut chars = word.chars();
                    match chars.next() {
                        Some(c) => c.to_uppercase().chain(chars).collect(),
                        None => String::new(),
                    }
                })
                .collect::<Vec<String>>()
                .join("")
                .chars()
                .filter(|c| c.is_ascii_alphabetic())
                .collect();
            variant
        })
        .collect();

    for variant in enum_variants.iter() {
        output.push_str(&format!("    {},\n", variant));
    }

    output.push_str("}\n\n");
    output.push_str(&format!("impl {} {{\n", enum_name));
    output.push_str("    pub fn as_str(&self) -> &'static str {\n");
    output.push_str("        match self {\n");

    for (variant, name) in enum_variants.iter().zip(names.iter()) {
        output.push_str(&format!(
            "            {}::{} => \"{}\",\n",
            enum_name, variant, name
        ));
    }

    output.push_str("        }\n");
    output.push_str("    }\n");
    output.push_str("}\n");

    let mut file = File::create(dest_path).expect("Could not create output file");
    file.write_all(output.as_bytes())
        .expect("Could not write to file");
}

fn load_json_file_and_strip_json_comments(path: &str) -> String {
    let json_string_with_comment = fs::read_to_string(path).unwrap();
    strip_json_comments(&json_string_with_comment, true)
}

/// Take a JSON string with comments and return the version without comments
/// which can be parsed well by serde_json as the standard JSON string.
/// Support line comment(//...) and block comment(/*...*/)
/// When preserve_locations is true this function will replace all the comments with spaces, so that JSON parsing
/// errors can point to the right location.
pub fn strip_json_comments(json_with_comments: &str, preserve_locations: bool) -> String {
    let mut json_without_comments = String::new();

    let mut block_comment_depth: u8 = 0;
    let mut is_in_string: bool = false; // Comments cannot be in strings

    for line in json_with_comments.split('\n') {
        let mut last_char: Option<char> = None;
        for cur_char in line.chars() {
            // Check whether we're in a string
            if block_comment_depth == 0 && last_char != Some('\\') && cur_char == '"' {
                is_in_string = !is_in_string;
            }

            // Check for line comment start
            if !is_in_string && last_char == Some('/') && cur_char == '/' {
                last_char = None;
                if preserve_locations {
                    json_without_comments.push_str("  ");
                }
                break; // Stop outputting or parsing this line
            }
            // Check for block comment start
            if !is_in_string && last_char == Some('/') && cur_char == '*' {
                block_comment_depth += 1;
                last_char = None;
                if preserve_locations {
                    json_without_comments.push_str("  ");
                }
            // Check for block comment end
            } else if !is_in_string && last_char == Some('*') && cur_char == '/' {
                if block_comment_depth > 0 {
                    block_comment_depth = block_comment_depth.saturating_sub(1);
                }
                last_char = None;
                if preserve_locations {
                    json_without_comments.push_str("  ");
                }

            // Output last char if not in any block comment
            } else {
                if block_comment_depth != 0 {
                    if preserve_locations {
                        json_without_comments.push(' ');
                    }
                } else if let Some(last_char) = last_char {
                    json_without_comments.push(last_char);
                }
                last_char = Some(cur_char);
            }
        }

        // Add last char and newline if not in any block comment
        if let Some(last_char) = last_char {
            if block_comment_depth == 0 {
                json_without_comments.push(last_char);
            } else if preserve_locations {
                json_without_comments.push(' ');
            }
        }

        // Remove trailing whitespace from line
        while json_without_comments.ends_with(' ') {
            json_without_comments.pop();
        }
        json_without_comments.push('\n');
    }

    json_without_comments
}