Skip to content

Commit

Permalink
Add reservable_writer and sized_reader concepts and documentation on …
Browse files Browse the repository at this point in the history
…readers.
  • Loading branch information
asoffer committed Sep 7, 2024
1 parent 6571994 commit 1c14725
Show file tree
Hide file tree
Showing 19 changed files with 258 additions and 9 deletions.
24 changes: 24 additions & 0 deletions docs/io/reader/file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# `//nth/io/reader:file`

## Overview

This target defines `nth::io::file_reader`, a type conforming to the
[nth::io::reader](/io/reader/reader) concept, which enables users to read data to a file. The
type can be constructed with the static member function `try_open`, by passing it a
[`nth::io::file_path`](/io/file_path). `try_open` will return `std::nullopt` if the file does not
exist.

## Example usage

```
// Reads at most 1024 bytes from the file and returns the contents as a
// `std::string`.
std::optional<std::string> ReadMessage(nth::io::file_path const & path) {
std::optional r = nth::io::file_reader::try_construct(path);
if (not r) { return std::nullopt; }
char buffer[1024];
auto result = nth::io::read_text(r, buffer);
return std::string(buffer, result.bytes_read());
}
```
71 changes: 71 additions & 0 deletions docs/io/reader/reader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# `//nth/io/reader`

## Overview

This target defines concepts related to writing data in a serialized format. In particular, it
defines the `nth::io::reader` and `nth::io::read_result_type` concepts.

As readers abstract the reading of both data (to be serialized) and text (to be parsed). One might
expect a reader to accept a `std::span<char>` that data can be read into, but this is fundamentally
the wrong type for such a generic interface. Instead, a reader traffics in `std::span<std::byte>`.
To address this common need, this header also provides a `read_text` free function, accepting a
`reader` reference and a `std::span<char>`, which converts the `std::span<char>` to a span of bytes.
Alternatively one may produce a `std::span<std::byte>` Via one of the functions in the
[memory/bytes](/memory/bytes) header.

Because reading data may not succeed, we must also represent the notion of a "read result." This is
formalized via the `nth::io::read_result_type` concept.

## `reader`

Formally, `reader` is a concept requiring the type `R` to contain a `read` member function that can
be invoked with a `std::span<std::byte>`. The function is responsible for writing data into this
span of bytes in accordance with how the type `R` defines "read." The function must return a
`nth::io::read_result<R>` indicating how many bytes were read. For an argument `byte_span`
producing a returned result of `result`,

* `result.bytes_read()` must be greater than or equal to zero,
* `result.bytes_read()` must be less than or equal to `byte_span.size()`.

## `sized_reader`

A `sized_reader` is a reader that also provides a `size` member function, The function
must return the number of bytes left to be read. (Streams for which this number is not computable
should be represented by `reader`s but not by `sized_reader`s).

## `read_result_type`

The type returned by a call to `read` on a reader must be something adhering to
`nth::io::read_result_type`. Types adhere to this concept provided they have a `bytes_read` const
member function taking no arguments and returning some integral type. The returned value represents
the number of bytes consumed by a call to `read`.

## Read result customization.

By default, a `reader`'s associated read result type is `nth::io::basic_read_result`. This is a
minimal, type adhering to the `read_result_type` concept, outlined here:

```
struct basic_read_result {
explicit constexpr basic_read_result(size_t n);
[[nodiscard]] constexpr size_t written() const;
private:
// ...
};
```

Type authors may customize their associated read result type by providing a nested type name
`nth_read_result_type`. This type must be adhere to the `read_result_type` concept and will be
used as the associated read result type.

One can query the read result for a reader type `R` via `nth::io::read_result<R>`.

## Examples

There are two builtin `nth::io::readers` of note:

* [nth::io::string_reader](/io/reader/string), for which calls to `read` append data to a
`std::string`, and
* [nth::io::file_reader](/io/reader/file), for which calls to `read` read the data to a
file.
27 changes: 27 additions & 0 deletions docs/io/reader/string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# `//nth/io/reader:string`

## Overview

This target defines `nth::io::string_reader`, a type conforming to the
[nth::io::sized_reader](/io/reader/reader) concept, which enables users to read data to a
string. The type can be constructed with a mutable reference to a `std::string`. The referenced
string must outlive the `string_reader`. Calls to `read` will append to the referenced string.

## Example usage

