Skip to content

Commit

Permalink
JSON support for changeset upload
Browse files Browse the repository at this point in the history
  • Loading branch information
mmd-osm committed Aug 7, 2024
1 parent a2889bb commit 362c467
Show file tree
Hide file tree
Showing 17 changed files with 1,370 additions and 28 deletions.
325 changes: 325 additions & 0 deletions include/cgimap/api06/changeset_upload/osmchange_json_input_format.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
/**
* SPDX-License-Identifier: GPL-2.0-only
*
* This file is part of openstreetmap-cgimap (https://github.com/zerebubuth/openstreetmap-cgimap/).
*
* Copyright (C) 2009-2023 by the CGImap developer community.
* For a full list of authors see the git log.
*/

#ifndef OSMCHANGE_JSON_INPUT_FORMAT_HPP
#define OSMCHANGE_JSON_INPUT_FORMAT_HPP

#include "cgimap/api06/changeset_upload/node.hpp"
#include "cgimap/api06/changeset_upload/osmobject.hpp"
#include "cgimap/api06/changeset_upload/parser_callback.hpp"
#include "cgimap/api06/changeset_upload/relation.hpp"
#include "cgimap/api06/changeset_upload/way.hpp"
#include "cgimap/types.hpp"

#include "sjparser/sjparser.h"

#include <fmt/core.h>

#include <cassert>
#include <memory>
#include <string>
#include <utility>
#include <vector>


namespace api06 {

using SJParser::Array;
using SJParser::Member;
using SJParser::Object;
using SJParser::Parser;
using SJParser::Presence;
using SJParser::SArray;
using SJParser::SAutoObject;
using SJParser::SMap;
using SJParser::Value;
using SJParser::Reaction;
using SJParser::ObjectOptions;

using std::placeholders::_1;

class OSMChangeJSONParserFormat {

static auto getMemberParser() {
return SAutoObject{std::tuple{Member{"type", Value<std::string>{}},
Member{"ref", Value<int64_t>{}},
Member{"role", Value<std::string>{}, Presence::Optional, ""}},
ObjectOptions{Reaction::Ignore}
};
}

template <typename ElementParserCallback = std::nullptr_t>
static auto getElementsParser(ElementParserCallback element_parser_callback = nullptr) {
return Object{
std::tuple{
Member{"type", Value<std::string>{}},
Member{"action", Value<std::string>{}},
Member{"if-unused", Value<bool>{}, Presence::Optional, false},
Member{"id", Value<int64_t>{}},
Member{"lat", Value<double>{}, Presence::Optional},
Member{"lon", Value<double>{}, Presence::Optional},
Member{"version", Value<int64_t>{}, Presence::Optional},
Member{"changeset", Value<int64_t>{}},
Member{"tags", SMap{Value<std::string>{}}, Presence::Optional},
Member{"nodes", SArray{Value<int64_t>{}}, Presence::Optional},
Member{"members", SArray{getMemberParser()}, Presence::Optional}
},
ObjectOptions{Reaction::Ignore},
element_parser_callback};
}

template <typename ElementParserCallback = std::nullptr_t>
static auto getMainParser(ElementParserCallback element_parser_callback = nullptr) {
return Parser{
Object{
std::tuple{
Member{"version", Value<std::string>{}, Presence::Optional},
Member{"generator", Value<std::string>{}, Presence::Optional},
Member{"osmChange", Array{getElementsParser(element_parser_callback)}}
},ObjectOptions{Reaction::Ignore}}};
}

friend class OSMChangeJSONParser;
};

class OSMChangeJSONParser {

public:
explicit OSMChangeJSONParser(Parser_Callback& callback)
: m_callback(callback) { }

OSMChangeJSONParser(const OSMChangeJSONParser &) = delete;
OSMChangeJSONParser &operator=(const OSMChangeJSONParser &) = delete;

OSMChangeJSONParser(OSMChangeJSONParser &&) = delete;
OSMChangeJSONParser &operator=(OSMChangeJSONParser &&) = delete;

void process_message(const std::string &data) {

try {
m_callback.start_document();
_parser.parse(data);
_parser.finish();

if (_parser.parser().isEmpty()) {
throw payload_error("Empty JSON payload");
}

if (element_count == 0) {
throw payload_error("osmChange array is empty");
}

m_callback.end_document();
} catch (const std::exception& e) {
throw http::bad_request(e.what()); // rethrow JSON parser error as HTTP 400 Bad request
}
}

private:

using ElementsParser = decltype(api06::OSMChangeJSONParserFormat::getElementsParser());
using MainParser = decltype(api06::OSMChangeJSONParserFormat::getMainParser());

MainParser _parser{api06::OSMChangeJSONParserFormat::getMainParser(std::bind(&api06::OSMChangeJSONParser::process_element, this, _1))};

// OSM element callback
bool process_element(ElementsParser &parser) {

element_count++;

// process action
process_action(parser);

// process if-unused flag for delete action
process_if_unused(parser);

// process type (node, way, relation)
process_type(parser);

return true;
}

void process_action(ElementsParser &parser) {

const std::string& action = parser.get<1>();

if (action == "create") {
m_operation = operation::op_create;
} else if (action == "modify") {
m_operation = operation::op_modify;
} else if (action == "delete") {
m_operation = operation::op_delete;
} else {
throw payload_error{fmt::format("Unknown action {}, choices are create, modify, delete", action)};
}
}

void process_if_unused(ElementsParser &parser) {

if (m_operation == operation::op_delete) {
m_if_unused = false;
if (parser.parser<2>().isSet()) {
m_if_unused = parser.get<2>();
}
}
}

void process_type(ElementsParser &parser) {

const std::string& type = parser.get<0>();

if (type == "node") {
process_node(parser);
} else if (type == "way") {
process_way(parser);
} else if (type == "relation") {
process_relation(parser);
} else {
throw payload_error{fmt::format("Unknown element {}, expecting node, way or relation", type)};
}
}

void process_node(ElementsParser& parser) {

Node node;
init_object(node, parser);

if (parser.parser<4>().isSet()) {
node.set_lat(parser.get<4>());
}

if (parser.parser<5>().isSet()) {
node.set_lon(parser.get<5>());
}

process_tags(node, parser);

if (!node.is_valid(m_operation)) {
throw payload_error{fmt::format("{} does not include all mandatory fields", node.to_string())};
}

m_callback.process_node(node, m_operation, m_if_unused);
}

void process_way(ElementsParser& parser) {

Way way;
init_object(way, parser);

// adding way nodes
if (parser.parser<9>().isSet()) {
for (const auto& value : parser.get<9>()) {
way.add_way_node(value);
}
}

process_tags(way, parser);

if (!way.is_valid(m_operation)) {
throw payload_error{fmt::format("{} does not include all mandatory fields", way.to_string())};
}

m_callback.process_way(way, m_operation, m_if_unused);
}

void process_relation(ElementsParser& parser) {

Relation relation;
init_object(relation, parser);

process_relation_members(relation, parser);

process_tags(relation, parser);

if (!relation.is_valid(m_operation)) {
throw payload_error{fmt::format("{} does not include all mandatory fields", relation.to_string())};
}

m_callback.process_relation(relation, m_operation, m_if_unused);
}

void process_relation_members(Relation &relation, ElementsParser& parser) {

if (!parser.parser<10>().isSet()) {
return;
}

for (auto &mbr : parser.get<10>()) {
const auto& [type, ref, role] = mbr;

RelationMember member;
member.set_type(type);
member.set_ref(ref);
member.set_role(role);

if (!member.is_valid()) {
throw payload_error{fmt::format("Missing mandatory field on relation member in {}", relation.to_string()) };
}
relation.add_member(member);
}
}

void process_tags(OSMObject &o, ElementsParser& parser) {

if (parser.parser<8>().isSet()) {
for (const auto &tag : parser.get<8>()) {
o.add_tag(tag.first, tag.second);
}
}
}

void init_object(OSMObject &object, ElementsParser& parser) {

// id
object.set_id(parser.get<3>());

// version
if (parser.parser<6>().isSet()) {
object.set_version(parser.get<6>());
}

// changeset
if (parser.parser<7>().isSet()) {
object.set_changeset(parser.get<7>());
}

// TODO: not needed, handled by sjparser
if (!object.has_id()) {
throw payload_error{ "Mandatory field id missing in object" };
}

if (!object.has_changeset()) {
throw payload_error{fmt::format("Changeset id is missing for {}", object.to_string()) };
}

if (m_operation == operation::op_create) {
// we always override version number for create operations (they are not
// mandatory)
object.set_version(0u);
} else if (m_operation == operation::op_delete ||
m_operation == operation::op_modify) {
// objects for other operations must have a positive version number
if (!object.has_version()) {
throw payload_error{fmt::format("Version is required when updating {}", object.to_string()) };
}
if (object.version() < 1) {
throw payload_error{ fmt::format("Invalid version number {} in {}", object.version(), object.to_string()) };
}
}
}

operation m_operation = operation::op_undefined;
Parser_Callback& m_callback;
bool m_if_unused = false;
int element_count = 0;
};

} // namespace api06

