Conversation
cmyr
left a comment
There was a problem hiding this comment.
so... lots of notes inline, but I think the general shape of this is okay?
- going through this does make me wonder if the closure-based version might not be preferable, since it's easier to get right without needing a lot of back-and-forth?
- In either case I think I'd like to see all of this new code live in a new module, like
non_file_loading.rsor similar, just so that we can keep it a bit more contained.
| /// Returns a [`DesignSpaceDocument`] loaded from an XML string. | ||
| pub fn load_str(contents: &str) -> Result<DesignSpaceDocument, DesignSpaceLoadError> { | ||
| quick_xml::de::from_str(contents).map_err(DesignSpaceLoadError::DeError) | ||
| } |
There was a problem hiding this comment.
total nit but I would prefer this to be right below the main load fn, above.
| /// The requested path. | ||
| path: PathBuf, | ||
| /// The underlying error. | ||
| source: IoError, |
There was a problem hiding this comment.
I don't think io::Error is the right error type, here; the error type on SourceResolver also isn't right, see below.
| /// Return UTF-8 contents for the provided path. | ||
| /// | ||
| /// Returning `Ok(None)` indicates a missing file. | ||
| fn get_contents(&self, path: &Path) -> Result<Option<String>, FontLoadError>; |
There was a problem hiding this comment.
What we want here is some very customizable error type that the implementor can use to communicate why the given resource isn't available; FontLoadError covers a lot of specific cases that aren't relevant to the job of "give me a string for this path".
The 'best' option is probably an associated type, so this would look like:
trait SourceResolver {
type Error;
fn get_contents(&self, path: &Path) -> Result<Option<String>, Self::Error>;
}but this might be a bit fiddly if you're not that comfortable with rust? Because we would probably need bounds on the error type elsewhere, so that the top-level Resolver variant would contain a Box<dyn std::error::Error>...
| /// Resolve a raw request path. | ||
| fn resolve_raw_path(&self, path: &Path, _requested_from: Option<&Path>) -> PathBuf { | ||
| path.to_path_buf() | ||
| } | ||
|
|
||
| /// Canonicalize a path if needed. | ||
| fn canonicalize(&self, path: &Path) -> Result<PathBuf, FontLoadError> { | ||
| Ok(path.to_path_buf()) | ||
| } |
There was a problem hiding this comment.
both of these methods are important for resolving include statements in a FEA file but are (I believe) irrelevant for your use-case, and can be removed.
| /// Filesystem-backed [`SourceResolver`]. | ||
| #[derive(Default)] | ||
| pub struct FileSystemResolver { | ||
| project_root: PathBuf, | ||
| } | ||
|
|
||
| impl FileSystemResolver { | ||
| /// Create a resolver rooted at `project_root`. | ||
| pub fn new(project_root: PathBuf) -> Self { | ||
| Self { project_root } | ||
| } | ||
| } | ||
|
|
||
| impl SourceResolver for FileSystemResolver { | ||
| fn get_contents(&self, path: &Path) -> Result<Option<String>, FontLoadError> { | ||
| match fs::read_to_string(path) { | ||
| Ok(contents) => Ok(Some(contents)), | ||
| Err(source) if source.kind() == ErrorKind::NotFound => Ok(None), | ||
| Err(source) => Err(FontLoadError::ResolverIo { path: path.to_path_buf(), source }), | ||
| } | ||
| } | ||
|
|
||
| fn resolve_raw_path(&self, path: &Path, requested_from: Option<&Path>) -> PathBuf { | ||
| if path.is_absolute() { | ||
| return path.to_path_buf(); | ||
| } | ||
| if let Some(parent) = requested_from.and_then(Path::parent) { | ||
| return parent.join(path); | ||
| } | ||
| self.project_root.join(path) | ||
| } | ||
|
|
||
| fn canonicalize(&self, path: &Path) -> Result<PathBuf, FontLoadError> { | ||
| Ok(path.to_path_buf()) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
do you even need a file-system backed resolver? this is used in fea-rs because the resolver API is the only API for handling include statements, but norad already has an API for loading from the file system...
| fn load_with_resolver_impl( | ||
| request: DataRequest, | ||
| resolver: &dyn SourceResolver, | ||
| ) -> Result<Font, FontLoadError> { |
There was a problem hiding this comment.
have a separate _impl method that we delegate too is unnecessary here (and is actually unnecessary in our main load fn too now, I believe it's a relic from an earlier time)
| request: DataRequest, | ||
| resolver: &dyn SourceResolver, | ||
| ) -> Result<Font, FontLoadError> { | ||
| let metainfo_str = required_file(resolver, Path::new(METAINFO_FILE), None)?; |
There was a problem hiding this comment.
this required_file method is only called once, i'd just inline that bit?
| } | ||
|
|
||
| let mut lib = if request.lib { | ||
| match optional_file(resolver, Path::new(LIB_FILE), None)? { |
There was a problem hiding this comment.
I think optional_file can also just be removed, we don't need to worry about canonicalization etc so should just call resolver.get_contents directly?
|
|
||
| let mut lib = if request.lib { | ||
| match optional_file(resolver, Path::new(LIB_FILE), None)? { | ||
| Some(lib_str) => plist::Value::from_reader(Cursor::new(lib_str.as_bytes())) |
There was a problem hiding this comment.
you shouldn't need Cursor here, just Value::from_reader_xml(lib_str.as_bytes())?
|
|
||
| let layers = load_layer_set_from_resolver(resolver, &request.layers)?; | ||
|
|
||
| let (groups, kerning) = (groups, kerning); |
|
I'm addressing all of your remarks in a new branch. Before making a PR, see diffs here: https://github.com/linebender/norad/compare/master...yanone:norad:non-file-io-save-and-includes?expand=1 This branch is a follow-up over both #387 and #390. Compared to #387, it keeps the non-file use case but drops the HashMap-specific API shape in favor of closure-based source/sink callbacks over UFO-root-relative paths. Compared to #390, it removes the public resolver/filesystem-resolver abstraction and moves the non-file code into a separate module. It also generalizes the error path away from io::Error/resolver-specific types. In addition, it goes beyond both earlier PRs in two ways that matter for my editor use case:
So the current branch is basically: closure-based non-file load/save, isolated implementation, Please review. If you're generally happy with this, I'll make a PR and then we can discuss details. |
|
yan please just open the new PR and I can review there :) |
Summary
This PR introduces a path-based source abstraction for UFO loading so norad can load from non-filesystem backends without exposing a HashMap-specific API.
Motivation
Downstream wasm and in-memory workflows need to provide UFO files from virtual storage.
The goal is to keep norad’s public API backend-agnostic while still supporting those use cases cleanly.
What changed
Added a source resolver abstraction for reading UFO resources by relative path.
Added a filesystem resolver implementation for current disk-based behavior.
Added resolver-based font loading flow, used by existing load entry points.
Added resolver-specific error handling for missing files and IO failures.
Kept default filesystem behavior intact for existing callers.
Compatibility
Existing filesystem loading behavior is preserved.
No HashMap-specific type is introduced in the public API.
This creates a clear extension point for custom loaders in downstream crates.