///|
pub(all) suberror SpecError {
  MissingOwner
  MissingRepo
  MissingVersion
  MissingPackagePath
  InvalidFormat(String)
} derive(Eq, Show)

///|
pub struct Spec {
  owner : String
  repo : String
  version : String?
  package_path : String
  package_name : String
} derive(Eq, Show)

///|
pub fn parse_spec(input : String) -> Result[Spec, SpecError] {
  let parts : Array[String] = input
    .split("/")
    .map(part => part.to_string())
    .to_array()
  if parts.length() == 0 || parts[0].length() == 0 {
    return Err(SpecError::MissingOwner)
  }
  if parts.length() < 2 || parts[1].length() == 0 {
    return Err(SpecError::MissingRepo)
  }
  if parts.length() < 3 {
    return Err(SpecError::MissingPackagePath)
  }
  let owner = parts[0]
  let repo_with_version = parts[1]
  let (repo, version) = match split_once(repo_with_version, "@") {
    Some(pair) => {
      let (repo, version) = pair
      if repo.length() == 0 {
        return Err(SpecError::InvalidFormat(input))
      }
      if version.length() == 0 {
        return Err(SpecError::MissingVersion)
      }
      (repo, Some(version))
    }
    None => {
      if repo_with_version.length() == 0 {
        return Err(SpecError::InvalidFormat(input))
      }
      (repo_with_version, None)
    }
  }
  let package_path = parts[2:].iter().join("/")
  if package_path.length() == 0 {
    return Err(SpecError::MissingPackagePath)
  }
  let package_name = parts[parts.length() - 1]
  Ok({ owner, repo, version, package_path, package_name })
}

///|
fn split_once(s : String, sep : StringView) -> (String, String)? {
  match s.find(sep) {
    Some(i) => {
      let sep_len = sep.length()
      try s[:i] catch {
        _ => None
      } noraise {
        left_view =>
          try s[i + sep_len:] catch {
            _ => None
          } noraise {
            right_view => Some((left_view.to_string(), right_view.to_string()))
          }
      }
    }
    None => None
  }
}