mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Fix PackageInfoShort to handle LocaleString on releaseNotes (#3112)
* Fix PackageInfoShort to handle LocaleString on releaseNotes * fix: filter by target_version in get_matching_models and pass otherVersions from install * chore: add exver documentation for ai agents
This commit is contained in:
301
agents/exver.md
Normal file
301
agents/exver.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# exver — Extended Versioning
|
||||||
|
|
||||||
|
Extended semver supporting **downstream versioning** (wrapper updates independent of upstream) and **flavors** (package fork variants).
|
||||||
|
|
||||||
|
Two implementations exist:
|
||||||
|
- **Rust crate** (`exver`) — used in `core/`. Source: https://github.com/Start9Labs/exver-rs
|
||||||
|
- **TypeScript** (`sdk/base/lib/exver/index.ts`) — used in `sdk/` and `web/`
|
||||||
|
|
||||||
|
Both parse the same string format and agree on `satisfies` semantics.
|
||||||
|
|
||||||
|
## Version Format
|
||||||
|
|
||||||
|
An **ExtendedVersion** string looks like:
|
||||||
|
|
||||||
|
```
|
||||||
|
[#flavor:]upstream:downstream
|
||||||
|
```
|
||||||
|
|
||||||
|
- **upstream** — the original package version (semver-style: `1.2.3`, `1.2.3-beta.1`)
|
||||||
|
- **downstream** — the StartOS wrapper version (incremented independently)
|
||||||
|
- **flavor** — optional lowercase ASCII prefix for fork variants
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `1.2.3:0` — upstream 1.2.3, first downstream release
|
||||||
|
- `1.2.3:2` — upstream 1.2.3, third downstream release
|
||||||
|
- `#bitcoin:21.0:1` — bitcoin flavor, upstream 21.0, downstream 1
|
||||||
|
- `1.0.0-rc.1:0` — upstream with prerelease tag
|
||||||
|
|
||||||
|
## Core Types
|
||||||
|
|
||||||
|
### `Version`
|
||||||
|
|
||||||
|
A semver-style version with arbitrary digit segments and optional prerelease.
|
||||||
|
|
||||||
|
**Rust:**
|
||||||
|
```rust
|
||||||
|
use exver::Version;
|
||||||
|
|
||||||
|
let v = Version::new([1, 2, 3], []); // 1.2.3
|
||||||
|
let v = Version::new([1, 0], ["beta".into()]); // 1.0-beta
|
||||||
|
let v: Version = "1.2.3".parse().unwrap();
|
||||||
|
|
||||||
|
v.number() // &[1, 2, 3]
|
||||||
|
v.prerelease() // &[]
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeScript:**
|
||||||
|
```typescript
|
||||||
|
const v = new Version([1, 2, 3], [])
|
||||||
|
const v = Version.parse("1.2.3")
|
||||||
|
|
||||||
|
v.number // number[]
|
||||||
|
v.prerelease // (string | number)[]
|
||||||
|
v.compare(other) // 'greater' | 'equal' | 'less'
|
||||||
|
v.compareForSort(other) // -1 | 0 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Default: `0`
|
||||||
|
|
||||||
|
### `ExtendedVersion`
|
||||||
|
|
||||||
|
The primary version type. Wraps upstream + downstream `Version` plus an optional flavor.
|
||||||
|
|
||||||
|
**Rust:**
|
||||||
|
```rust
|
||||||
|
use exver::ExtendedVersion;
|
||||||
|
|
||||||
|
let ev = ExtendedVersion::new(
|
||||||
|
Version::new([1, 2, 3], []),
|
||||||
|
Version::default(), // downstream = 0
|
||||||
|
);
|
||||||
|
let ev: ExtendedVersion = "1.2.3:0".parse().unwrap();
|
||||||
|
|
||||||
|
ev.flavor() // Option<&str>
|
||||||
|
ev.upstream() // &Version
|
||||||
|
ev.downstream() // &Version
|
||||||
|
|
||||||
|
// Builder methods (consuming):
|
||||||
|
ev.with_flavor("bitcoin")
|
||||||
|
ev.without_flavor()
|
||||||
|
ev.map_upstream(|v| ...)
|
||||||
|
ev.map_downstream(|v| ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeScript:**
|
||||||
|
```typescript
|
||||||
|
const ev = new ExtendedVersion(null, upstream, downstream)
|
||||||
|
const ev = ExtendedVersion.parse("1.2.3:0")
|
||||||
|
const ev = ExtendedVersion.parseEmver("1.2.3.4") // emver compat
|
||||||
|
|
||||||
|
ev.flavor // string | null
|
||||||
|
ev.upstream // Version
|
||||||
|
ev.downstream // Version
|
||||||
|
|
||||||
|
ev.compare(other) // 'greater' | 'equal' | 'less' | null
|
||||||
|
ev.equals(other) // boolean
|
||||||
|
ev.greaterThan(other) // boolean
|
||||||
|
ev.lessThan(other) // boolean
|
||||||
|
ev.incrementMajor() // new ExtendedVersion
|
||||||
|
ev.incrementMinor() // new ExtendedVersion
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ordering:** Versions with different flavors are **not comparable** (`PartialOrd`/`compare` returns `None`/`null`).
|
||||||
|
|
||||||
|
Default: `0:0`
|
||||||
|
|
||||||
|
### `VersionString` (Rust only, StartOS wrapper)
|
||||||
|
|
||||||
|
Defined in `core/src/util/version.rs`. Caches the original string representation alongside the parsed `ExtendedVersion`. Used as the key type in registry version maps.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::util::VersionString;
|
||||||
|
|
||||||
|
let vs: VersionString = "1.2.3:0".parse().unwrap();
|
||||||
|
let vs = VersionString::from(extended_version);
|
||||||
|
|
||||||
|
// Deref to ExtendedVersion:
|
||||||
|
vs.satisfies(&range);
|
||||||
|
vs.upstream();
|
||||||
|
|
||||||
|
// String access:
|
||||||
|
vs.as_str(); // &str
|
||||||
|
AsRef::<str>::as_ref(&vs);
|
||||||
|
```
|
||||||
|
|
||||||
|
`Ord` is implemented with a total ordering — versions with different flavors are ordered by flavor name (unflavored sorts last).
|
||||||
|
|
||||||
|
### `VersionRange`
|
||||||
|
|
||||||
|
A predicate over `ExtendedVersion`. Supports comparison operators, boolean logic, and flavor constraints.
|
||||||
|
|
||||||
|
**Rust:**
|
||||||
|
```rust
|
||||||
|
use exver::VersionRange;
|
||||||
|
|
||||||
|
// Constructors:
|
||||||
|
VersionRange::any() // matches everything
|
||||||
|
VersionRange::none() // matches nothing
|
||||||
|
VersionRange::exactly(ev) // = ev
|
||||||
|
VersionRange::anchor(GTE, ev) // >= ev
|
||||||
|
VersionRange::caret(ev) // ^ev (compatible changes)
|
||||||
|
VersionRange::tilde(ev) // ~ev (patch-level changes)
|
||||||
|
|
||||||
|
// Combinators (smart — eagerly simplify):
|
||||||
|
VersionRange::and(a, b) // a && b
|
||||||
|
VersionRange::or(a, b) // a || b
|
||||||
|
VersionRange::not(a) // !a
|
||||||
|
|
||||||
|
// Parsing:
|
||||||
|
let r: VersionRange = ">=1.0.0:0".parse().unwrap();
|
||||||
|
let r: VersionRange = "^1.2.3:0".parse().unwrap();
|
||||||
|
let r: VersionRange = ">=1.0.0 <2.0.0".parse().unwrap(); // implicit AND
|
||||||
|
let r: VersionRange = ">=1.0.0 || >=2.0.0".parse().unwrap();
|
||||||
|
let r: VersionRange = "#bitcoin".parse().unwrap(); // flavor match
|
||||||
|
let r: VersionRange = "*".parse().unwrap(); // any
|
||||||
|
|
||||||
|
// Monoid wrappers for folding:
|
||||||
|
AnyRange // fold with or, empty = None
|
||||||
|
AllRange // fold with and, empty = Any
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeScript:**
|
||||||
|
```typescript
|
||||||
|
// Constructors:
|
||||||
|
VersionRange.any()
|
||||||
|
VersionRange.none()
|
||||||
|
VersionRange.anchor('=', ev)
|
||||||
|
VersionRange.anchor('>=', ev)
|
||||||
|
VersionRange.anchor('^', ev) // ^ and ~ are first-class operators
|
||||||
|
VersionRange.anchor('~', ev)
|
||||||
|
VersionRange.flavor(null) // match unflavored versions
|
||||||
|
VersionRange.flavor("bitcoin") // match #bitcoin versions
|
||||||
|
|
||||||
|
// Combinators — static (smart, variadic):
|
||||||
|
VersionRange.and(a, b, c, ...)
|
||||||
|
VersionRange.or(a, b, c, ...)
|
||||||
|
|
||||||
|
// Combinators — instance (not smart, just wrap):
|
||||||
|
range.and(other)
|
||||||
|
range.or(other)
|
||||||
|
range.not()
|
||||||
|
|
||||||
|
// Parsing:
|
||||||
|
VersionRange.parse(">=1.0.0:0")
|
||||||
|
VersionRange.parseEmver(">=1.2.3.4") // emver compat
|
||||||
|
|
||||||
|
// Analysis (TS only):
|
||||||
|
range.normalize() // canonical form (see below)
|
||||||
|
range.satisfiable() // boolean
|
||||||
|
range.intersects(other) // boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
**Checking satisfaction:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Rust:
|
||||||
|
version.satisfies(&range) // bool
|
||||||
|
```
|
||||||
|
```typescript
|
||||||
|
// TypeScript:
|
||||||
|
version.satisfies(range) // boolean
|
||||||
|
range.satisfiedBy(version) // boolean (convenience)
|
||||||
|
```
|
||||||
|
|
||||||
|
Also available on `Version` (wraps in `ExtendedVersion` with downstream=0).
|
||||||
|
|
||||||
|
When no operator is specified in a range string, `^` (caret) is the default.
|
||||||
|
|
||||||
|
## Operators
|
||||||
|
|
||||||
|
| Syntax | Rust | TS | Meaning |
|
||||||
|
|--------|------|----|---------|
|
||||||
|
| `=` | `EQ` | `'='` | Equal |
|
||||||
|
| `!=` | `NEQ` | `'!='` | Not equal |
|
||||||
|
| `>` | `GT` | `'>'` | Greater than |
|
||||||
|
| `>=` | `GTE` | `'>='` | Greater than or equal |
|
||||||
|
| `<` | `LT` | `'<'` | Less than |
|
||||||
|
| `<=` | `LTE` | `'<='` | Less than or equal |
|
||||||
|
| `^` | expanded to `And(GTE, LT)` | `'^'` | Compatible (first non-zero digit unchanged) |
|
||||||
|
| `~` | expanded to `And(GTE, LT)` | `'~'` | Patch-level (minor unchanged) |
|
||||||
|
|
||||||
|
## Flavor Rules
|
||||||
|
|
||||||
|
- Versions with **different flavors** never satisfy comparison operators (except `!=`, which returns true)
|
||||||
|
- `VersionRange::Flavor(Some("bitcoin"))` matches only `#bitcoin:*` versions
|
||||||
|
- `VersionRange::Flavor(None)` matches only unflavored versions
|
||||||
|
- Flavor constraints compose with `and`/`or`/`not` like any other range
|
||||||
|
|
||||||
|
## Reduction and Normalization
|
||||||
|
|
||||||
|
### Rust: `reduce()` (shallow)
|
||||||
|
|
||||||
|
`VersionRange::reduce(self) -> Self` re-applies smart constructor rules to one level of the AST. Useful for simplifying a node that was constructed directly (e.g. deserialized) rather than through the smart constructors.
|
||||||
|
|
||||||
|
**Smart constructor rules applied by `and`, `or`, `not`, and `reduce`:**
|
||||||
|
|
||||||
|
`and`:
|
||||||
|
- `and(Any, b) → b`, `and(a, Any) → a`
|
||||||
|
- `and(None, _) → None`, `and(_, None) → None`
|
||||||
|
|
||||||
|
`or`:
|
||||||
|
- `or(Any, _) → Any`, `or(_, Any) → Any`
|
||||||
|
- `or(None, b) → b`, `or(a, None) → a`
|
||||||
|
|
||||||
|
`not`:
|
||||||
|
- `not(=v) → !=v`, `not(!=v) → =v`
|
||||||
|
- `not(and(a, b)) → or(not(a), not(b))` (De Morgan)
|
||||||
|
- `not(or(a, b)) → and(not(a), not(b))` (De Morgan)
|
||||||
|
- `not(not(a)) → a`
|
||||||
|
- `not(Any) → None`, `not(None) → Any`
|
||||||
|
|
||||||
|
### TypeScript: `normalize()` (deep, canonical)
|
||||||
|
|
||||||
|
`VersionRange.normalize(): VersionRange` in `sdk/base/lib/exver/index.ts` performs full normalization by converting the range AST into a canonical form. This is a deep operation that produces a semantically equivalent but simplified range.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
|
||||||
|
1. **`tables()`** — Converts the VersionRange AST into truth tables (`VersionRangeTable`). Each table is a number line split at version boundary points, with boolean values for each segment indicating whether versions in that segment satisfy the range. Separate tables are maintained per flavor (and for flavor negations).
|
||||||
|
|
||||||
|
2. **`VersionRangeTable.zip(a, b, func)`** — Merges two tables by walking their boundary points in sorted order and applying a boolean function (`&&` or `||`) to combine segment values. Adjacent segments with the same boolean value are collapsed automatically.
|
||||||
|
|
||||||
|
3. **`VersionRangeTable.and/or/not`** — Table-level boolean operations. `and` computes the cross-product of flavor tables (since `#a && #b` for different flavors is unsatisfiable). `not` inverts all segment values.
|
||||||
|
|
||||||
|
4. **`VersionRangeTable.collapse()`** — Checks if a table is uniformly true or false across all flavors and segments. Returns `true`, `false`, or `null` (mixed).
|
||||||
|
|
||||||
|
5. **`VersionRangeTable.minterms()`** — Converts truth tables back into a VersionRange AST in [sum-of-products](https://en.wikipedia.org/wiki/Canonical_normal_form#Minterms) canonical form. Each `true` segment becomes a product term (conjunction of boundary constraints), and all terms are joined with `or`. Adjacent boundary points collapse into `=` anchors.
|
||||||
|
|
||||||
|
**Example:** `normalize` can simplify:
|
||||||
|
- `>=1.0.0:0 && <=1.0.0:0` → `=1.0.0:0`
|
||||||
|
- `>=2.0.0:0 || >=1.0.0:0` → `>=1.0.0:0`
|
||||||
|
- `!(!>=1.0.0:0)` → `>=1.0.0:0`
|
||||||
|
|
||||||
|
**Also exposes:**
|
||||||
|
- `satisfiable(): boolean` — returns `true` if there exists any version satisfying the range (checks if `collapse(tables())` is not `false`)
|
||||||
|
- `intersects(other): boolean` — returns `true` if `and(this, other)` is satisfiable
|
||||||
|
|
||||||
|
## API Differences Between Rust and TypeScript
|
||||||
|
|
||||||
|
| | Rust | TypeScript |
|
||||||
|
|-|------|------------|
|
||||||
|
| **`^` / `~`** | Expanded at construction to `And(GTE, LT)` | First-class operator on `Anchor` |
|
||||||
|
| **`not()`** | Static, eagerly simplifies (De Morgan, double negation) | Instance method, just wraps |
|
||||||
|
| **`and()`/`or()`** | Binary static | Both binary instance and variadic static |
|
||||||
|
| **Normalization** | `reduce()` — shallow, one AST level | `normalize()` — deep canonical form via truth tables |
|
||||||
|
| **Satisfiability** | Not available | `satisfiable()` and `intersects(other)` |
|
||||||
|
| **ExtendedVersion helpers** | `with_flavor()`, `without_flavor()`, `map_upstream()`, `map_downstream()` | `incrementMajor()`, `incrementMinor()`, `greaterThan()`, `lessThan()`, `equals()`, etc. |
|
||||||
|
| **Monoid wrappers** | `AnyRange` (fold with `or`) and `AllRange` (fold with `and`) | Not present — use variadic static methods |
|
||||||
|
| **`VersionString`** | Wrapper caching parsed + string form | Not present |
|
||||||
|
| **Emver compat** | `From<emver::Version>` for `ExtendedVersion` | `ExtendedVersion.parseEmver()`, `VersionRange.parseEmver()` |
|
||||||
|
|
||||||
|
## Serde
|
||||||
|
|
||||||
|
All types serialize/deserialize as strings (requires `serde` feature, enabled in StartOS):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.2.3:0",
|
||||||
|
"targetVersion": ">=1.0.0:0 <2.0.0:0",
|
||||||
|
"sourceVersion": "^0.3.0:0"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -137,6 +137,7 @@ pub async fn install(
|
|||||||
json!({
|
json!({
|
||||||
"id": id,
|
"id": id,
|
||||||
"targetVersion": VersionRange::exactly(version.deref().clone()),
|
"targetVersion": VersionRange::exactly(version.deref().clone()),
|
||||||
|
"otherVersions": "none",
|
||||||
}),
|
}),
|
||||||
RegistryUrlParams {
|
RegistryUrlParams {
|
||||||
registry: registry.clone(),
|
registry: registry.clone(),
|
||||||
@@ -484,7 +485,7 @@ pub async fn cli_install(
|
|||||||
let mut packages: GetPackageResponse = from_value(
|
let mut packages: GetPackageResponse = from_value(
|
||||||
ctx.call_remote::<RegistryContext>(
|
ctx.call_remote::<RegistryContext>(
|
||||||
"package.get",
|
"package.get",
|
||||||
json!({ "id": &id, "targetVersion": version, "sourceVersion": source_version }),
|
json!({ "id": &id, "targetVersion": version, "sourceVersion": source_version, "otherVersions": "none" }),
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::progress::{FullProgressTracker, ProgressUnits};
|
|||||||
use crate::registry::context::RegistryContext;
|
use crate::registry::context::RegistryContext;
|
||||||
use crate::registry::device_info::DeviceInfo;
|
use crate::registry::device_info::DeviceInfo;
|
||||||
use crate::registry::package::index::{PackageIndex, PackageVersionInfo};
|
use crate::registry::package::index::{PackageIndex, PackageVersionInfo};
|
||||||
|
use crate::s9pk::manifest::LocaleString;
|
||||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||||
use crate::s9pk::v2::SIG_CONTEXT;
|
use crate::s9pk::v2::SIG_CONTEXT;
|
||||||
use crate::util::VersionString;
|
use crate::util::VersionString;
|
||||||
@@ -38,11 +39,11 @@ impl Default for PackageDetailLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct PackageInfoShort {
|
pub struct PackageInfoShort {
|
||||||
pub release_notes: String,
|
pub release_notes: LocaleString,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, TS, Parser, HasModel)]
|
#[derive(Debug, Deserialize, Serialize, TS, Parser, HasModel)]
|
||||||
@@ -89,17 +90,20 @@ impl GetPackageResponse {
|
|||||||
|
|
||||||
let lesser_versions: BTreeMap<_, _> = self
|
let lesser_versions: BTreeMap<_, _> = self
|
||||||
.other_versions
|
.other_versions
|
||||||
.as_ref()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter(|(v, _)| ***v < *version)
|
.filter(|(v, _)| **v < *version)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !lesser_versions.is_empty() {
|
if !lesser_versions.is_empty() {
|
||||||
table.add_row(row![bc => "OLDER VERSIONS"]);
|
table.add_row(row![bc => "OLDER VERSIONS"]);
|
||||||
table.add_row(row![bc => "VERSION", "RELEASE NOTES"]);
|
table.add_row(row![bc => "VERSION", "RELEASE NOTES"]);
|
||||||
for (version, info) in lesser_versions {
|
for (version, info) in lesser_versions {
|
||||||
table.add_row(row![AsRef::<str>::as_ref(version), &info.release_notes]);
|
table.add_row(row![
|
||||||
|
AsRef::<str>::as_ref(&version),
|
||||||
|
&info.release_notes.localized()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +151,7 @@ fn get_matching_models(
|
|||||||
id,
|
id,
|
||||||
source_version,
|
source_version,
|
||||||
device_info,
|
device_info,
|
||||||
|
target_version,
|
||||||
..
|
..
|
||||||
}: &GetPackageParams,
|
}: &GetPackageParams,
|
||||||
) -> Result<Vec<(PackageId, ExtendedVersion, Model<PackageVersionInfo>)>, Error> {
|
) -> Result<Vec<(PackageId, ExtendedVersion, Model<PackageVersionInfo>)>, Error> {
|
||||||
@@ -165,26 +170,29 @@ fn get_matching_models(
|
|||||||
.as_entries()?
|
.as_entries()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(v, info)| {
|
.map(|(v, info)| {
|
||||||
|
let ev = ExtendedVersion::from(v);
|
||||||
Ok::<_, Error>(
|
Ok::<_, Error>(
|
||||||
if source_version.as_ref().map_or(Ok(true), |source_version| {
|
if target_version.as_ref().map_or(true, |tv| ev.satisfies(tv))
|
||||||
Ok::<_, Error>(
|
&& source_version.as_ref().map_or(Ok(true), |source_version| {
|
||||||
source_version.satisfies(
|
Ok::<_, Error>(
|
||||||
&info
|
source_version.satisfies(
|
||||||
.as_source_version()
|
&info
|
||||||
.de()?
|
.as_source_version()
|
||||||
.unwrap_or(VersionRange::any()),
|
.de()?
|
||||||
),
|
.unwrap_or(VersionRange::any()),
|
||||||
)
|
),
|
||||||
})? {
|
)
|
||||||
|
})?
|
||||||
|
{
|
||||||
let mut info = info.clone();
|
let mut info = info.clone();
|
||||||
if let Some(device_info) = &device_info {
|
if let Some(device_info) = &device_info {
|
||||||
if info.for_device(device_info)? {
|
if info.for_device(device_info)? {
|
||||||
Some((k.clone(), ExtendedVersion::from(v), info))
|
Some((k.clone(), ev, info))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Some((k.clone(), ExtendedVersion::from(v), info))
|
Some((k.clone(), ev, info))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -207,12 +215,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
|
|||||||
for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? {
|
for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? {
|
||||||
let package_best = best.entry(id.clone()).or_default();
|
let package_best = best.entry(id.clone()).or_default();
|
||||||
let package_other = other.entry(id.clone()).or_default();
|
let package_other = other.entry(id.clone()).or_default();
|
||||||
if params
|
if package_best.keys().all(|k| !(**k > version)) {
|
||||||
.target_version
|
|
||||||
.as_ref()
|
|
||||||
.map_or(true, |v| version.satisfies(v))
|
|
||||||
&& package_best.keys().all(|k| !(**k > version))
|
|
||||||
{
|
|
||||||
for worse_version in package_best
|
for worse_version in package_best
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|k| ***k < version)
|
.filter(|k| ***k < version)
|
||||||
@@ -569,3 +572,42 @@ pub async fn cli_download(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_matching_info_short() {
|
||||||
|
use crate::registry::package::index::PackageMetadata;
|
||||||
|
use crate::s9pk::manifest::{Alerts, Description};
|
||||||
|
use crate::util::DataUrl;
|
||||||
|
|
||||||
|
let lang_map = |s: &str| {
|
||||||
|
LocaleString::LanguageMap([("en".into(), s.into())].into_iter().collect())
|
||||||
|
};
|
||||||
|
|
||||||
|
let info = PackageVersionInfo {
|
||||||
|
metadata: PackageMetadata {
|
||||||
|
title: "Test Package".into(),
|
||||||
|
icon: DataUrl::from_vec("image/png", vec![]),
|
||||||
|
description: Description {
|
||||||
|
short: lang_map("A short description"),
|
||||||
|
long: lang_map("A longer description of the test package"),
|
||||||
|
},
|
||||||
|
release_notes: lang_map("Initial release"),
|
||||||
|
git_hash: None,
|
||||||
|
license: "MIT".into(),
|
||||||
|
wrapper_repo: "https://github.com/example/wrapper".parse().unwrap(),
|
||||||
|
upstream_repo: "https://github.com/example/upstream".parse().unwrap(),
|
||||||
|
support_site: "https://example.com/support".parse().unwrap(),
|
||||||
|
marketing_site: "https://example.com".parse().unwrap(),
|
||||||
|
donation_url: None,
|
||||||
|
docs_url: None,
|
||||||
|
alerts: Alerts::default(),
|
||||||
|
dependency_metadata: BTreeMap::new(),
|
||||||
|
os_version: exver::Version::new([0, 3, 6], []),
|
||||||
|
sdk_version: None,
|
||||||
|
hardware_acceleration: false,
|
||||||
|
},
|
||||||
|
source_version: None,
|
||||||
|
s9pks: Vec::new(),
|
||||||
|
};
|
||||||
|
from_value::<PackageInfoShort>(to_value(&info).unwrap()).unwrap();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user