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
3 changes: 2 additions & 1 deletion .agents/skills/mx-api-doctrine/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ Responses to wrong api usage, in order of preference:
2. Defined fallback: document a harmless result and return it. A wrong-kind choice accessor
returns a default-constructed copy; the writer drops the half of an encoding that is
meaningless for the note it is on (a tie on a silent cue note is written as `<tied>`
notation only, never as a sound-level `<tie>`). No signal to the caller.
notation only, never as a sound-level `<tie>`). Report an important adjustment through an
optional `Diagnostics` collector.
3. `Result<T>` (`Result.h`): the error channel of last resort. It exists for the `MusicXml`
I/O boundary, where failure is real (unreadable file, unparseable XML). Do not spread it
into the data model.
Expand Down
70 changes: 70 additions & 0 deletions src/include/mx/api/Diagnostics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#pragma once

#include "mx/api/Result.h"

#include <functional>
#include <span>
#include <string>
#include <vector>

namespace mx
{
namespace api
{
// How a successful translation affected the resulting score or document.
enum class Severity
{
info, // noteworthy normalization without meaningful loss
warning, // adjusted data; the result remains usable
error // omitted data; the operation still produced a value
};

// The recoverable decision reported by a diagnostic.
enum class DiagnosticCode
{
valueAdjusted, // a value was changed to one that MusicXML can represent
unmatchedSpanner // a spanner endpoint had no matching endpoint
};

// A non-fatal problem noticed while producing a score or MusicXML document.
struct Diagnostic
{
Severity severity = Severity::warning; // effect on the translated value
DiagnosticCode code = DiagnosticCode::valueAdjusted; // kind of recovery performed
Location location; // source or output score position, when known
std::string message; // human-readable description
};

// Receives a diagnostic while a score or MusicXML document is translated.
using DiagnosticHandler = std::function<void(const Diagnostic &)>;

// Collects diagnostics from one or more translations. A handler, when supplied,
// runs as each diagnostic is added.
class Diagnostics
{
public:
Diagnostics() = default;
explicit Diagnostics(DiagnosticHandler handler);

// Adds a diagnostic to this collector and calls its handler.
void add(Diagnostic diagnostic);

// All diagnostics collected so far, in the order they were reported.
std::span<const Diagnostic> all() const noexcept;

// True when the collection contains this severity or a more severe one.
bool hasSeverityOrWorse(Severity threshold) const noexcept;

private:
std::vector<Diagnostic> myDiagnostics;
DiagnosticHandler myHandler;
};

// Writes a diagnostic on one line for logs and test output.
std::string formatDiagnostic(const Diagnostic &diagnostic);
} // namespace api
} // namespace mx
29 changes: 27 additions & 2 deletions src/include/mx/api/MusicXml.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#pragma once

#include "mx/api/Diagnostics.h"
#include "mx/api/Result.h"
#include "mx/api/ScoreData.h"

Expand Down Expand Up @@ -66,24 +67,48 @@ class MusicXml
class Impl;
std::unique_ptr<Impl> myImpl;

friend Result<ScoreData> getScore(const MusicXml &document);
friend Result<MusicXml> fromScore(const ScoreData &score);
friend Result<ScoreData> getScore(const MusicXml &document, Diagnostics &diagnostics);
friend Result<MusicXml> fromScore(const ScoreData &score, Diagnostics &diagnostics);
};

// Reads the score out of the document. The document stays alive and can be
// read again or written out.
Result<ScoreData> getScore(const MusicXml &document);

// Reads the score and reports any adjustments made while translating it.
//
// To inspect diagnostics after the translation:
//
// Diagnostics diagnostics;
// auto score = getScore(document, diagnostics);
// for (const auto &diagnostic : diagnostics.all()) {
// std::cerr << formatDiagnostic(diagnostic) << '\n';
// }
//
// To handle diagnostics as they are found:
//
// Diagnostics diagnostics{[](const Diagnostic &diagnostic) {
// std::cerr << formatDiagnostic(diagnostic) << '\n';
// }};
// auto score = getScore(document, diagnostics);
Result<ScoreData> getScore(const MusicXml &document, Diagnostics &diagnostics);

// Reads the score out of the document and consumes it: the underlying tree
// is freed when this function returns rather than when your MusicXml binding
// goes out of scope. Pass the document with std::move, or hand over the
// Result's value directly.
Result<ScoreData> intoScore(MusicXml document);

// Consumes the document and reports any adjustments made while reading it.
Result<ScoreData> intoScore(MusicXml document, Diagnostics &diagnostics);

// Authors a new document from ScoreData. Fails with an error result when the
// ScoreData describes something the core model will not represent (e.g. more
// than 8 beams) rather than silently dropping data.
Result<MusicXml> fromScore(const ScoreData &score);

// Authors a document and reports recoverable authoring decisions.
Result<MusicXml> fromScore(const ScoreData &score, Diagnostics &diagnostics);

} // namespace api
} // namespace mx
67 changes: 67 additions & 0 deletions src/private/mx/api/Diagnostics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#include "mx/api/Diagnostics.h"

