while implementing this, I've discovered a conceptual error: we allow to accept attributes, even when we've already entered the child scope. This means that we can not predictable get back at the "last" (i.e. the currently touched) element, because this might be such an attribute. So a really correct implementation would have to memorise the "current" element, which is really tricky, given the various ways of touching elements in our diff language. In the end I've decided to ignore this problem (maybe a better solution would have been to disallow those "late" attributes?) My reasoning is that attributes are unlikely to be full records, rather just values, and values are never mutated. (but note that it is definitively possible to have an record as attribute!)
363 lines
13 KiB
C++
363 lines
13 KiB
C++
/*
|
|
TREE-DIFF-APPLICATION.hpp - language to describe differences in linearised form
|
|
|
|
Copyright (C) Lumiera.org
|
|
2014, Hermann Vosseler <Ichthyostega@web.de>
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of
|
|
the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
|
|
*/
|
|
|
|
|
|
/** @file tree-diff-application.hpp
|
|
** Concrete implementation(s) to apply structural changes to hierarchical
|
|
** data structures. Together with the generic #DiffApplicator, this allows
|
|
** to receive linearised structural diff descriptions and apply them to
|
|
** a given target data structure, to effect the corresponding changes.
|
|
**
|
|
** \par Design considerations
|
|
** While -- conceptually -- our tree diff handling can be seen as an extension
|
|
** and generalisation of list diffing, the decision was \em not to embody this
|
|
** extension into the implementation technically, for sake of clarity. More so,
|
|
** since the Record, which serves as foundation for our »External Tree Description«,
|
|
** was made to look and behave like a list-like entity, but built with two distinct
|
|
** scopes at implementation level: the attribute scope and the contents scope. This
|
|
** carries over to the fine points of the list diff language semantics, especially
|
|
** when it comes to fault tolerance and strictness vs fuzziness in diff application.
|
|
** The implementation is thus faced with having to deal with an internal focus and
|
|
** a switch from scope to scope, which adds a lot of complexity. So the list diff
|
|
** application strategy can be seen as blueprint and demonstration of principles.
|
|
**
|
|
** Another point in question is weather see the diff application as manipulating
|
|
** a target data structure, or rather building a reshaped copy. The fact that
|
|
** GenNode and Record are designed as immutable values seems to favour the latter,
|
|
** yet the very reason to engage into building this diff framework was how to
|
|
** handle partial updates within a expectedly very large UI model, reflecting
|
|
** the actual session model in Proc-Layer. So we end up working on a Mutator,
|
|
** which clearly signals we're going to reshape and re-rig the target data.
|
|
**
|
|
** @see diff-list-application-test.cpp
|
|
** @see VerbToken
|
|
**
|
|
*/
|
|
|
|
|
|
#ifndef LIB_DIFF_TREE_DIFF_APPLICATION_H
|
|
#define LIB_DIFF_TREE_DIFF_APPLICATION_H
|
|
|
|
|
|
#include "lib/diff/tree-diff.hpp"
|
|
#include "lib/diff/gen-node.hpp"
|
|
#include "lib/format-string.hpp"
|
|
#include "lib/util.hpp"
|
|
|
|
#include <utility>
|
|
#include <stack>
|
|
|
|
namespace lib {
|
|
namespace diff{
|
|
|
|
using util::unConst;
|
|
using util::_Fmt;
|
|
using std::move;
|
|
using std::swap;
|
|
|
|
|
|
/**
|
|
* Interpreter for the tree-diff-language to work on GenNode elements
|
|
* A concrete strategy to apply a structural diff to a target data structure
|
|
* made from #Record<GenNode> elements. This data structure is assumed to be
|
|
* recursive, tree-like. But because Record elements are conceived as immutable
|
|
* and value-like, the tree diff application actually works on a Rec::Mutator
|
|
* wrapping the target record to be altered through consuming the diff.
|
|
* @throws lumiera::error::State when diff application fails due to the
|
|
* target sequence being different than assumed by the given diff.
|
|
* @see #TreeDiffInterpreter explanation of the verbs
|
|
*/
|
|
template<>
|
|
class DiffApplicationStrategy<Rec::Mutator>
|
|
: public TreeDiffInterpreter
|
|
{
|
|
using Mutator = Rec::Mutator;
|
|
using Content = Rec::ContentMutator;
|
|
using Iter = Content::Iter;
|
|
|
|
struct ScopeFrame
|
|
{
|
|
Mutator& target;
|
|
Content content;
|
|
|
|
ScopeFrame(Mutator& toModify)
|
|
: target(toModify)
|
|
, content()
|
|
{ }
|
|
|
|
void init()
|
|
{
|
|
target.swapContent (content);
|
|
}
|
|
};
|
|
|
|
/** Storage: a stack of workspaces
|
|
* used to handle nested child objects */
|
|
std::stack<ScopeFrame> scopes_;
|
|
|
|
|
|
Mutator& out() { return scopes_.top().target; }
|
|
Content& src() { return scopes_.top().content; }
|
|
Iter& srcPos() { return scopes_.top().content.pos; }
|
|
bool endOfData() { return srcPos() == src().end(); }
|
|
Rec& alteredRec() { return out(); }
|
|
|
|
|
|
void
|
|
__expect_in_target (GenNode const& elm, Literal oper)
|
|
{
|
|
if (endOfData())
|
|
throw error::State(_Fmt("Unable to %s element %s from target as demanded; "
|
|
"no (further) elements in target sequence") % oper % elm
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
|
|
if (elm.matches(Ref::CHILD) and not srcPos()->isNamed())
|
|
return; // allow for anonymous pick or delete of children
|
|
|
|
if (not srcPos()->matches(elm))
|
|
throw error::State(_Fmt("Unable to %s element %s from target as demanded; "
|
|
"found element %s on current target position instead")
|
|
% oper % elm % *srcPos()
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
}
|
|
|
|
void
|
|
__expect_further_elements (GenNode const& elm)
|
|
{
|
|
if (endOfData())
|
|
throw error::State(_Fmt("Premature end of target sequence, still expecting element %s; "
|
|
"unable to apply diff further.") % elm
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
}
|
|
|
|
void
|
|
__expect_found (GenNode const& elm, Iter const& targetPos)
|
|
{
|
|
if (targetPos == src().end())
|
|
throw error::State(_Fmt("Premature end of sequence; unable to locate "
|
|
"element %s in the remainder of the target.") % elm
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
}
|
|
|
|
void
|
|
__expect_successful_location (GenNode const& elm)
|
|
{
|
|
if (endOfData()
|
|
and not ( elm.matches(Ref::END) // after(_END_) -> its OK we hit the end
|
|
or (elm.matches(Ref::ATTRIBS) and src().children.empty()))) // after(_ATTRIBS_) -> if there are no children, it's OK to hit the end
|
|
throw error::State(_Fmt("Unable locate position 'after(%s)'") % elm.idi
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
}
|
|
|
|
void
|
|
__expect_valid_parent_scope (GenNode::ID const& idi)
|
|
{
|
|
if (scopes_.empty())
|
|
throw error::State(_Fmt("Unbalanced child scope bracketing tokens in diff; "
|
|
"When leaving scope %s, we fell out of root scope.") % idi.getSym()
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
|
|
if (alteredRec().empty())
|
|
throw error::State(_Fmt("Corrupted state. When leaving scope %s, "
|
|
"we found an empty parent scope.") % idi.getSym()
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
}
|
|
|
|
void
|
|
__expect_end_of_scope (GenNode::ID const& idi)
|
|
{
|
|
if (not endOfData())
|
|
throw error::State(_Fmt("Incomplete diff: when about to leave scope %s, "
|
|
"not all previously existing elements have been confirmed by the diff. "
|
|
"At least one spurious element %s was left over") % idi.getSym() % *srcPos()
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
}
|
|
|
|
|
|
Iter
|
|
find_in_current_scope (GenNode const& elm)
|
|
{
|
|
Iter end_of_scope = src().currIsAttrib()? src().attribs.end()
|
|
: src().children.end();
|
|
return std::find (srcPos(), end_of_scope, elm);
|
|
}
|
|
|
|
GenNode const&
|
|
find_child (GenNode::ID const& idi)
|
|
{
|
|
if (alteredRec().empty())
|
|
throw error::State(_Fmt("Attempt to mutate element %s, but current target data scope is empty. "
|
|
"Sender and receiver out of sync?") % idi.getSym()
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
|
|
// Short-cut-mutation: look at the last element.
|
|
// this should be the one just added. BUT NOTE: this fails
|
|
// when adding an attribute after entering the child scope.
|
|
// Since attributes are typically values and not mutated,
|
|
// this inaccuracy was deemed acceptable
|
|
auto& current = out().accessLast();
|
|
if (Ref::THIS.matches(idi) or current.matches(idi))
|
|
return current;
|
|
|
|
for (auto & child : alteredRec())
|
|
if (child.idi == idi)
|
|
return child;
|
|
|
|
throw error::State(_Fmt("Attempt to mutate non existing child record; unable to locate child %s "
|
|
"after applying the diff. Current scope: %s") % idi.getSym() % alteredRec()
|
|
, LUMIERA_ERROR_DIFF_CONFLICT);
|
|
}
|
|
|
|
void
|
|
move_into_new_sequence (Iter pos)
|
|
{
|
|
if (src().currIsAttrib())
|
|
out().appendAttrib (move(*pos)); //////////////TICKET #969 was it a good idea to allow adding attributes "after the fact"?
|
|
else
|
|
out().appendChild (move(*pos));
|
|
}
|
|
|
|
|
|
|
|
/* == Implementation of the list diff application primitives == */
|
|
|
|
void
|
|
ins (GenNode const& n) override
|
|
{
|
|
if (n.isNamed())
|
|
if (n.isTypeID())
|
|
out().setType (n.data.get<string>());
|
|
else
|
|
out().appendAttrib(n); //////////////TICKET #969 dto.
|
|
else
|
|
{
|
|
out().appendChild(n);
|
|
if (src().currIsAttrib())
|
|
src().jumpToChildScope();
|
|
}
|
|
}
|
|
|
|
void
|
|
del (GenNode const& n) override
|
|
{
|
|
__expect_in_target(n, "remove");
|
|
++src();
|
|
}
|
|
|
|
void
|
|
pick (GenNode const& n) override
|
|
{
|
|
__expect_in_target(n, "pick");
|
|
move_into_new_sequence (srcPos());
|
|
++src();
|
|
}
|
|
|
|
void
|
|
skip (GenNode const& n) override
|
|
{
|
|
__expect_further_elements (n);
|
|
++src();
|
|
} // assume the actual content has been moved away by a previous find()
|
|
|
|
void
|
|
find (GenNode const& n) override
|
|
{
|
|
__expect_further_elements (n);
|
|
Iter found = find_in_current_scope(n);
|
|
__expect_found (n, found);
|
|
move_into_new_sequence (found);
|
|
} // consume and leave waste, expected to be cleaned-up by skip() later
|
|
|
|
|
|
|
|
/* == Implementation of the tree diff application primitives == */
|
|
|
|
/** cue to a position behind the named node,
|
|
* thereby picking (accepting) all traversed elements
|
|
* into the reshaped new data structure as-is */
|
|
void
|
|
after (GenNode const& n) override
|
|
{
|
|
if (n.matches(Ref::ATTRIBS))
|
|
while (not endOfData() and srcPos()->isNamed())
|
|
{
|
|
move_into_new_sequence (srcPos());
|
|
++src();
|
|
}
|
|
else
|
|
if (n.matches(Ref::END))
|
|
while (not endOfData())
|
|
{
|
|
move_into_new_sequence (srcPos());
|
|
++src();
|
|
}
|
|
else
|
|
while (not (endOfData() or srcPos()->matches(n)))
|
|
{
|
|
move_into_new_sequence (srcPos());
|
|
++src();
|
|
}
|
|
|
|
__expect_successful_location(n);
|
|
|
|
if (not endOfData() and srcPos()->matches(n))
|
|
++src(); // get /after/ an explicitly given position
|
|
}
|
|
|
|
/** open nested scope to apply diff to child object */
|
|
void
|
|
mut (GenNode const& n) override
|
|
{
|
|
GenNode const& child = find_child (n.idi);
|
|
Rec const& childRecord = child.data.get<Rec>();
|
|
scopes_.emplace (mutateInPlace (unConst(childRecord)));
|
|
}
|
|
|
|
/** finish and leave child object scope, return to parent */
|
|
void
|
|
emu (GenNode const& n) override
|
|
{
|
|
__expect_end_of_scope (n.idi);
|
|
scopes_.pop();
|
|
__expect_valid_parent_scope (n.idi);
|
|
}
|
|
|
|
|
|
public:
|
|
explicit
|
|
DiffApplicationStrategy(Rec::Mutator& mutableTargetRecord)
|
|
: scopes_()
|
|
{
|
|
scopes_.emplace(mutableTargetRecord);
|
|
}
|
|
|
|
void
|
|
initDiffApplication()
|
|
{
|
|
REQUIRE (1 == scopes_.size());
|
|
scopes_.top().init();
|
|
}
|
|
};
|
|
|
|
|
|
}} // namespace lib::diff
|
|
#endif /*LIB_DIFF_TREE_DIFF_APPLICATION_H*/
|