Unverified Commit 2eb8f502 authored by Robert Hensing's avatar Robert Hensing Committed by GitHub
Browse files

Merge pull request #246115 from h7x4/add-libconfig-format-generator

pkgs.formats: Add libconfig format generator
parents 89d1f10e 5707d01d
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -28,12 +28,19 @@ rec {
      generate = ...;

    });

  Please note that `pkgs` may not always be available for use due to the split
  options doc build introduced in fc614c37c653, so lazy evaluation of only the
  'type' field is required.

  */


  inherit (import ./formats/java-properties/default.nix { inherit lib pkgs; })
    javaProperties;

  libconfig = (import ./formats/libconfig/default.nix { inherit lib pkgs; }).format;

  json = {}: {

    type = with lib.types; let
+121 −0
Original line number Diff line number Diff line
{ lib
, pkgs
}:
let
  inherit (pkgs) buildPackages callPackage;
  # Implementation notes:
  #   Libconfig spec: https://hyperrealm.github.io/libconfig/libconfig_manual.html
  #
  #   Since libconfig does not allow setting names to start with an underscore,
  #   this is used as a prefix for both special types and include directives.
  #
  #   The difference between 32bit and 64bit values became optional in libconfig
  #   1.5, so we assume 64bit values for all numbers.

  libconfig-generator = buildPackages.rustPlatform.buildRustPackage {
    name = "libconfig-generator";
    version = "0.1.0";
    src = ./src;

    passthru.updateScript = ./update.sh;

    cargoLock.lockFile = ./src/Cargo.lock;
  };

  libconfig-validator = buildPackages.runCommandCC "libconfig-validator"
    {
      buildInputs = with buildPackages; [ libconfig ];
    }
    ''
      mkdir -p "$out/bin"
      $CC -lconfig -x c - -o "$out/bin/libconfig-validator" ${./validator.c}
    '';
in
{
  format = { generator ? libconfig-generator, validator ? libconfig-validator }: {
    inherit generator;

    type = with lib.types;
      let
        valueType = (oneOf [
          bool
          int
          float
          str
          path
          (attrsOf valueType)
          (listOf valueType)
        ]) // {
          description = "libconfig value";
        };
      in
      attrsOf valueType;

    lib = {
      mkHex = value: {
        _type = "hex";
        inherit value;
      };
      mkOctal = value: {
        _type = "octal";
        inherit value;
      };
      mkFloat = value: {
        _type = "float";
        inherit value;
      };
      mkArray = value: {
        _type = "array";
        inherit value;
      };
      mkList = value: {
        _type = "list";
        inherit value;
      };
    };

    generate = name: value:
      callPackage
        ({
          stdenvNoCC
        , libconfig-generator
        , libconfig-validator
        , writeText
        }: stdenvNoCC.mkDerivation rec {
          inherit name;

          dontUnpack = true;

          json = builtins.toJSON value;
          passAsFile = [ "json" ];

          strictDeps = true;
          nativeBuildInputs = [ libconfig-generator ];
          buildPhase = ''
            runHook preBuild
            libconfig-generator < $jsonPath > output.cfg
            runHook postBuild
          '';

          doCheck = true;
          nativeCheckInputs = [ libconfig-validator ];
          checkPhase = ''
            runHook preCheck
            libconfig-validator output.cfg
            runHook postCheck
          '';

          installPhase = ''
            runHook preInstall
            mv output.cfg $out
            runHook postInstall
          '';

          passthru.json = writeText "${name}.json" json;
        })
        {
          libconfig-generator = generator;
          libconfig-validator = validator;
        };
  };
}
+40 −0
Original line number Diff line number Diff line
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"

[[package]]
name = "libconfig-generator"
version = "0.1.0"
dependencies = [
 "serde",
 "serde_json",
]

