package_json_parser 0.0.17

A parser for package.json
Documentation
use jsonc_parser::ast::ObjectProp;
use serde::de::{self, IgnoredAny, MapAccess, Visitor, value::MapAccessDeserializer};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
use validator::ValidateUrl;

use crate::ext::{Validator, validation_error, value_range};

#[derive(Debug, Serialize, Clone)]
pub struct Repository {
  #[serde(skip_serializing_if = "Option::is_none")]
  pub r#type: Option<String>,

  #[serde(skip_serializing_if = "Option::is_none")]
  pub url: Option<String>,

  #[serde(skip_serializing_if = "Option::is_none")]
  pub directory: Option<String>,
}

impl<'de> Deserialize<'de> for Repository {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    const FIELDS: &[&str] = &["type", "url", "directory"];

    enum Field {
      Type,
      Url,
      Directory,
      Ignore,
    }

    impl<'de> Deserialize<'de> for Field {
      fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
      where
        D: Deserializer<'de>,
      {
        struct FieldVisitor;

        impl<'de> Visitor<'de> for FieldVisitor {
          type Value = Field;

          fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
            formatter.write_str("`type`, `url` or `directory`")
          }

          fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
          where
            E: de::Error,
          {
            Ok(match value {
              "type" => Field::Type,
              "url" => Field::Url,
              "directory" => Field::Directory,
              _ => Field::Ignore,
            })
          }
        }

        deserializer.deserialize_identifier(FieldVisitor)
      }
    }

    struct RepositoryVisitor;

    impl<'de> Visitor<'de> for RepositoryVisitor {
      type Value = Repository;

      fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("an object for repository")
      }

      fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
      where
        M: MapAccess<'de>,
      {
        let mut r#type = None;
        let mut url = None;
        let mut directory = None;
        let mut seen_type = false;
        let mut seen_url = false;
        let mut seen_directory = false;

        while let Some(key) = map.next_key::<Field>()? {
          match key {
            Field::Type => {
              if seen_type {
                return Err(de::Error::duplicate_field("type"));
              }
              r#type = map.next_value()?;
              seen_type = true;
            }
            Field::Url => {
              if seen_url {
                return Err(de::Error::duplicate_field("url"));
              }
              url = map.next_value()?;
              seen_url = true;
            }
            Field::Directory => {
              if seen_directory {
                return Err(de::Error::duplicate_field("directory"));
              }
              directory = map.next_value()?;
              seen_directory = true;
            }
            Field::Ignore => {
              let _: IgnoredAny = map.next_value()?;
            }
          }
        }

        Ok(Repository {
          r#type,
          url,
          directory,
        })
      }
    }

    deserializer.deserialize_struct("Repository", FIELDS, RepositoryVisitor)
  }
}

#[derive(Debug, Serialize, Clone)]
pub enum RepositoryOrString {
  Repository(Repository),
  String(String),
}

impl<'de> Deserialize<'de> for RepositoryOrString {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    struct RepositoryOrStringVisitor;

    impl<'de> Visitor<'de> for RepositoryOrStringVisitor {
      type Value = RepositoryOrString;

      fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("a string or an object for repository")
      }

      fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
      where
        E: serde::de::Error,
      {
        Ok(RepositoryOrString::String(value.to_string()))
      }

      fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
      where
        E: serde::de::Error,
      {
        Ok(RepositoryOrString::String(value))
      }

      fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
      where
        M: MapAccess<'de>,
      {
        let repository = Repository::deserialize(MapAccessDeserializer::new(map))?;
        Ok(RepositoryOrString::Repository(repository))
      }
    }

    deserializer.deserialize_any(RepositoryOrStringVisitor)
  }
}

impl Validator for RepositoryOrString {
  fn validate(&self, repository: Option<&ObjectProp>) -> miette::Result<()> {
    match self {
      RepositoryOrString::Repository(repos) => {
        if let Some(url) = repos.url.as_ref() {
          if !url.validate_url() {
            return Err(validation_error(
              "Invalid url",
              Some("invalid_url"),
              "Please provide a valid url",
              value_range(repository, &["url"]),
              "Invalid url",
            ));
          }
        }
        Ok(())
      }
      RepositoryOrString::String(string) => {
        if !string.validate_url() {
          return Err(validation_error(
            "Invalid url",
            Some("invalid_url"),
            "Please provide a valid url",
            value_range(repository, &[]),
            "Invalid url",
          ));
        }
        Ok(())
      }
    }
  }
}

#[cfg(test)]
mod tests {
  use crate::PackageJsonParser;

  #[test]
  fn should_pass_validate_repository() {
    let jsones = [
      r#"{"repository": {"type": "git", "url": "https://github.com/rust-lang/rust", "directory": "src"}}"#,
      r#"{"repository": "https://github.com/rust-lang/rust"}"#,
    ];

    for json in jsones {
      let res = PackageJsonParser::parse_str(json).unwrap();
      let res = res.validate();
      assert!(res.is_ok());
    }
  }

  #[test]
  fn should_fail_validate_repository() {
    let jsones = [
      r#"{"repository": {"type": "git", "url": "invalid", "directory": "src"}}"#,
      r#"{"repository": "invalid"}"#,
    ];

    for json in jsones {
      let res = PackageJsonParser::parse_str(json).unwrap();
      let res = res.validate();
      assert!(res.is_err());
    }
  }

  #[test]
  fn should_deserialize_repository_successfully() {
    let parsed = PackageJsonParser::parse_str(
      r#"{"repository":{ "type": "git", "url": "https://example.com" }}"#,
    );
    assert!(parsed.is_ok());
  }

  #[test]
  fn should_fail_deserialize_repository_when_field_type_is_invalid() {
    let parsed = PackageJsonParser::parse_str(r#"{"repository":{ "url": true }}"#);
    assert!(parsed.is_ok());
    let parsed = parsed.unwrap();
    assert!(parsed.repository().is_err());
  }

  #[test]
  fn should_fail_deserialize_repository_or_string_when_type_is_invalid() {
    let parsed = PackageJsonParser::parse_str(r#"{"repository":123}"#);
    assert!(parsed.is_ok());
    let parsed = parsed.unwrap();
    assert!(parsed.repository().is_err());
  }

  #[test]
  fn should_fail_deserialize_repository_when_json_is_invalid() {
    let parsed = PackageJsonParser::parse_str("{");
    assert!(parsed.is_err());
  }
}