@ 261107.5
current
use std::{error::Error, fs, path::Path};

use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

// Reuse the layout used by the macros without spawning Cargo at runtime.
// The macro generator and CLI entry point are unused in this crate.
#[allow(dead_code)]
#[path = "../../macros/src/parser_fields.rs"]
mod parser_fields;

#[derive(Serialize)]
pub struct Version {
  pub raw: String,
  pub major: u64,
  pub minor: u64,
  pub patch: u64,
  pub prerelease: String,
}

#[derive(Serialize)]
pub struct BuildInfo {
  pub version: Version,
  pub constants: Map<String, Value>,
}

#[derive(Deserialize)]
struct Package {
  version: String,
}

#[derive(Deserialize)]
struct Manifest {
  package: Package,
}

fn read_yaml_list(name: &str) -> Result<Vec<String>, Box<dyn Error>> {
  let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
  let raw = fs::read_to_string(root.join(format!("macros/constants/{name}.yml")))?;
  Ok(serde_yaml::from_str(&raw)?)
}

fn read_version() -> Result<Version, Box<dyn Error>> {
  let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
  let manifest: Manifest = toml::from_str(&fs::read_to_string(root.join("parser/Cargo.toml"))?)?;
  let parsed = semver::Version::parse(&manifest.package.version)?;
  Ok(Version {
    raw: manifest.package.version,
    major: parsed.major,
    minor: parsed.minor,
    patch: parsed.patch,
    prerelease: parsed.pre.to_string(),
  })
}

fn read_parser_fields() -> Map<String, Value> {
  // The JavaScript helper spawned Cargo; Rust can reuse the same offsets
  // directly.
  parser_fields::FIELDS
    .iter()
    .map(|&(name, offset)| (format!("PARSER_FIELD_{name}"), offset.into()))
    .collect()
}

/// Reads the current repository inputs, independently of the working directory.
pub fn get_build_info() -> Result<BuildInfo, Box<dyn Error>> {
  let version = read_version()?;
  let methods = read_yaml_list("methods")?;
  let errors = read_yaml_list("errors")?;
  let callbacks = read_yaml_list("callbacks")?;
  let states = read_yaml_list("states")?;
  let parser_fields = read_parser_fields();

  // Insertion order is part of the generated JSON and downstream bindings.
  let mut constants = Map::new();
  for (i, method) in methods.iter().enumerate() {
    constants.insert(format!("METHOD_{}", method.replace('-', "_")), i.into());
  }
  for (i, callback) in callbacks.iter().enumerate() {
    constants.insert(format!("CALLBACK_{}", callback.to_uppercase()), i.into());
  }
  constants.insert("EVENT_END".into(), 0.into());
  for (i, callback) in callbacks.iter().enumerate() {
    let name = callback.strip_prefix("on_").unwrap_or(callback).to_uppercase();
    constants.insert(format!("EVENT_{name}"), (i + 1).into());
  }

  let mut all = 0;
  constants.insert("CALLBACK_ACTIVE_NONE".into(), 0.into());
  constants.insert("EVENT_ACTIVE_NONE".into(), 0.into());
  for (i, callback) in callbacks.iter().enumerate() {
    // Match JavaScript's signed 32-bit shifts, including the masked shift count.
    let bit = 1_i32.wrapping_shl((i % 32) as u32);
    constants.insert(format!("CALLBACK_ACTIVE_{}", callback.to_uppercase()), bit.into());
    constants.insert(format!("EVENT_ACTIVE_{}", callback.to_uppercase()), bit.into());
    all |= bit;
  }
  constants.insert("CALLBACK_ACTIVE_ALL".into(), all.into());
  constants.insert("EVENT_ACTIVE_ALL".into(), all.into());
  for (i, error) in errors.iter().enumerate() {
    constants.insert(format!("ERROR_{error}"), i.into());
  }
  for (i, state) in states.iter().enumerate() {
    constants.insert(format!("STATE_{}", state.to_uppercase()), i.into());
  }
  constants.extend(parser_fields);

  Ok(BuildInfo { version, constants })
}