///|
/// A dependency of one published version, as its `moon.mod` declared it.
///
/// Most are `"name": "version"`. Some name a local `path` (with or without a
/// version) or import the module under another name (`alias` in the index).
pub struct Dep {
name : String
/// `None` for a local path dependency published without a version.
version : String?
/// The name the module is imported as, when not its own.
imported_as : String?
path : String?
} derive(Eq, Debug)
///|
pub extend Dep with Eq::{equal, not_equal}
///|
pub extend Dep with @debug.Debug::{to_repr}
///|
pub fn Dep::new(
name : String,
version? : String,
imported_as? : String,
path? : String,
) -> Dep {
{ name, version, imported_as, path, }
}
///|
/// What a version says about its module, from its `moon.mod`. Every field is
/// optional in the index.
pub struct About {
description : String?
repository : String?
license : String?
keywords : Array[String]
/// Why the version was yanked, when it was and the owner said.
yanked_reason : String?
} derive(Eq, Debug)
///|
pub extend About with Eq::{equal, not_equal}
///|
pub extend About with @debug.Debug::{to_repr}
///|
pub fn About::new(
description? : String,
repository? : String,
license? : String,
keywords~ : Array[String] = [],
yanked_reason? : String,
) -> About {
{ description, repository, license, keywords, yanked_reason, }
}
///|
/// One line of the index: one published version of one module.
pub struct VersionRecord {
name : String
version : String
deps : Array[Dep]
/// Missing on some early versions.
created_at : String?
yanked : Bool
deprecated : String?
about : About
} derive(Eq, Debug)
///|
pub extend VersionRecord with Eq::{equal, not_equal}
///|
pub extend VersionRecord with @debug.Debug::{to_repr}
///|
pub fn VersionRecord::new(
name : String,
version~ : String,
deps~ : Array[Dep] = [],
created_at? : String,
yanked~ : Bool = false,
deprecated? : String,
about~ : About = About::new(),
) -> VersionRecord {
{ name, version, deps, created_at, yanked, deprecated, about, }
}
///|
/// A line that is not a version record.
pub(all) suberror ParseError {
InvalidJson(message~ : String)
MissingField(field~ : String)
} derive(Eq, Debug)
///|
pub extend ParseError with Eq::{equal, not_equal}
///|
pub extend ParseError with @debug.Debug::{to_repr}
///|
/// Parses one line of the index, or one line of the natural JSONL that
/// `VersionRecord::to_json` writes; the latter is a subset of the former.
///
/// Fields other than the ones in `VersionRecord` and `About` are ignored;
/// empty strings in `About` read as absent. A dependency is either
/// `"name": "version"` or an object with any of `version`, `alias` and
/// `path`.
pub fn parse_line(line : StringView) -> VersionRecord raise ParseError {
let json = @json.parse(line) catch {
e => raise InvalidJson(message=e.to_string())
}
guard json is Object(obj) else { raise InvalidJson(message="not an object") }
guard obj.get("name") is Some(String(name)) else {
raise MissingField(field="name")
}
guard obj.get("version") is Some(String(version)) else {
raise MissingField(field="version")
}
let deps = []
if obj.get("deps") is Some(Object(map)) {
for dep, req in map {
let field = fn(o : Map[String, Json], key : String) -> String? {
if o.get(key) is Some(String(v)) {
Some(v)
} else {
None
}
}
deps.push(
match req {
String(v) =>
{ name: dep, version: Some(v), imported_as: None, path: None, }
Object(o) =>
{
name: dep,
version: field(o, "version"),
imported_as: field(o, "alias"),
path: field(o, "path"),
}
_ => { name: dep, version: None, imported_as: None, path: None, }
},
)
}
}
let created_at = if obj.get("created_at") is Some(String(t)) {
Some(t)
} else {
None
}
let yanked = obj.get("yanked") is Some(True)
let deprecated = if obj.get("deprecated") is Some(String(d)) {
Some(d)
} else {
None
}
let text = fn(key : String) -> String? {
match obj.get(key) {
Some(String(v)) if !v.trim().is_empty() => Some(v)
_ => None
}
}
let keywords = []
if obj.get("keywords") is Some(Array(list)) {
for k in list {
if k is String(k) && !k.trim().is_empty() {
keywords.push(k)
}
}
}
let about = {
description: text("description"),
repository: text("repository"),
license: text("license"),
keywords,
yanked_reason: text("yanked_reason"),
}
{ name, version, deps, created_at, yanked, deprecated, about, }
}
///|
/// The fields that are present, under their index names.
pub impl ToJson for About with fn to_json(self) {
let obj : Map[String, Json] = Map([])
let put = fn(key : String, value : String?) {
if value is Some(v) {
obj[key] = Json::string(v)
}
}
put("description", self.description)
put("repository", self.repository)
put("license", self.license)
if !self.keywords.is_empty() {
obj["keywords"] = Json::array(self.keywords.map(Json::string))
}
put("yanked_reason", self.yanked_reason)
Json::object(obj)
}
///|
pub extend About with ToJson::{to_json}
///|
/// The natural JSONL shape:
/// `{"name","version","created_at","deps":{…},"yanked","deprecated"?}`
/// followed by the `About` fields that are present. A dependency is its
/// version (or `null`), or, when it has another import name or a path, an
/// object of the fields it has, under the index's names.
pub impl ToJson for VersionRecord with fn to_json(self) {
let deps : Map[String, Json] = Map([])
for dep in self.deps {
deps[dep.name] = match dep {
{ imported_as: None, path: None, version: Some(v), .. } => Json::string(v)
{ imported_as: None, path: None, version: None, .. } => Json::null()
_ => {
let o : Map[String, Json] = Map([])
for
field in [
("version", dep.version),
("alias", dep.imported_as),
("path", dep.path),
] {
if field.1 is Some(v) {
o[field.0] = Json::string(v)
}
}
Json::object(o)
}
}
}
let obj : Map[String, Json] = Map([
("name", Json::string(self.name)),
("version", Json::string(self.version)),
(
"created_at",
match self.created_at {
Some(t) => Json::string(t)
None => Json::null()
},
),
("deps", Json::object(deps)),
("yanked", Json::boolean(self.yanked)),
])
if self.deprecated is Some(d) {
obj["deprecated"] = Json::string(d)
}
if self.about.to_json() is Object(about) {
for key, value in about {
obj[key] = value
}
}
Json::object(obj)
}
///|
/// The owner part of a module name: `"moonbitlang/x"` → `"moonbitlang"`.
pub fn owner(name : StringView) -> StringView {
match name.find("/") {
Some(i) => name[:i]
None => name
}
}
///|
pub extend VersionRecord with ToJson::{to_json}
///|
/// Lexicographic order by UTF-16 code unit, which for module names (ASCII)
/// is byte order. `String::compare` orders by length first, so a sorted
/// listing would not look sorted.
pub fn compare_names(a : String, b : String) -> Int32 {
let n = if a.length() < b.length() { a.length() } else { b.length() }
for i in 0.. Int32 {
match (parse_version(a), parse_version(b)) {
(Some((ca, pa)), Some((cb, pb))) => {
for i in 0..<3 {
let c = ca[i].compare(cb[i])
if c != 0 {
return c
}
}
match (pa, pb) {
([], []) => 0
([], _) => 1
(_, []) => -1
_ => {
let n = if pa.length() < pb.length() {
pa.length()
} else {
pb.length()
}
for i in 0.. compare_names(a, b)
}
}
///|
/// Whether `v` is a semantic version, so `compare_versions` orders it as one.
/// Requirements such as `*` or `^0.1.0` are not.
pub fn is_version(v : String) -> Bool {
parse_version(v) is Some(_)
}
///|
/// The release line semantic versioning keeps compatible within: `"0.22"`
/// for `0.22.5` (a minor bump breaks before 1.0), `"2"` for `2.3.1`.
pub fn release_line(v : String) -> String? {
guard parse_version(v) is Some((n, _)) else { return None }
Some(if n[0] == 0L { "0.\{n[1]}" } else { "\{n[0]}" })
}
///|
fn parse_version(v : String) -> (FixedArray[Int64], Array[String])? {
let v = match v.find("+") {
Some(i) => v[:i]
None => v[:]
}
let (core, pre) = match v.find("-") {
Some(i) => (v[:i], v[i + 1:].split(".").map(s => s.to_owned()).collect())
None => (v, [])
}
let parts = core.split(".").collect()
guard parts.length() == 3 else { return None }
let nums = FixedArray::make(3, 0L)
for i, p in parts {
guard !p.is_empty() && p.iter().all(c => c >= '0' && c <= '9') else {
return None
}
nums[i] = @string.parse_int64(p) catch { _ => return None }
}
Some((nums, pre))
}
///|
fn compare_identifiers(a : String, b : String) -> Int32 {
let numeric = fn(s : String) {
!s.is_empty() && s.iter().all(c => c >= '0' && c <= '9')
}
match (numeric(a), numeric(b)) {
(true, true) =>
if a.length() != b.length() {
a.length().compare(b.length())
} else {
compare_names(a, b)
}
(true, false) => -1
(false, true) => 1
(false, false) => compare_names(a, b)
}
}