#endif // OSMCHANGE_JSON_INPUT_FORMAT_HPP
3 changes: 3 additions & 0 deletions include/cgimap/json_formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class json_formatter : public output_formatter {
void start_changeset(bool) override;
void end_changeset(bool) override;

void start_diffresult() override;
void end_diffresult() override;
void start_action(action_type type) override;
void end_action(action_type type) override;
void error(const std::exception &e) override;
Expand All @@ -63,6 +65,7 @@ class json_formatter : public output_formatter {
const osm_nwr_signed_id_t old_id,
const osm_nwr_id_t new_id,
const osm_version_t new_version) override;

void write_diffresult_delete(const element_type elem,
const osm_nwr_signed_id_t old_id) override;

Expand Down
2 changes: 2 additions & 0 deletions include/cgimap/osm_diffresult_responder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class osm_diffresult_responder : public osm_responder {

~osm_diffresult_responder() override;

// lists the standard types that OSM format can respond in
std::vector<mime::type> types_available() const override;

void write(output_formatter& f,
const std::string &generator,
Expand Down
22 changes: 22 additions & 0 deletions include/cgimap/output_formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ T element_type_name(element_type elt) noexcept {
return "";
}

template <typename T = const char*>
T action_type_name(action_type action) noexcept {

switch (action) {
case action_type::create:
return "create";
break;
case action_type::modify:
return "modify";
break;
case action_type::del:
return "delete";
break;
}
return "";
}

} // anonymous namespace

struct element_info {
Expand Down Expand Up @@ -216,6 +233,11 @@ struct output_formatter {

virtual void end_changeset(bool) = 0;

// marks the beginning of diffResult response processing
virtual void start_diffresult() = 0;

virtual void end_diffresult() = 0;

// TODO: document me.
virtual void start_action(action_type type) = 0;
virtual void end_action(action_type type) = 0;
Expand Down
2 changes: 2 additions & 0 deletions include/cgimap/text_formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class text_formatter : public output_formatter {
void start_changeset(bool) override;
void end_changeset(bool) override;

void start_diffresult() override;
void end_diffresult() override;
void start_action(action_type type) override;
void end_action(action_type type) override;
void error(const std::exception &e) override;
Expand Down
Loading

0 comments on commit 362c467

Please sign in to comment.