Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 87 additions & 36 deletions crates/workshop-rs/src/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//! vocabulary. Consumers compose and present the matches; this module owns
//! no data of its own.

use crate::catalog::{Catalog, CatalogEntry, Kind, Locale};
use crate::catalog::{Catalog, CatalogEntry, EnumDomain, Kind, Locale};
use crate::core::error::{Result, WorkshopError};
use crate::core::suggest;
use crate::settings::{self, PathPart, SettingDefinition, SettingValueDomain, table};
Expand Down Expand Up @@ -178,15 +178,7 @@ impl Catalog {
/// )));
/// ```
pub fn lookup(&self, locale: &Locale, query: &str) -> Result<Vec<LookupMatch>> {
if !self.supports(locale) {
return Err(WorkshopError::unsupported(
format!(
"lookup for locale '{locale}' is not supported: \
the catalog does not declare it"
),
None,
));
}
self.check_locale(locale)?;
let primary = self.primary_locale();
let distinct_locale = locale != primary;
let prepared = PreparedQuery::new(query);
Expand All @@ -204,15 +196,7 @@ impl Catalog {
.filter_map(|text| prepared.name_score(text))
.min();
if let Some(score) = score {
matches.push((
score,
LookupMatch::Builtin {
kind: entry.kind,
id: entry.id.clone(),
display_name: entry.spelling(locale).map(String::from),
signature: signature(self, entry, locale),
},
));
matches.push((score, self.builtin_match(entry, locale)));
}
}
for domain in self.enum_domains() {
Expand All @@ -222,14 +206,7 @@ impl Catalog {
.filter_map(|text| prepared.name_score(text))
.min();
if let Some(score) = score {
matches.push((
score,
LookupMatch::EnumDomain {
domain: domain.domain.clone(),
display_name: domain.spelling(locale).map(String::from),
members: self.enum_members(&domain.domain, locale),
},
));
matches.push((score, self.enum_domain_match(domain, locale)));
}
}
for domain in self.enum_domains() {
Expand Down Expand Up @@ -332,15 +309,7 @@ impl Catalog {
within: &str,
query: Option<&str>,
) -> Result<Vec<LookupMatch>> {
if !self.supports(locale) {
return Err(WorkshopError::unsupported(
format!(
"lookup for locale '{locale}' is not supported: \
the catalog does not declare it"
),
None,
));
}
self.check_locale(locale)?;
let prepared = query
.filter(|query| !query.is_empty())
.map(PreparedQuery::new);
Expand All @@ -357,6 +326,88 @@ impl Catalog {
Ok(scored.into_iter().map(|(_, lookup)| lookup).collect())
}

/// Every entry of a listable `kind` under `locale` — one
/// [`LookupMatch`] per catalog entry of the kind, in catalog order,
/// with no query, matching, or ranking.
///
/// `Kind::Action`, `Kind::Value`, and `Kind::Event` list one
/// [`LookupMatch::Builtin`] per entry — the same match
/// [`Catalog::lookup`] builds for that entry, signature included.
/// `Kind::Enum` lists one [`LookupMatch::EnumDomain`] per declared
/// enum domain, in [`Catalog::enum_domains`] order. Enum members and
/// call parameters are scoped children that already list through
/// [`Catalog::lookup_within`]; the other kinds have no listing
/// surface, and naming one is an `Err`.
///
/// `Err` reports a locale the catalog does not declare or a `kind`
/// with no listing surface. The result is the complete listable
/// vocabulary of the kind: paging and presentation belong to the
/// consumer.
///
/// ```
/// use workshop_rs::catalog::{Catalog, Kind, Locale};
/// use workshop_rs::lookup::LookupMatch;
///
/// let catalog = Catalog::builtin().unwrap();
/// let events = catalog
/// .lookup_entries(&Locale::new("en-US"), Kind::Event)
/// .unwrap();
/// assert!(events.iter().all(
/// |m| matches!(m, LookupMatch::Builtin { kind: Kind::Event, .. })
/// ));
/// ```
pub fn lookup_entries(&self, locale: &Locale, kind: Kind) -> Result<Vec<LookupMatch>> {
self.check_locale(locale)?;
match kind {
Kind::Action | Kind::Value | Kind::Event => Ok(self
.entries_of(kind)
.map(|entry| self.builtin_match(entry, locale))
.collect()),
Kind::Enum => Ok(self
.enum_domains()
.map(|domain| self.enum_domain_match(domain, locale))
.collect()),
Kind::Structural | Kind::Operator | Kind::Setting => Err(WorkshopError::unsupported(
format!(
"listing kind '{}' is not supported: \
the listable kinds are action, value, event, and enum",
kind.as_str()
),
None,
)),
}
}

fn check_locale(&self, locale: &Locale) -> Result<()> {
if self.supports(locale) {
return Ok(());
}
Err(WorkshopError::unsupported(
format!(
"lookup for locale '{locale}' is not supported: \
the catalog does not declare it"
),
None,
))
}

fn builtin_match(&self, entry: &CatalogEntry, locale: &Locale) -> LookupMatch {
LookupMatch::Builtin {
kind: entry.kind,
id: entry.id.clone(),
display_name: entry.spelling(locale).map(String::from),
signature: signature(self, entry, locale),
}
}

fn enum_domain_match(&self, domain: &EnumDomain, locale: &Locale) -> LookupMatch {
LookupMatch::EnumDomain {
domain: domain.domain.clone(),
display_name: domain.spelling(locale).map(String::from),
members: self.enum_members(&domain.domain, locale),
}
}

fn within_enum_domain(
&self,
locale: &Locale,
Expand Down
134 changes: 134 additions & 0 deletions crates/workshop-rs/tests/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,137 @@ fn scoped_lookup_reports_an_unknown_scope() {
"unknown scope is an Unknown diagnostic: {error:?}"
);
}

#[test]
fn listing_entries_keeps_catalog_order_for_each_builtin_kind() {
let catalog = catalog();
for kind in [Kind::Action, Kind::Value, Kind::Event] {
let listed = catalog
.lookup_entries(&en(), kind)
.expect("a builtin kind is listable");
let ids: Vec<&str> = listed
.iter()
.map(|entry| match entry {
LookupMatch::Builtin {
kind: listed_kind,
id,
..
} => {
assert_eq!(*listed_kind, kind);
id.as_str()
}
other => panic!("a builtin kind lists builtins: {other:?}"),
})
.collect();
let expected: Vec<&str> = catalog
.entries_of(kind)
.map(|entry| entry.id.as_str())
.collect();
assert_eq!(ids, expected, "{kind:?} listing keeps catalog order");
let unique: std::collections::HashSet<&str> = ids.iter().copied().collect();
assert_eq!(
unique.len(),
ids.len(),
"{kind:?} listing has no duplicates"
);
}
}

#[test]
fn listing_entries_keeps_catalog_order_for_enum_domains() {
let catalog = catalog();
let listed = catalog
.lookup_entries(&en(), Kind::Enum)
.expect("enum domains are listable");
let domains: Vec<&str> = listed
.iter()
.map(|entry| match entry {
LookupMatch::EnumDomain { domain, .. } => domain.as_str(),
other => panic!("Kind::Enum lists enum domains: {other:?}"),
})
.collect();
let expected: Vec<&str> = catalog
.enum_domains()
.map(|domain| domain.domain.as_str())
.collect();
assert_eq!(domains, expected);
}

#[test]
fn listed_entries_are_the_matches_lookup_returns() {
let catalog = catalog();
for locale in [en(), zh()] {
for kind in [Kind::Action, Kind::Value, Kind::Event] {
for listed in catalog
.lookup_entries(&locale, kind)
.expect("a builtin kind is listable")
{
let LookupMatch::Builtin {
kind: listed_kind,
id,
..
} = &listed
else {
panic!("a builtin kind lists builtins: {listed:?}");
};
// A query of the exact id can return another Builtin first:
// `modifyGlobalVariable` is declared under both Structural
// and Action. Compare against the Builtin match of the same
// kind and id rather than the top-ranked match.
let from_lookup = catalog
.lookup(&locale, id)
.expect("lookup answers")
.into_iter()
.find(|entry| {
matches!(entry, LookupMatch::Builtin {
kind: found_kind,
id: found,
..
} if found == id && found_kind == listed_kind)
})
.unwrap_or_else(|| panic!("lookup finds {listed_kind:?} '{id}'"));
assert_eq!(listed, from_lookup, "{locale} {kind:?} '{id}'");
}
}
for listed in catalog
.lookup_entries(&locale, Kind::Enum)
.expect("enum domains are listable")
{
let LookupMatch::EnumDomain { domain, .. } = &listed else {
panic!("Kind::Enum lists enum domains: {listed:?}");
};
let from_lookup = catalog
.lookup(&locale, domain)
.expect("lookup answers")
.into_iter()
.find(|entry| {
matches!(entry, LookupMatch::EnumDomain { domain: found, .. } if found == domain)
})
.unwrap_or_else(|| panic!("lookup finds domain '{domain}'"));
assert_eq!(listed, from_lookup, "{locale} domain '{domain}'");
}
}
}

#[test]
fn listing_entries_rejects_undeclared_locales_and_unlistable_kinds() {
let catalog = catalog();
for kind in [Kind::Action, Kind::Value, Kind::Event, Kind::Enum] {
let error = catalog
.lookup_entries(&Locale::new("es-CL"), kind)
.expect_err("an undeclared locale is unsupported");
assert!(
matches!(error, WorkshopError::Unsupported { .. }),
"{kind:?}: {error:?}"
);
}
for kind in [Kind::Structural, Kind::Operator, Kind::Setting] {
let error = catalog
.lookup_entries(&en(), kind)
.expect_err("a kind with no listing surface is unsupported");
assert!(
matches!(error, WorkshopError::Unsupported { .. }),
"{kind:?}: {error:?}"
);
}
}
28 changes: 28 additions & 0 deletions docs/lookup.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@ the next path segment below a settings prefix (`heroes.<team>`,
naming no known scope is an explicit
`WorkshopError::Unknown` with `kind: "lookup scope"`.

## Listing entries

`Catalog::lookup_entries` enumerates the whole listable vocabulary of a
kind under `locale` — no query, matching, or ranking; the result keeps
catalog order. Each element is the same `LookupMatch` a `lookup` query
builds for that entry, signature included, so a consumer paging through
the list sees the entry a query would have returned.

```rust
use workshop_rs::catalog::{Catalog, Kind, Locale};
use workshop_rs::lookup::LookupMatch;

let catalog = Catalog::builtin()?;
let values = catalog.lookup_entries(&Locale::new("en-US"), Kind::Value)?;
assert!(values.iter().all(|m| matches!(m, LookupMatch::Builtin { kind: Kind::Value, .. })));
# Ok::<(), workshop_rs::WorkshopError>(())
```

`Kind::Action`, `Kind::Value`, and `Kind::Event` list
`LookupMatch::Builtin` matches in `entries_of` order; `Kind::Enum`
lists one `LookupMatch::EnumDomain` per declared domain in
`enum_domains` order. A domain's members and a callable's parameters
are scoped children that already list through `lookup_within`.
Structural keywords, operators, and settings have no listing surface —
naming such a kind is a `WorkshopError::Unsupported`, as is a locale
the catalog does not declare. The result is the complete vocabulary of
the kind — paging, cursors, and presentation belong to the consumer.

## Signature form

`Signature::text` renders the call syntax in the requested locale: parameters
Expand Down
Loading