[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"

[[package]]
name = "serde"
version = "1.0.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"

[[package]]
name = "serde_json"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
dependencies = [
 "itoa",
 "ryu",
 "serde",
]
+10 −0
Original line number Diff line number Diff line
[package]
name = "libconfig-generator"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = "1.0.178"
serde_json = "1.0.104"
+271 −0
Original line number Diff line number Diff line
use serde_json::Value;
use std::mem::discriminant;

#[derive(Debug)]
enum LibConfigIntNumber {
    Oct(i64),
    Hex(i64),
    Int(i64),
}

#[derive(Debug)]
enum LibConfigValue {
    Bool(bool),
    Int(LibConfigIntNumber),
    Float(f64),
    String(String),
    Array(Vec<LibConfigValue>),
    List(Vec<LibConfigValue>),
    Group(Vec<String>, Vec<(String, LibConfigValue)>),
}

fn validate_setting_name(key: &str) -> bool {
    let first_char = key.chars().next().expect("Empty setting name");
    (first_char.is_alphabetic() || first_char == '*')
        && key[1..]
            .chars()
            .all(|c| c.is_alphanumeric() || c == '_' || c == '*')
}

const SPECIAL_TYPES: [&str; 5] = ["octal", "hex", "float", "list", "array"];

fn object_is_special_type(o: &serde_json::Map<String, Value>) -> Option<&str> {
    o.get("_type").and_then(|x| x.as_str()).and_then(|x| {
        if SPECIAL_TYPES.contains(&x) {
            Some(x)
        } else {
            None
        }
    })
}

fn vec_is_array(v: &Vec<LibConfigValue>) -> bool {
    if v.is_empty() {
        return true;
    }

    let first_item = v.first().unwrap();

    if match first_item {
        LibConfigValue::Array(_) => true,
        LibConfigValue::List(_) => true,
        LibConfigValue::Group(_, _) => true,
        _ => false,
    } {
        return false;
    };

    v[1..]
        .iter()
        .all(|item| discriminant(first_item) == discriminant(item))
}

fn json_to_libconfig(v: &Value) -> LibConfigValue {
    match v {
        Value::Null => panic!("Null value not allowed in libconfig"),
        Value::Bool(b) => LibConfigValue::Bool(b.clone()),
        Value::Number(n) => {
            if n.is_i64() {
                LibConfigValue::Int(LibConfigIntNumber::Int(n.as_i64().unwrap()))
            } else if n.is_f64() {
                LibConfigValue::Float(n.as_f64().unwrap())
            } else {
                panic!("{} is not i64 or f64, cannot be represented as number in libconfig", n);
            }
        }
        Value::String(s) => LibConfigValue::String(s.to_string()),
        Value::Array(a) => {
            let items = a
                .iter()
                .map(|item| json_to_libconfig(item))
                .collect::<Vec<LibConfigValue>>();
            LibConfigValue::List(items)
        }
        Value::Object(o) => {
            if let Some(_type) = object_is_special_type(o) {
                let value = o
                    .get("value")
                    .expect(format!("Missing value for special type: {}", &_type).as_str());

                return match _type {
                    "octal" => {
                        let str_value = value
                            .as_str()
                            .expect(
                                format!("Value is not a string for special type: {}", &_type)
                                    .as_str(),
                            )
                            .to_owned();

                        LibConfigValue::Int(LibConfigIntNumber::Oct(
                            i64::from_str_radix(&str_value, 8)
                                .expect(format!("Invalid octal value: {}", value).as_str()),
                        ))
                    }
                    "hex" => {
                        let str_value = value
                            .as_str()
                            .expect(
                                format!("Value is not a string for special type: {}", &_type)
                                    .as_str(),
                            )
                            .to_owned();

                        LibConfigValue::Int(LibConfigIntNumber::Hex(
                            i64::from_str_radix(&str_value[2..], 16)
                                .expect(format!("Invalid hex value: {}", value).as_str()),
                        ))
                    }
                    "float" => {
                        let str_value = value
                            .as_str()
                            .expect(
                                format!("Value is not a string for special type: {}", &_type)
                                    .as_str(),
                            )
                            .to_owned();

                        LibConfigValue::Float(
                            str_value
                                .parse::<f64>()
                                .expect(format!("Invalid float value: {}", value).as_str()),
                        )
                    }
                    "list" => {
                        let items = value
                            .as_array()
                            .expect(
                                format!("Value is not an array for special type: {}", &_type)
                                    .as_str(),
                            )
                            .to_owned()
                            .iter()
                            .map(|item| json_to_libconfig(item))
                            .collect::<Vec<LibConfigValue>>();

                        LibConfigValue::List(items)
                    }
                    "array" => {
                        let items = value
                            .as_array()
                            .expect(
                                format!("Value is not an array for special type: {}", &_type)
                                    .as_str(),
                            )
                            .to_owned()
                            .iter()
                            .map(|item| json_to_libconfig(item))
                            .collect::<Vec<LibConfigValue>>();

                        if !vec_is_array(&items) {
                            panic!(
                                "This can not be an array because of its contents: {:#?}",
                                items
                            );
                        }

                        LibConfigValue::Array(items)
                    }
                    _ => panic!("Invalid type: {}", _type),
                };
            }

            let mut items = o
                .iter()
                .filter(|(key, _)| key.as_str() != "_includes")
                .map(|(key, value)| (key.clone(), json_to_libconfig(value)))
                .collect::<Vec<(String, LibConfigValue)>>();
            items.sort_by(|(a,_),(b,_)| a.partial_cmp(b).unwrap());

            let includes = o
                .get("_includes")
                .map(|x| {
                    x.as_array()
                        .expect("_includes is not an array")
                        .iter()
                        .map(|x| {
                            x.as_str()
                                .expect("_includes item is not a string")
                                .to_owned()
                        })
                        .collect::<Vec<String>>()
                })
                .unwrap_or(vec![]);

            for (key,_) in items.iter() {
                if !validate_setting_name(key) {
                    panic!("Invalid setting name: {}", key);
                }
            }
            LibConfigValue::Group(includes, items)
        }
    }
}

impl ToString for LibConfigValue {
    fn to_string(&self) -> String {
        match self {
            LibConfigValue::Bool(b) => b.to_string(),
            LibConfigValue::Int(i) => match i {
                LibConfigIntNumber::Oct(n) => format!("0{:o}", n),
                LibConfigIntNumber::Hex(n) => format!("0x{:x}", n),
                LibConfigIntNumber::Int(n) => n.to_string(),
            },
            LibConfigValue::Float(n) => format!("{:?}", n),
            LibConfigValue::String(s) => {
                format!("\"{}\"", s.replace("\\", "\\\\").replace("\"", "\\\""))
            }
            LibConfigValue::Array(a) => {
                let items = a
                    .iter()
                    .map(|item| item.to_string())
                    .collect::<Vec<String>>()
                    .join(", ");
                format!("[{}]", items)
            }
            LibConfigValue::List(a) => {
                let items = a
                    .iter()
                    .map(|item| item.to_string())
                    .collect::<Vec<String>>()
                    .join(", ");
                format!("({})", items)
            }
            LibConfigValue::Group(i, o) => {
                let includes = i
                    .iter()
                    .map(|x| x.replace("\\", "\\\\").replace("\"", "\\\""))
                    .map(|x| format!("@include \"{}\"", x))
                    .collect::<Vec<String>>()
                    .join("\n");
                let items = o
                    .iter()
                    .map(|(key, value)| format!("{}={};", key, value.to_string()))
                    .collect::<Vec<String>>()
                    .join("");
                if includes.is_empty() {
                    format!("{{{}}}", items)
                } else {
                    format!("{{\n{}\n{}}}", includes, items)
                }
            }
        }
    }
}

fn main() {
    let stdin = std::io::stdin().lock();
    let json = serde_json::Deserializer::from_reader(stdin)
        .into_iter::<Value>()
        .next()
        .expect("Could not read content from stdin")
        .expect("Could not parse JSON from stdin");

    for (key, value) in json
        .as_object()
        .expect("Top level of JSON file is not an object")
    {
        print!("{}={};", key, json_to_libconfig(value).to_string());
    }
    print!("\n\n");
}
Loading