1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 06:47:35 +00:00

Spreadsheet: Port XSV writer to Core::Stream

This commit is contained in:
Karol Kosek 2023-01-02 11:05:52 +01:00 committed by Ali Mohammad Pur
parent 5793f7749c
commit 3a8450ae11
5 changed files with 74 additions and 120 deletions

View file

@ -10,10 +10,9 @@
#include <AK/DeprecatedString.h> #include <AK/DeprecatedString.h>
#include <AK/JsonArray.h> #include <AK/JsonArray.h>
#include <AK/LexicalPath.h> #include <AK/LexicalPath.h>
#include <AK/MemoryStream.h>
#include <Applications/Spreadsheet/CSVExportGML.h> #include <Applications/Spreadsheet/CSVExportGML.h>
#include <LibCore/File.h>
#include <LibCore/FileStream.h> #include <LibCore/FileStream.h>
#include <LibCore/MemoryStream.h>
#include <LibCore/StandardPaths.h> #include <LibCore/StandardPaths.h>
#include <LibGUI/Application.h> #include <LibGUI/Application.h>
#include <LibGUI/CheckBox.h> #include <LibGUI/CheckBox.h>
@ -90,7 +89,7 @@ CSVExportDialogPage::CSVExportDialogPage(Sheet const& sheet)
update_preview(); update_preview();
} }
auto CSVExportDialogPage::make_writer(OutputStream& stream) -> ErrorOr<XSV> auto CSVExportDialogPage::make_writer(Core::Stream::Handle<Core::Stream::Stream> stream) -> ErrorOr<NonnullOwnPtr<XSV>>
{ {
auto delimiter = TRY([this]() -> ErrorOr<DeprecatedString> { auto delimiter = TRY([this]() -> ErrorOr<DeprecatedString> {
if (m_delimiter_other_radio->is_checked()) { if (m_delimiter_other_radio->is_checked()) {
@ -152,23 +151,22 @@ auto CSVExportDialogPage::make_writer(OutputStream& stream) -> ErrorOr<XSV>
if (should_quote_all_fields) if (should_quote_all_fields)
behaviors = behaviors | Writer::WriterBehavior::QuoteAll; behaviors = behaviors | Writer::WriterBehavior::QuoteAll;
return XSV(stream, m_data, move(traits), *headers, behaviors); return try_make<XSV>(move(stream), m_data, move(traits), *headers, behaviors);
} }
void CSVExportDialogPage::update_preview() void CSVExportDialogPage::update_preview()
{ {
DuplexMemoryStream memory_stream; auto maybe_error = [this]() -> ErrorOr<void> {
auto writer_or_error = make_writer(memory_stream); Core::Stream::AllocatingMemoryStream memory_stream;
if (!writer_or_error.is_error()) { auto writer = TRY(make_writer(Core::Stream::Handle<Core::Stream::Stream>(memory_stream)));
m_data_preview_text_editor->set_text(DeprecatedString::formatted("Cannot update preview: {}", writer_or_error.error())); TRY(writer->generate_preview());
return; auto buffer = TRY(memory_stream.read_until_eof());
} m_data_preview_text_editor->set_text(StringView(buffer));
auto writer = writer_or_error.release_value(); m_data_preview_text_editor->update();
return {};
writer.generate_preview(); }();
auto buffer = memory_stream.copy_into_contiguous_buffer(); if (maybe_error.is_error())
m_data_preview_text_editor->set_text(StringView(buffer)); m_data_preview_text_editor->set_text(DeprecatedString::formatted("Cannot update preview: {}", maybe_error.error()));
m_data_preview_text_editor->update();
} }
ErrorOr<void> ExportDialog::make_and_run_for(StringView mime, Core::File& file, Workbook& workbook) ErrorOr<void> ExportDialog::make_and_run_for(StringView mime, Core::File& file, Workbook& workbook)
@ -188,12 +186,9 @@ ErrorOr<void> ExportDialog::make_and_run_for(StringView mime, Core::File& file,
if (wizard->exec() != GUI::Dialog::ExecResult::OK) if (wizard->exec() != GUI::Dialog::ExecResult::OK)
return Error::from_string_literal("CSV Export was cancelled"); return Error::from_string_literal("CSV Export was cancelled");
auto file_stream = Core::OutputFileStream(file); auto file_stream = TRY(try_make<Core::Stream::WrappedAKOutputStream>(TRY(try_make<Core::OutputFileStream>(file))));
auto writer = TRY(page.make_writer(file_stream)); auto writer = TRY(page.make_writer(move(file_stream)));
writer.generate(); return writer->generate();
if (writer.has_error())
return Error::from_string_literal("CSV Export failed");
return {};
}; };
auto export_worksheet = [&]() -> ErrorOr<void> { auto export_worksheet = [&]() -> ErrorOr<void> {

View file

@ -23,7 +23,7 @@ struct CSVExportDialogPage {
explicit CSVExportDialogPage(Sheet const&); explicit CSVExportDialogPage(Sheet const&);
NonnullRefPtr<GUI::WizardPage> page() { return *m_page; } NonnullRefPtr<GUI::WizardPage> page() { return *m_page; }
ErrorOr<XSV> make_writer(OutputStream&); ErrorOr<NonnullOwnPtr<XSV>> make_writer(Core::Stream::Handle<Core::Stream::Stream>);
protected: protected:
void update_preview(); void update_preview();

View file

@ -15,8 +15,8 @@ namespace Writer {
template<typename ContainerType> template<typename ContainerType>
class CSV : public XSV<ContainerType> { class CSV : public XSV<ContainerType> {
public: public:
CSV(OutputStream& output, ContainerType const& data, Vector<StringView> const& headers = {}, WriterBehavior behaviors = default_behaviors()) CSV(Core::Stream::Handle<Core::Stream::Stream> output, ContainerType const& data, Vector<StringView> headers = {}, WriterBehavior behaviors = default_behaviors())
: XSV<ContainerType>(output, data, { ",", "\"", WriterTraits::Repeat }, headers, behaviors) : XSV<ContainerType>(move(output), data, { ",", "\"", WriterTraits::Repeat }, move(headers), behaviors)
{ {
} }
}; };

View file

@ -8,7 +8,7 @@
#include "../CSV.h" #include "../CSV.h"
#include "../XSV.h" #include "../XSV.h"
#include <AK/MemoryStream.h> #include <LibCore/MemoryStream.h>
TEST_CASE(can_write) TEST_CASE(can_write)
{ {
@ -18,17 +18,17 @@ TEST_CASE(can_write)
{ 7, 8, 9 }, { 7, 8, 9 },
}; };
auto buffer = ByteBuffer::create_uninitialized(1024).release_value(); Core::Stream::AllocatingMemoryStream stream;
OutputMemoryStream stream { buffer }; auto csv = Writer::CSV(Core::Stream::Handle<Core::Stream::Stream>(stream), data);
MUST(csv.generate());
Writer::CSV csv(stream, data);
auto expected_output = R"~(1,2,3 auto expected_output = R"~(1,2,3
4,5,6 4,5,6
7,8,9 7,8,9
)~"; )~"sv;
EXPECT_EQ(StringView { stream.bytes() }, expected_output); auto buffer = MUST(stream.read_until_eof());
EXPECT_EQ(StringView { buffer.bytes() }, expected_output);
} }
TEST_CASE(can_write_with_header) TEST_CASE(can_write_with_header)
@ -39,18 +39,18 @@ TEST_CASE(can_write_with_header)
{ 7, 8, 9 }, { 7, 8, 9 },
}; };
auto buffer = ByteBuffer::create_uninitialized(1024).release_value(); Core::Stream::AllocatingMemoryStream stream;
OutputMemoryStream stream { buffer }; auto csv = Writer::CSV(Core::Stream::Handle<Core::Stream::Stream>(stream), data, { "A"sv, "B\""sv, "C"sv });
MUST(csv.generate());
Writer::CSV csv(stream, data, { "A"sv, "B\""sv, "C"sv });
auto expected_output = R"~(A,"B""",C auto expected_output = R"~(A,"B""",C
1,2,3 1,2,3
4,5,6 4,5,6
7,8,9 7,8,9
)~"; )~"sv;
EXPECT_EQ(StringView { stream.bytes() }, expected_output); auto buffer = MUST(stream.read_until_eof());
EXPECT_EQ(StringView { buffer.bytes() }, expected_output);
} }
TEST_CASE(can_write_with_different_behaviors) TEST_CASE(can_write_with_different_behaviors)
@ -60,15 +60,15 @@ TEST_CASE(can_write_with_different_behaviors)
{ "We\"ll", "Hello,", " Friends" }, { "We\"ll", "Hello,", " Friends" },
}; };
auto buffer = ByteBuffer::create_uninitialized(1024).release_value(); Core::Stream::AllocatingMemoryStream stream;
OutputMemoryStream stream { buffer }; auto csv = Writer::CSV(Core::Stream::Handle<Core::Stream::Stream>(stream), data, { "A"sv, "B\""sv, "C"sv }, Writer::WriterBehavior::QuoteOnlyInFieldStart | Writer::WriterBehavior::WriteHeaders);
MUST(csv.generate());
Writer::CSV csv(stream, data, { "A"sv, "B\""sv, "C"sv }, Writer::WriterBehavior::QuoteOnlyInFieldStart | Writer::WriterBehavior::WriteHeaders);
auto expected_output = R"~(A,B",C auto expected_output = R"~(A,B",C
Well,Hello",Friends Well,Hello",Friends
We"ll,"Hello,", Friends We"ll,"Hello,", Friends
)~"; )~"sv;
EXPECT_EQ(StringView { stream.bytes() }, expected_output); auto buffer = MUST(stream.read_until_eof());
EXPECT_EQ(StringView { buffer.bytes() }, expected_output);
} }