#include "mx/api/LocationFormatting.h"

#include <utility>

namespace mx
{
namespace api
{
Diagnostics::Diagnostics(DiagnosticHandler handler) : myHandler{std::move(handler)}
{
}

void Diagnostics::add(Diagnostic diagnostic)
{
myDiagnostics.push_back(std::move(diagnostic));
if (myHandler)
{
myHandler(myDiagnostics.back());
}
}

std::span<const Diagnostic> Diagnostics::all() const noexcept
{
return myDiagnostics;
}

bool Diagnostics::hasSeverityOrWorse(Severity threshold) const noexcept
{
for (const auto &diagnostic : myDiagnostics)
{
if (diagnostic.severity >= threshold)
{
return true;
}
}
return false;
}

std::string formatDiagnostic(const Diagnostic &diagnostic)
{
const char *severityName = "warning";
switch (diagnostic.severity)
{
case Severity::info:
severityName = "info";
break;
case Severity::warning:
severityName = "warning";
break;
case Severity::error:
severityName = "error";
break;
}

std::string result{"mx: "};
result += severityName;
result += formatLocationAndMessage(diagnostic.location, diagnostic.message);
return result;
}
} // namespace api
} // namespace mx
51 changes: 51 additions & 0 deletions src/private/mx/api/LocationFormatting.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#include "mx/api/LocationFormatting.h"

namespace mx
{
namespace api
{
std::string formatLocationAndMessage(const Location &location, const std::string &message)
{
std::string result;
std::string where;
const auto appendWhere = [&where](const char *name, long long value) {
if (value < 0)
{
return;
}
if (!where.empty())
{
where += ' ';
}
where += name;
where += '=';
where += std::to_string(value);
};

where += location.xmlPath;
appendWhere("part", location.partIndex);
appendWhere("measure", location.measureIndex);
appendWhere("staff", location.staffIndex);
appendWhere("voice", location.voiceIndex);
appendWhere("tick", location.tickTimePosition);
appendWhere("offset", location.byteOffset);

if (!where.empty())
{
result += " at ";
result += where;
}

if (!message.empty())
{
result += ": ";
result += message;
}
return result;
}
} // namespace api
} // namespace mx
17 changes: 17 additions & 0 deletions src/private/mx/api/LocationFormatting.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// MusicXML Class Library
// Copyright (c) by Matthew James Briggs
// Distributed under the MIT License

#pragma once

#include "mx/api/Result.h"

#include <string>

namespace mx
{
namespace api
{
std::string formatLocationAndMessage(const Location &location, const std::string &message);
} // namespace api
} // namespace mx
26 changes: 22 additions & 4 deletions src/private/mx/api/MusicXml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,16 @@ const core::Document &MusicXml::getCoreDocument() const
}

Result<MusicXml> fromScore(const ScoreData &score)
{
Diagnostics diagnostics;
return fromScore(score, diagnostics);
}

Result<MusicXml> fromScore(const ScoreData &score, Diagnostics &diagnostics)
{
try
{
impl::ScoreWriter writer{score};
impl::ScoreWriter writer{score, impl::DiagnosticsContext{diagnostics}};
core::ScorePartwise scorePartwise = writer.getScorePartwise();

if (score.musicXmlType == "timewise")
Expand Down Expand Up @@ -346,6 +352,12 @@ Result<MusicXml> fromScore(const ScoreData &score)
}

Result<ScoreData> getScore(const MusicXml &document)
{
Diagnostics diagnostics;
return getScore(document, diagnostics);
}

Result<ScoreData> getScore(const MusicXml &document, Diagnostics &diagnostics)
{
try
{
Expand All @@ -356,13 +368,13 @@ Result<ScoreData> getScore(const MusicXml &document)
if (coreDocument.isScoreTimewise())
{
const core::ScorePartwise scorePartwise = impl::timewisePartwise(coreDocument.asScoreTimewise());
impl::ScoreReader reader{scorePartwise};
impl::ScoreReader reader{scorePartwise, impl::DiagnosticsContext{diagnostics}};
auto score = reader.getScoreData();
score.musicXmlType = "timewise";
return score;
}

impl::ScoreReader reader{coreDocument.asScorePartwise()};
impl::ScoreReader reader{coreDocument.asScorePartwise(), impl::DiagnosticsContext{diagnostics}};
return reader.getScoreData();
}
catch (const std::bad_alloc &)
Expand All @@ -380,10 +392,16 @@ Result<ScoreData> getScore(const MusicXml &document)
}

Result<ScoreData> intoScore(MusicXml document)
{
Diagnostics diagnostics;
return intoScore(std::move(document), diagnostics);
}

Result<ScoreData> intoScore(MusicXml document, Diagnostics &diagnostics)
{
// the parameter owns the document; its destructor frees the underlying
// tree when this function returns
return getScore(document);
return getScore(document, diagnostics);
}
} // namespace api
} // namespace mx
Loading
Loading