```
std::string s = "Hello, world!";
nth::io::string_reader r(s);
char buffer[7] = {0};
auto result1 = nth::io::read_text(r, buffer);
NTH_EXPECT(result1.bytes_read() == 7);
NTH_EXPECT(std::string_view(buffer) == "Hello, ");
auto result2 = nth::io::read_text(r, buffer);
NTH_EXPECT(result2.bytes_read() == 6);
// Note the trailing space, as the last byte was not modified, and retaines its
// previous value from when the buffer held "Hello, ".
NTH_EXPECT(std::string_view(buffer) == "world! ");
```
2 changes: 1 addition & 1 deletion docs/io/writer/file.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ bool WriteMessage(nth::io::file_path const & path) {
if (not w) { return false; }
std::string_view message = "hello,";
auto result = nth::io::write_text(w, message);
auto result = nth::io::write_text(*w, message);
return result.written() == message.size();
}
```
6 changes: 3 additions & 3 deletions docs/io/writer/string.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
## Overview

This target defines `nth::io::string_writer`, a type conforming to the
[nth::io::writer](/io/writer/writer) concept, which enables users to write data to a string. The
type can be constructed with a mutable reference to a `std::string`. The referenced string must
outlive the `string_writer`. Calls to `write` will append to the referenced string.
[nth::io::reservable_writer](/io/writer/writer) concept, which enables users to write data to a
string. The type can be constructed with a mutable reference to a `std::string`. The referenced
string must outlive the `string_writer`. Calls to `write` will append to the referenced string.

## Example usage

Expand Down
14 changes: 12 additions & 2 deletions docs/io/writer/writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ via [`nth::bytes`](/memory/bytes), or view a sequence of bytes represented by an
Because writing data may not succeed, we must also represent the notion of a "write result." This is
formalized via the `nth::io::write_result_type` concept.

## The `writer` concept
## `writer`

Formally, `writer` is a concept requiring the type `W` to contain a `write` member function that can
be invoked with a `std::span<std::byte const>`. The function is responsible for writing the span of
Expand All @@ -31,7 +31,16 @@ producing a returned result of `result`,
* Callers may interpret `result.written() == byte_span.size()` as a successful write,
`result.written() == 0` as a failure, and all other possibilities as partial success.

## The `write_result_type` concept
## `reservable_writer`

A `reservable_writer` is a writer that also provides a `reserve` member function. The function
accepts a `size_t` and returns a `std::span<std::byte>` of that size, which the user is responsible
for filling with their desired content. Writes to this buffer must be completed before the next
modification of the writer. The behavior of `reserve` must be identical to that of filling an
external buffer of the same size and then calling `write` on that buffer. The reserve call often
provides an opportunity to write directly thereby avoiding an extra memory copy.

## `write_result_type`

The type returned by a call to `write` on a writer must be something adhering to
`nth::io::write_result_type`. Types adhere to this concept provided they have a `written` const
Expand All @@ -47,6 +56,7 @@ minimal, type adhering to the `write_result_type` concept, outlined here:
struct basic_write_result {
explicit constexpr basic_write_result(size_t n);
[[nodiscard]] constexpr size_t written() const;
private:
// ...
};
Expand Down
4 changes: 4 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ nav:
- interpolate: format/interpolate.md
#- hash: hash.md
- io:
- reader:
- reader: io/reader/reader.md
- file: io/reader/file.md
- string: io/reader/string.md
- writer:
- writer: io/writer/writer.md
- file: io/writer/file.md
Expand Down
11 changes: 10 additions & 1 deletion nth/io/reader/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace nth::io {

std::optional<file_reader> file_reader::try_open(file_path const& f) {
std::optional<file_reader> reader;
std::FILE* file = std::fopen(f.path().c_str(), "r");
std::FILE* file = std::fopen(f.path().c_str(), "rb");
if (file) {
reader = file_reader();
reader->file_ = file;
Expand All @@ -19,4 +19,13 @@ basic_read_result file_reader::read(std::span<std::byte> buffer) {
return basic_read_result(std::fread(buffer.data(), 1, buffer.size(), file_));
}

size_t file_reader::bytes_remaining() const {
long pos = std::ftell(file_);
if (std::fseek(file_, 0, SEEK_END) != 0) { std::abort(); }
long end = std::ftell(file_);
size_t result = end - pos;
if (std::fseek(file_, pos, SEEK_SET) != 0) { std::abort(); }
return result;
}

} // namespace nth::io
2 changes: 2 additions & 0 deletions nth/io/reader/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct file_reader {

basic_read_result read(std::span<std::byte> buffer);

size_t bytes_remaining() const;

private:
file_reader() = default;
std::FILE* file_;
Expand Down
17 changes: 17 additions & 0 deletions nth/io/reader/file_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,22 @@ NTH_TEST("/nth/io/reader/file/read") {
NTH_EXPECT(std::string_view(buffer, 13) == "Hello, world!");
}

NTH_TEST("/nth/io/reader/file/bytes_remaining") {
char buffer[5] = {0};
std::optional f =
file_path::try_construct("/tmp/nth_io_file_reader_test.txt");
NTH_ASSERT(f.has_value());
NTH_ASSERT(write_file(*f));
std::optional r = file_reader::try_open(*f);
NTH_ASSERT(r.has_value());
NTH_EXPECT(r->bytes_remaining() == 13u);
NTH_ASSERT(read_text(*r, buffer).bytes_read() == 5u);
NTH_EXPECT(r->bytes_remaining() == 8u);
NTH_ASSERT(read_text(*r, buffer).bytes_read() == 5u);
NTH_EXPECT(r->bytes_remaining() == 3u);
NTH_ASSERT(read_text(*r, buffer).bytes_read() == 3u);
NTH_EXPECT(r->bytes_remaining() == 0u);
}

} // namespace
} // namespace nth::io
7 changes: 7 additions & 0 deletions nth/io/reader/reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ concept reader = requires(R r) {
} -> nth::precisely<read_result<R>>;
};

// A `sized_reader` is a reader for which the size in bytes of the content being
// read is computable via a `bytes_remaining` member function.
template <typename R>
concept sized_reader = reader<R> and requires(R r) {
{ r.bytes_remaining() } -> nth::precisely<size_t>;
};

template <reader R>
read_result<R> read(R& r, std::span<std::byte const> bytes) {
return r.read(bytes);
Expand Down
5 changes: 4 additions & 1 deletion nth/io/reader/string.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

namespace nth::io {

// Reads data from the `std::string_view` passed into the constructor of this `reader`.
// Reads data from the `std::string_view` passed into the constructor of this
// `reader`.
struct string_reader {
explicit string_reader(std::string_view s) : s_(s) {}

basic_read_result read(std::span<std::byte> buffer);

size_t bytes_remaining() const { return s_.size(); }

private:
std::string_view s_;
};
Expand Down
13 changes: 13 additions & 0 deletions nth/io/reader/string_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,18 @@ NTH_TEST("string_reader/read") {
NTH_EXPECT(std::string_view(buffer, 4) == "ebcd");
}

NTH_TEST("string_reader/bytes_remaining") {
std::string s = "Hello, world!";
string_reader r(s);
char buffer[5] = {0};
NTH_EXPECT(r.bytes_remaining() == 13u);
NTH_ASSERT(read_text(r, buffer).bytes_read() == 5u);
NTH_EXPECT(r.bytes_remaining() == 8u);
NTH_ASSERT(read_text(r, buffer).bytes_read() == 5u);
NTH_EXPECT(r.bytes_remaining() == 3u);
NTH_ASSERT(read_text(r, buffer).bytes_read() == 3u);
NTH_EXPECT(r.bytes_remaining() == 0u);
}

} // namespace
} // namespace nth::io
2 changes: 2 additions & 0 deletions nth/io/writer/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ cc_library(
name = "writer",
hdrs = ["writer.h"],
deps = [
"//nth/io/reader",
"//nth/meta/concepts:convertible",
"//nth/meta/concepts:core",
],
)
Expand Down
2 changes: 1 addition & 1 deletion nth/io/writer/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace nth::io {

std::optional<file_writer> file_writer::try_open(file_path const& f) {
std::FILE* ptr = std::fopen(f.path().c_str(), "w");
std::FILE* ptr = std::fopen(f.path().c_str(), "wb");
if (not ptr) { return std::nullopt; }
return std::optional<file_writer>(file_writer(ptr));
}
Expand Down
6 changes: 6 additions & 0 deletions nth/io/writer/string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ basic_write_result string_writer::write(std::span<std::byte const> data) {
return basic_write_result(data.size());
}

std::span<std::byte> string_writer::reserve(size_t n) {
size_t size = s_.size();
s_.resize(s_.size() + n);
return std::span(reinterpret_cast<std::byte*>(s_.data()) + size, n);
}

} // namespace nth::io
2 changes: 2 additions & 0 deletions nth/io/writer/string.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ struct string_writer {

basic_write_result write(std::span<std::byte const> data);

std::span<std::byte> reserve(size_t n);

private:
std::string& s_;
};
Expand Down
12 changes: 12 additions & 0 deletions nth/io/writer/string_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,17 @@ NTH_TEST("string_writer/write") {
NTH_EXPECT(s == "abcdefgh");
}

NTH_TEST("string_writer/reserve") {
std::string s;
string_writer w(s);
NTH_ASSERT(write_text(w, "abcd").written() == 4u);
std::span chars = w.reserve(13);
std::memcpy(chars.data(), "Hello, world!", 13);
NTH_EXPECT(s.size() == 17u);
NTH_EXPECT(write_text(w, "efgh").written() == 4u);
NTH_EXPECT(s.size() == 21u);
NTH_EXPECT(s == "abcdHello, world!efgh");
}

} // namespace
} // namespace nth::io
Loading

0 comments on commit 1c14725

Please sign in to comment.