View file

@ -9,10 +9,10 @@
#include <AK/DeprecatedString.h> #include <AK/DeprecatedString.h>
#include <AK/GenericLexer.h> #include <AK/GenericLexer.h>
#include <AK/OwnPtr.h> #include <AK/OwnPtr.h>
#include <AK/Stream.h>
#include <AK/StringView.h> #include <AK/StringView.h>
#include <AK/Types.h> #include <AK/Types.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibCore/Stream.h>
namespace Writer { namespace Writer {
@ -34,17 +34,6 @@ struct WriterTraits {
} quote_escape { Repeat }; } quote_escape { Repeat };
}; };
#define ENUMERATE_WRITE_ERRORS() \
E(None, "No errors") \
E(NonConformingColumnCount, "Header count does not match given column count") \
E(InternalError, "Internal error")
enum class WriteError {
#define E(name, _) name,
ENUMERATE_WRITE_ERRORS()
#undef E
};
constexpr WriterBehavior default_behaviors() constexpr WriterBehavior default_behaviors()
{ {
return WriterBehavior::None; return WriterBehavior::None;
@ -53,12 +42,12 @@ constexpr WriterBehavior default_behaviors()
template<typename ContainerType, typename HeaderType = Vector<StringView>> template<typename ContainerType, typename HeaderType = Vector<StringView>>
class XSV { class XSV {
public: public:
XSV(OutputStream& output, ContainerType const& data, WriterTraits traits, HeaderType const& headers = {}, WriterBehavior behaviors = default_behaviors()) XSV(Core::Stream::Handle<Core::Stream::Stream> output, ContainerType const& data, WriterTraits traits, HeaderType headers = {}, WriterBehavior behaviors = default_behaviors())
: m_data(data) : m_data(data)
, m_traits(move(traits)) , m_traits(move(traits))
, m_behaviors(behaviors) , m_behaviors(behaviors)
, m_names(headers) , m_names(headers)
, m_output(output) , m_output(move(output))
{ {
if (!headers.is_empty()) if (!headers.is_empty())
m_behaviors = m_behaviors | WriterBehavior::WriteHeaders; m_behaviors = m_behaviors | WriterBehavior::WriteHeaders;
@ -66,94 +55,71 @@ public:
virtual ~XSV() = default; virtual ~XSV() = default;
bool has_error() const { return m_error != WriteError::None; } ErrorOr<void> generate()
WriteError error() const { return m_error; }
DeprecatedString error_string() const
{
switch (m_error) {
#define E(x, y) \
case WriteError::x: \
return y;
ENUMERATE_WRITE_ERRORS();
#undef E
}
VERIFY_NOT_REACHED();
}
void generate()
{ {
auto with_headers = has_flag(m_behaviors, WriterBehavior::WriteHeaders); auto with_headers = has_flag(m_behaviors, WriterBehavior::WriteHeaders);
if (with_headers) { if (with_headers) {
write_row(m_names); TRY(write_row(m_names));
if (m_output.write({ "\n", 1 }) != 1) TRY(m_output->write_entire_buffer({ "\n", 1 }));
set_error(WriteError::InternalError);
} }
for (auto&& row : m_data) { for (auto&& row : m_data) {
if (with_headers) { if (with_headers) {
if (row.size() != m_names.size()) if (row.size() != m_names.size())
set_error(WriteError::NonConformingColumnCount); return Error::from_string_literal("Header count does not match given column count");
} }
write_row(row); TRY(write_row(row));
if (m_output.write({ "\n", 1 }) != 1) TRY(m_output->write_entire_buffer({ "\n", 1 }));
set_error(WriteError::InternalError);
} }
return {};
} }
void generate_preview() ErrorOr<void> generate_preview()
{ {
auto lines_written = 0; auto lines_written = 0;
constexpr auto max_preview_lines = 8; constexpr auto max_preview_lines = 8;
auto with_headers = has_flag(m_behaviors, WriterBehavior::WriteHeaders); auto with_headers = has_flag(m_behaviors, WriterBehavior::WriteHeaders);
if (with_headers) { if (with_headers) {
write_row(m_names); TRY(write_row(m_names));
if (m_output.write({ "\n", 1 }) != 1) TRY(m_output->write_entire_buffer({ "\n", 1 }));
set_error(WriteError::InternalError);
++lines_written; ++lines_written;
} }
for (auto&& row : m_data) { for (auto&& row : m_data) {
if (with_headers) { if (with_headers) {
if (row.size() != m_names.size()) if (row.size() != m_names.size())
set_error(WriteError::NonConformingColumnCount); return Error::from_string_literal("Header count does not match given column count");
} }
write_row(row); TRY(write_row(row));
if (m_output.write({ "\n", 1 }) != 1) TRY(m_output->write_entire_buffer({ "\n", 1 }));
set_error(WriteError::InternalError);
++lines_written; ++lines_written;
if (lines_written >= max_preview_lines) if (lines_written >= max_preview_lines)
break; break;
} }
return {};
} }
private: private:
void set_error(WriteError error)
{
if (m_error == WriteError::None)
m_error = error;
}
template<typename T> template<typename T>
void write_row(T&& row) ErrorOr<void> write_row(T&& row)
{ {
bool first = true; bool first = true;
for (auto&& entry : row) { for (auto&& entry : row) {
if (!first) { if (!first) {
if (m_output.write(m_traits.separator.bytes()) != m_traits.separator.length()) TRY(m_output->write_entire_buffer(m_traits.separator.bytes()));
set_error(WriteError::InternalError);
} }
first = false; first = false;
write_entry(entry); TRY(write_entry(entry));
} }
return {};
} }
template<typename T> template<typename T>
void write_entry(T&& entry) ErrorOr<void> write_entry(T&& entry)
{ {
auto string = DeprecatedString::formatted("{}", FormatIfSupported(entry)); auto string = DeprecatedString::formatted("{}", FormatIfSupported(entry));
@ -169,49 +135,42 @@ private:
} }
if (safe_to_write_normally) { if (safe_to_write_normally) {
if (m_output.write(string.bytes()) != string.length()) if (!string.is_empty())
set_error(WriteError::InternalError); TRY(m_output->write_entire_buffer(string.bytes()));
return; return {};
} }
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length()) TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
set_error(WriteError::InternalError);
GenericLexer lexer(string); GenericLexer lexer(string);
while (!lexer.is_eof()) { while (!lexer.is_eof()) {
if (lexer.consume_specific(m_traits.quote)) { if (lexer.consume_specific(m_traits.quote)) {
switch (m_traits.quote_escape) { switch (m_traits.quote_escape) {
case WriterTraits::Repeat: case WriterTraits::Repeat:
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length()) TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
set_error(WriteError::InternalError); TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
set_error(WriteError::InternalError);
break; break;
case WriterTraits::Backslash: case WriterTraits::Backslash:
if (m_output.write({ "\\", 1 }) != 1) TRY(m_output->write_entire_buffer({ "\\", 1 }));
set_error(WriteError::InternalError); TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
set_error(WriteError::InternalError);
break; break;
} }
continue; continue;
} }
auto ch = lexer.consume(); auto ch = lexer.consume();
if (m_output.write({ &ch, 1 }) != 1) TRY(m_output->write_entire_buffer({ &ch, 1 }));
set_error(WriteError::InternalError);
} }
if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length()) TRY(m_output->write_entire_buffer(m_traits.quote.bytes()));
set_error(WriteError::InternalError); return {};
} }
ContainerType const& m_data; ContainerType const& m_data;
WriterTraits m_traits; WriterTraits m_traits;
WriterBehavior m_behaviors; WriterBehavior m_behaviors;
HeaderType const& m_names; HeaderType m_names;
WriteError m_error { WriteError::None }; Core::Stream::Handle<Core::Stream::Stream> m_output;
OutputStream& m_output;
}; };
} }