xaizek / dit (License: GPLv3) (since 2018-12-07)
Command-line task keeper that remembers all old values and is meant to combine several orthogonal features to be rather flexible in managing items.
<root> / src / Item.cpp (56b123d4de508668eb5c0181d46726c8d7c06c7d) (5,413B) (mode 100644) [raw]
// Copyright (C) 2015 xaizek <xaizek@posteo.net>
//
// This file is part of dit.
//
// dit is free software: you can redistribute it and/or modify
// it under the terms of version 3 of the GNU Affero General Public
// License as published by the Free Software Foundation.
//
// dit 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 dit.  If not, see <http://www.gnu.org/licenses/>.

#include "Item.hpp"

#include <functional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

#include <boost/range/adaptor/reversed.hpp>

#include "utils/Passkey.hpp"
#include "utils/time.hpp"
#include "Change.hpp"
#include "Storage.hpp"
#include "parsing.hpp"

/**
 * @brief Function used to obtain current timestamp.
 */
static std::function<std::time_t()> getTime =
    [](){
        return std::time(nullptr);
    };

bool
Item::isValidKeyName(const std::string &name, bool forWrite, std::string &error)
{
    auto iter = name.cbegin();
    auto end = name.cend();
    if (!parseKeyName(iter, end)) {
        error = "Invalid key name at " + std::string(&*iter);
        return false;
    }

    if (forWrite && name[0] == '_') {
        error = "The key is read-only.";
        return false;
    }

    return true;
}

Item::Item(Storage &storage, std::string id, bool exists, pk<Storage>)
    : StorageBacked<Item>(!exists), storage(storage), id(std::move(id))
{
    // Count item creation as a modification.
    if (!exists) {
        markModified();
    }
}

Item::Item(Storage &storage, std::string id, pk<Tests>)
    : StorageBacked<Item>(true), storage(storage), id(std::move(id))
{
}

Item::~Item()
{
}

const std::string &
Item::getId() const
{
    return id;
}

std::string
Item::getValue(const std::string &key)
{
    std::string error;
    if (!isValidKeyName(key, false, error)) {
        throw std::runtime_error(error);
    }

    // Handle pseudo fields.
    if (key == "_id") {
        return getId();
    } else if(key == "_created") {
        if (changes.empty()) {
            return std::string();
        }

        return timeToString(changes.front().getTimestamp());
    } else if(key == "_changed") {
        if (changes.empty()) {
            return std::string();
        }

        return timeToString(changes.back().getTimestamp());
    }

    const Change *const change = getLatestChange(key);
    return (change != nullptr) ? change->getValue() : std::string();
}

std::set<std::string>
Item::listRecordNames()
{
    ensureLoaded();

    std::set<std::string> names;
    for (const Change &c : changes) {
        if (!c.getValue().empty()) {
            names.insert(c.getKey());
        } else {
            names.erase(c.getKey());
        }
    }
    return names;
}

void
Item::load()
{
    storage.fill(*this, {});

    for (int i = 0, c = changes.size(); i < c - 1; ++i) {
        if (changes[i].getTimestamp() > changes[i + 1].getTimestamp()) {
            throw std::logic_error("Change set for " + id + " is not sorted.");
        }
    }
}

void
Item::setValue(const std::string &key, const std::string &value)
{
    std::string error;
    if (!isValidKeyName(key, true, error)) {
        throw std::runtime_error(error);
    }

    const std::time_t timestamp = getTime();

    if (Change *const change = getLatestChange(key)) {
        if (change->getValue() == value) {
            return;
        }

        if (change->getTimestamp() == timestamp) {
            // Update previous change with new value.
            *change = Change(timestamp, key, value);

            if (Change *const prev = getLatestChange(key, change)) {
                if (prev->getValue() == value) {
                    // Remove the change that matches old value.
                    changes.erase(changes.begin() + (change - &changes[0]));
                }
            } else if (value.empty()) {
                // Remove the change that matches old value.
                changes.erase(changes.begin() + (change - &changes[0]));
            }

            markModified();
            return;
        }
    } else if (value.empty()) {
        return;
    }

    changes.emplace_back(timestamp, key, value);
    markModified();
}

Change *
Item::getLatestChange(const std::string &key)
{
    ensureLoaded();

    for (Change &c : boost::adaptors::reverse(changes)) {
        if (c.getKey() == key) {
            return &c;
        }
    }
    return nullptr;
}

Change *
Item::getLatestChange(const std::string &key, Change *before)
{
    for (Change *c = before - 1; c >= &changes[0]; --c) {
        if (c->getKey() == key) {
            return c;
        }
    }
    return nullptr;
}

bool
Item::wasChanged() const
{
    return isModified();
}

const std::vector<Change> &
Item::getChanges()
{
    ensureLoaded();
    return changes;
}

std::vector<Change> &
Item::getChanges(pk<Storage>)
{
    return changes;
}

const std::vector<Change> &
Item::getChanges(pk<Storage>) const
{
    return changes;
}

void
Item::setTimeSource(std::function<std::time_t()> getTime, pk<Tests>)
{
    if (!getTime) {
        ::getTime = [](){ return std::time(nullptr); };
    } else {
        ::getTime = getTime;
    }
}
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/dit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/dit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master