-
Notifications
You must be signed in to change notification settings - Fork 145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mid-Level File API Proposal #50
Comments
So pub struct FileList { ... }
impl FileList {
// Returns `None` if `input` does not have `type="file"`?
pub fn new(input: &web_sys::InputElement) -> Option<Self> { ... }
}
impl Iterator for FileList {
type Item = gloo_file::Blob;
fn next(&mut self) -> Option<Self::Item> { ... }
} This makes sense to me. Does it make sense to also give it a method to get the ith file? I think so, but it is also an addition that can be made in a semver compatible way.
So Can we re-use the I'm not super familiar with the file API, so: as far as the file API is concerned (ignoring other API's potential
It probably makes sense for us to manually define the state machine future here, but the
At this level of abstraction API (sort of mid-to-high), I would expect that the progress events are exposed as a stream (or eventually a
Can we maybe punt on the sync file reader until after we get the normal async My biggest question, coming away from reading this proposal, is whether it makes sense to jump directly to a state machine futures API, or whether we should cut out a mid-level API between |
@fitzgen awesome feedback thanks. The web-sys wrangling for everything but the After writing code today I'm starting to think we should have that 1 to 1 API, but I would hope most people wouldn't use it. |
Yes, the code as it currently stands uses the mime crate.
So, there's not a huge difference. Files have additional metadata like a name, but that's about it (at least as far as I'm aware of). We could bring them together in one type, but I'm not sure what to call it. Calling it a
Cool crate! I will take a look
We should definitely have this. The only real question is will there be two crates (mid and high level crates) or just one high level crate?
Yep. The only interesting question here is can we enforce at compile time that these APIs only get used in web workers? |
We've more-or-less decided that we should have one crate, with submodules: |
After going through the draft/WIP PR I don't think it makes sense to have different submodules for the whole API of callbacks and futures in this case, since it is only a few methods on the Instead, I think we should just separate the read results into the two different modules, and have pub mod callbacks {
// Thin wrapper around `EventListener` to enable cancellation of reads via `drop`.
pub struct ReadAsString {
listener: gloo_events::EventListener,
}
impl ReadAsString {
pub fn forget(self) {
self.listener.forget();
}
}
}
#[cfg(feature = "futures")]
pub mod futures {
// Thin futures-based wrapper over the event listener and oneshot receiver.
pub struct ReadAsString {
receiver: oneshot::Receiver<String>,
listener: gloo_events::EventListener,
}
impl Future for ReadAsString {
...
}
}
impl FileReader {
pub fn with_read_as_string<F>(f: F) -> self::callbacks::ReadAsString
where
F: FnOnce(String)
{ ... }
#[cfg(feature = "futures")]
pub fn read_as_string() -> self::futures::ReadAsString { ... }
} Thoughts? |
It's true that it's only a few methods for the various reads, but how we handle more fine grained control (e.g., loadprogress, loadstart, etc)? Do we only want to expose these as a stream? What are the functions that return this stream called? |
@fitzgen @Pauan This proposal predates the proposal flow from #65 and as such there is already a pull request with the majority of the implementation done (#51). Do we need to modify this proposal before accepting it? I think there might be a few elements that were first agreed upon in the implementation PR and are not reflected here. |
At the very least, I think it would be good to post a sketch in here of the public API (just the types, excluding the implementation). |
Ok here's how the proposed API will look like: First we have struct FileList { ... }
impl FileList {
fn from_raw_input(input: &web_sys::HtmlInputElement) -> Option<Self> { }
fn from_raw(inner: web_sys::FileList) -> Self { }
fn get(index: usize) -> Option<File> { }
fn len(&self) -> usize { }
fn iter(&self) -> FileListIter { }
fn into_iter(self) -> FileListIntoIter { }
fn to_vec(&self) -> Vec<File> { }
} There is no Next we have a trait BlobLike {
fn size(&self) -> usize { }
#[cfg(feature = "mime")]
fn mime_type(&self) -> Result<mime::Mime, mime::FromStrError> { }
fn raw_mime_type(&self) -> String { }
fn as_raw(&self) -> &web_sys::Blob;
} There are two structs that implement this trait: #[derive(Debug, Clone)]
struct Blob { ... }
impl Blob {
fn new<T>(content: Option<T>, mime_type: Option<String>) -> Blob
where
T: std::convert::Into<BlobContents> // We'll look at BlobContents below
{ ... }
fn from_raw(inner: web_sys::Blob) -> Blob { }
fn slice(&self, start: usize, end: usize) -> Blob { }
}
#[derive(Debug, Clone)]
pub struct File { ... }
impl File {
fn new<T>(
name: String,
contents: Option<T>,
mime_type: Option<String>,
last_modified_date: Option<u64>,
) -> File
where
T: std::convert::Into<BlobContents>,
{ ... }
fn from_raw(inner: web_sys::File) -> File { }
fn name(&self) -> String { }
fn last_modified_date(&self) -> u64 { }
fn size(&self) -> u64 { }
fn slice(&self, start: usize, end: usize) -> File { }
fn as_blob(self) -> Blob { }
} Both Blob and File come with builders that allow for creating new Blobs and Files in the builder style and not having to use
#[derive(Debug, Clone)]
pub struct BlobContents {
inner: wasm_bindgen::JsValue,
} There are there conversions from types into impl std::convert::Into<BlobContents> for &str
impl std::convert::Into<BlobContents> for &[u8]
impl std::convert::Into<BlobContents> for Blob
impl std::convert::Into<BlobContents> for js_sys::ArrayBuffer Lastly there's the #[derive(Clone, Debug)]
pub struct FileReader { }
impl FileReader {
fn new() -> FileReader { }
fn read_as_string(self, blob: &impl BlobLike) -> ReadAs<String> { }
fn read_as_data_url(self, blob: &impl BlobLike) -> ReadAs<String> { }
fn read_as_array_buffer(self, blob: &impl BlobLike) -> ReadAs<js_sys::ArrayBuffer>
fn on_abort<F>(&mut self, callback: F)
where F: FnMut(AbortEvent) + 'static { }
fn on_progress<F>(&mut self, callback: F)
where F: FnMut(ProgressEvent) + 'static { }
fn on_load_start<F>(&mut self, callback: F)
where F: FnMut(LoadStartEvent) + 'static { }
fn on_load_end<F>(&mut self, callback: F)
where F: FnMut(LoadEndEvent) + 'static { }
}
pub struct ReadAs<T> { ... }
impl<T> Future for ReadAs<T> {
type Item = T;
type Error = FileReaderError;
...
}
// Make sure that dropping the Future properly aborts the reading
impl<T> std::ops::Drop for ReadAs<T> {
fn drop(&mut self) {
if self.inner.ready_state() < 2 {
self.inner.abort();
}
}
} We do not expose explicit Questions:Should we expose Should we include a Stream backed state machine API for all the different states a file reader can be in? This also has the question of what happens if the user sets explicit callbacks and uses the stream API? Does the naming all make sense? @Pauan @fitzgen I'm going to stop any work on #51 until this proposal is accepted since that's the newly agreed upon process. Let me know what you think! |
Overall this looks really great and I think we are very close :)
|
@fitzgen +1 points on I think the separation of FileReader into two implementations (futures based and callbacks based), gets us the "correctness" we want while still providing flexibility. Let's go down that route.
This I'm on sure of. I think having both would be good, but perhaps we want to stick with just one for now. I need to think this over a bit.
Me neither :-( MDN refers to |
One open question is if the However, as it currently stands, this means that the Since |
It seems to me that one can always attach progress et al listeners after the read has been kicked off (but you won't get called for earlier events, of course) so it doesn't seem like that big of a deal to not consume self. Note that if we don't consume self, then the callbacks for The other thing is that we can allow re-use for the futures methods via giving the FileReader back as an argument to either the I don't have strong opinions either way on this whole topic, so I defer to you / others. |
Thanks for your work on this, here is my thorough review:
I don't like this method. I can't quite put my finger on it, but somehow it feels not right. I guess it's because this feels like it should be a Maybe we can leave this off, since we can add it in later in a backwards compatible way?
Should this be
In addition to this method, should it also implement the Also, it needs to accept
This needs to return
I think it'd be really cool if we could somehow I think the only way we could do that is if we represented
What's the use case for this?
Isn't this just
Are we sure that the
This one is really tricky. According to the spec this should be So the correct thing to do would be to return On the other hand, I don't have any good suggestions for this one. I just know that it definitely shouldn't be
What's with the Also, rather than making
Similarly, should this be
Just like I don't have any good suggestions for this, but it also shouldn't be
Just like For the options, I think we should create an actual struct (similar to
Similarly, should this be
I'm not sure, but maybe this should be called
Same comments as
Same comments as
This should accept Actually... if we implement
Why does this accept Also, just a side note, I really like that |
That depends on if we want to allow for reading multiple files with a single Personally, I'm fine with only reading one file per In certain conditions it might mean some extra allocations, but in practice I think that should be rare: you usually want one If we're going with the "one P.S. I'm not liking this whole "do design in GitHub issues" approach. It makes it really hard to do proper reviews of the API designs (see my post above for an example of how awkward it is). Can we move to a system where we do API design in pull requests instead (like how RFCs are handled)? Maybe with an |
Yeah I'm +1 on this -- want to make a PR to |
@fitzgen @Pauan I put the latest updates in a gist - hopefully this will make it easier while we figure out an RFC process: https://gist.github.com/rylev/b4991af28b2d1b6bed769b19c1611182 I've gone with I've also split the FileReader into two modules one for callbacks and one for Futures/Streams. @fitzgen I've kept the @Pauan here's the feedback to your feedback:
|
With JS I assume that things work similarly for But even if they had higher limits, that still leaves us with only two choices:
My suggestion was for
Yup, I know that feeling. I originally proposed using traits, but we had a long discussion where they convinced me that Since in this case |
Sure thing. This implementation: impl<T> Future for ReadAs<T> { ... } Doesn't make sense as written. It will either
But the larger question is: why make a generic struct unless API consumers are going to leverage that genericism? Generics in public APIs have a learning/complexity cost, and unless we are gaining something in return, we should avoid it. |
The reason I like the generic is because it makes it really clear to the consumer what the return type will be: fn read_as_string(self, blob: &impl BlobLike) -> ReadAs<String> { }
fn read_as_data_url(self, blob: &impl BlobLike) -> ReadAs<String> { }
fn read_as_array_buffer(self, blob: &impl BlobLike) -> ReadAs<js_sys::ArrayBuffer> { } fn read_as_string(self, blob: &impl BlobLike) -> ReadAsString { }
fn read_as_data_url(self, blob: &impl BlobLike) -> ReadAsString { }
fn read_as_array_buffer(self, blob: &impl BlobLike) -> ReadAsArrayBuffer { } The first seems a lot clearer to me, since it explicitly tells you what the type will be. It's similar to something like The second is more opaque, requiring you to actually look at the docs for And no, I don't think it's correct to assume that I'm not going to block on that though, since the |
Addresses #47 in part by proposing a mid-level API. The possibility for a higher level API is left open.
File API
The mid-level file API aims to implement File I/O on top of the raw JS apis found here: https://w3c.github.io/FileAPI/ which includes listing files, creating files, and reading files.
General Differences
In general this API will mostly differ from the
web_sys
interface in the following ways:No js_sys Types
js_sys tends to be fairly "stringly" typed, so where we have known enums, the crate will provide bespoke types.
This is also true for errors which are usually exposed as JsValues. In this crate they will be exposed as well-formed Rust types.
Callbacks
All callbacks will be of the appropriate closure type (e.g., FnOnce).
The API
FileList
JS provides a basic collection type over files (usually from an tag). In general
FileList
will be an opaque struct that offers a rich interface to the collection through anIterator
implementation.Blob
Blob
is a "file like" interface. The crate would expose this as a Rust interface from which the specific typesFile
andRawData
inherit. These types would be roughly 1 to 1 translations of theFile
andBlob
objects just with less "stringly" typed interfaces. For example,RawData
would accept a known list of mime-typesFileReader
The FileReader API is a bit wonky and can easily be used in the incorrect way. For instance, if the user calls
readAsArrayBuffer
or other "read" APIs before the data is loaded, the operation won't return data. The crate will expose an API that more properly models the "read" state machine.This will be done through a futures API which takes the user through the state machine:
The various "read" methods will take ownership of
self
so that users can't try to read multiple files at once. These methods return anInProgressRead
struct that is a future and also contains anabort
method for aborting the upload.The events "error", "load", and "loadend" are all representable through the future and thus aren't exposed on the file reader. The events "progress", "loadstart", and "abort" are still exposed as methods that take the appropriate Rust closure.
FileReaderSync
This API would mirror the future based API just in a different namespace.
Drawbacks, Rationale, and Alternatives
FileReader exposes a very different API from its JS counterpart. We could do a more 1 to 1 translation of the API and leave the improved API for a higher-level crate. This would make this crate only a very marginal improvement on what is already exposed in
js_sys
.Unresolved Questions
Is there an ergonomic way to expose FileReader "progress", "loadstart", and "abort" events as part of the future API?
Is there a better way to model the relationship between
Blob
andFile
? Possibilities include an enum and aFile
type which contains aBlob
type.The text was updated successfully, but these errors were encountered: