diff --git a/autotests/datatypestest.cpp b/autotests/datatypestest.cpp index a565403..646c39e 100644 --- a/autotests/datatypestest.cpp +++ b/autotests/datatypestest.cpp @@ -1,153 +1,170 @@ /* Copyright (c) 2018 Volker Krause Copyright (c) 2018 Luca Beltrame This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; class DatatypesTest : public QObject { Q_OBJECT private Q_SLOTS: void testValueTypeSemantics() { Airport airport; airport.setName(QLatin1String("Berlin Tegel")); { auto ap2 = airport; QCOMPARE(ap2.name(), QLatin1String("Berlin Tegel")); ap2.setIataCode(QLatin1String("TXL")); QVERIFY(airport.iataCode().isEmpty()); } Place place = airport; // assignment to base class works, but cannot be reversed QCOMPARE(place.name(), QLatin1String("Berlin Tegel")); place.setName(QLatin1String("London Heathrow")); // changing a value degenerated to its base class will detach but not slice QCOMPARE(place.name(), QLatin1String("London Heathrow")); QCOMPARE(JsonLdDocument::readProperty(place, "className").toString(), QLatin1String("Place")); // className is not polymorphic QVERIFY(!airport.geo().isValid()); QCOMPARE(JsonLdDocument::readProperty(airport, "className").toString(), QLatin1String("Airport")); QCOMPARE(JsonLdDocument::readProperty(airport, "name").toString(), QLatin1String("Berlin Tegel")); // detach on base properties must not slice FlightReservation res; QCOMPARE(JsonLdDocument::readProperty(res, "className").toString(), QLatin1String("FlightReservation")); res.setAirplaneSeat(QLatin1String("10E")); QCOMPARE(res.airplaneSeat(), QLatin1String("10E")); auto res2 = res; QCOMPARE(res2.airplaneSeat(), QLatin1String("10E")); res2.setReservationNumber(QLatin1String("XXX007")); QCOMPARE(res2.airplaneSeat(), QLatin1String("10E")); // changing default-created properties should not leak Flight flight; QCOMPARE(JsonLdDocument::readProperty(flight, "className").toString(), QLatin1String("Flight")); QVariant v = flight; QVERIFY(v.canConvert()); JsonLdDocument::writeProperty(v, "departureAirport", airport); QCOMPARE(v.value().departureAirport().name(), QLatin1String("Berlin Tegel")); // make sure all meta types are generated TrainTrip tt; QCOMPARE(JsonLdDocument::readProperty(tt, "className").toString(), QLatin1String("TrainTrip")); BusTrip bus; QCOMPARE(JsonLdDocument::readProperty(bus, "className").toString(), QLatin1String("BusTrip")); Ticket ticket; QCOMPARE(JsonLdDocument::readProperty(ticket.ticketedSeat(), "className").toString(), QLatin1String("Seat")); Organization org; org.setName(QLatin1String("JR East")); org.setEmail(QLatin1String("nowhere@nowhere.com")); org.setTelephone(QLatin1String("+55-1234-345")); org.setUrl(QUrl(QLatin1String("http://www.jreast.co.jp/e/"))); QCOMPARE(JsonLdDocument::readProperty(org, "className").toString(), QLatin1String("Organization")); QCOMPARE(org.name(), QLatin1String("JR East")); QCOMPARE(org.email(), QLatin1String("nowhere@nowhere.com")); QCOMPARE(org.telephone(), QLatin1String("+55-1234-345")); QCOMPARE(org.url(), QUrl(QLatin1String("http://www.jreast.co.jp/e/"))); tt.setProvider(org); flight.setProvider(org); res.setProvider(org); bus.setProvider(org); QCOMPARE(tt.provider().name(), QLatin1String("JR East")); QCOMPARE(tt.provider().email(), QLatin1String("nowhere@nowhere.com")); QCOMPARE(flight.provider().name(), QLatin1String("JR East")); QCOMPARE(flight.provider().email(), QLatin1String("nowhere@nowhere.com")); QCOMPARE(res.provider().name(), QLatin1String("JR East")); QCOMPARE(res.provider().email(), QLatin1String("nowhere@nowhere.com")); QCOMPARE(bus.provider().name(), QLatin1String("JR East")); QCOMPARE(bus.provider().email(), QLatin1String("nowhere@nowhere.com")); Airline airline; airline.setIataCode(QLatin1String("LH")); flight.setAirline(airline); QCOMPARE(flight.airline().iataCode(), QLatin1String("LH")); { const auto flight2 = flight; QCOMPARE(flight2.airline().iataCode(), QLatin1String("LH")); QCOMPARE(JsonLdDocument::readProperty(flight.airline(), "className").toString(), QLatin1String("Airline")); QCOMPARE(JsonLdDocument::readProperty(flight.airline(), "iataCode").toString(), QLatin1String("LH")); } } void testQmlCompatibility() { // one variant and one typed property FlightReservation res; Flight flight; Airport airport; airport.setName(QLatin1String("Berlin Tegel")); flight.setDepartureAirport(airport); res.setReservationFor(flight); QQmlEngine engine; engine.rootContext()->setContextProperty(QLatin1String("_res"), res); QQmlComponent component(&engine); component.setData("import QtQml 2.2\nQtObject { Component.onCompleted: console.log(_res.reservationFor.departureAirport.name); }", QUrl()); if (component.status() == QQmlComponent::Error) { qWarning() << component.errorString(); } QCOMPARE(component.status(), QQmlComponent::Ready); auto obj = component.create(); QVERIFY(obj); } + + void testUpCastHelper() + { + Place p; + FoodEstablishment r; + Airport a; + a.setName(QLatin1String("Berlin Tegel")); + a.setIataCode(QLatin1String("TXL")); + + QVERIFY(JsonLd::canConvert(a)); + QVERIFY(JsonLd::canConvert(a)); + QVERIFY(!JsonLd::canConvert(a)); + QVERIFY(!JsonLd::canConvert(p)); + + p = JsonLd::convert(a); + QCOMPARE(p.name(), QLatin1String("Berlin Tegel")); + } }; QTEST_MAIN(DatatypesTest) #include "datatypestest.moc" diff --git a/src/datatypes/datatypes.h b/src/datatypes/datatypes.h index 7e1a6a0..d152a20 100644 --- a/src/datatypes/datatypes.h +++ b/src/datatypes/datatypes.h @@ -1,65 +1,96 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KITINERARY_DATATYPES_H #define KITINERARY_DATATYPES_H #include #include +#include #include class QString; namespace KItinerary { + +/** JSON-LD data type helper functions. */ +namespace JsonLd { + +/** Checks if the given value can be up-cast to T */ +template +inline bool canConvert(const QVariant &value) +{ + const auto mo = QMetaType(value.userType()).metaObject(); + if (!mo) { + return false; + } + return mo->inherits(&T::staticMetaObject); +} + +/** Up-cast @p value to T. + * @note This does not perform any safety checks! + * @see canConvert + */ +template +inline T convert(const QVariant &value) +{ + return T(*static_cast(value.constData())); +} + +} + +///@cond internal namespace detail { template struct parameter_type { using type = typename std::conditional::value, T, const T&>::type; }; } } #define KITINERARY_GADGET(Class) \ Q_GADGET \ Q_PROPERTY(QString className READ className STORED false CONSTANT) \ QString className() const; \ public: \ Class(); \ Class(const Class &other); \ ~Class(); \ Class& operator=(const Class &other); \ operator QVariant () const; \ private: #define KITINERARY_BASE_GADGET(Class) \ KITINERARY_GADGET(Class) \ protected: \ Class(Class ## Private *dd); \ private: #define KITINERARY_PROPERTY(Type, Name, SetName) \ Q_PROPERTY(Type Name READ Name WRITE SetName STORED true) \ public: \ Type Name() const; \ void SetName(KItinerary::detail::parameter_type::type v); \ private: +///@endcond + #endif diff --git a/src/datatypes/datatypes_p.h b/src/datatypes/datatypes_p.h index 2ab3dde..03b1eb6 100644 --- a/src/datatypes/datatypes_p.h +++ b/src/datatypes/datatypes_p.h @@ -1,89 +1,84 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KITINERARY_DATATYPES_P_H #define KITINERARY_DATATYPES_P_H #include #include #include namespace KItinerary { #define KITINERARY_PRIVATE_BASE_GADGET(Class) \ public: \ virtual ~ Class ## Private() = default; \ virtual Class ## Private * clone() const { \ return new Class ##Private(*this); \ } \ typedef Class ## Private base_type; \ private: \ #define KITINERARY_PRIVATE_GADGET(Class) \ public: \ inline base_type* clone() const override { \ return new Class ## Private(*this); \ } \ private: - -#define KITINERARY_MAKE_SIMPLE_CLASS(Class) \ -Class::Class() : d(new Class ## Private) {} \ +#define KITINERARY_MAKE_CLASS_IMPL(Class) \ Class::Class(const Class &other) = default; \ Class::~Class() = default; \ Class& Class::operator=(const Class &other) { d = other.d; return *this; } \ QString Class::className() const { return QStringLiteral(#Class); } \ -Class::operator QVariant() const { return QVariant::fromValue(*this); } +Class::operator QVariant() const { return QVariant::fromValue(*this); } \ +static_assert(sizeof(Class) == sizeof(void*), "dptr must be the only member!"); \ + +#define KITINERARY_MAKE_SIMPLE_CLASS(Class) \ +Class::Class() : d(new Class ## Private) {} \ +KITINERARY_MAKE_CLASS_IMPL(Class) #define KITINERARY_MAKE_BASE_CLASS(Class) \ Class::Class() : d(new Class ## Private) {} \ -Class::Class(const Class &other) = default; \ Class::Class(Class ## Private *dd) : d(dd) {} \ -Class::~Class() = default; \ -Class& Class::operator=(const Class &other) { d = other.d; return *this; } \ -QString Class::className() const { return QStringLiteral(#Class); } \ -Class::operator QVariant() const { return QVariant::fromValue(*this); } +KITINERARY_MAKE_CLASS_IMPL(Class) #define KITINERARY_MAKE_SUB_CLASS(Class, Base) \ Class::Class() : Base(new Class ## Private) {} \ -Class::Class(const Class &other) = default; \ -Class::~Class() = default; \ -Class& Class::operator=(const Class &other) { d = other.d; return *this; } \ -QString Class::className() const { return QStringLiteral(#Class); } \ -Class::operator QVariant() const { return QVariant::fromValue(*this); } +KITINERARY_MAKE_CLASS_IMPL(Class) #define K_D(Class) auto d = static_cast(this->d.data()) #define KITINERARY_MAKE_PROPERTY(Class, Type, Name, SetName) \ Type Class::Name() const { return static_cast(d.data())->Name; } \ void Class::SetName(detail::parameter_type::type value) { \ d.detach(); \ static_cast(d.data())->Name = value; \ } static inline QString localizedDateTime(const QDateTime &dt) { auto s = QLocale().toString(dt, QLocale::ShortFormat); if (dt.timeSpec() == Qt::TimeZone || dt.timeSpec() == Qt::OffsetFromUTC) { s += QLatin1Char(' ') + dt.timeZone().abbreviation(dt); } return s; } } #endif diff --git a/src/mergeutil.cpp b/src/mergeutil.cpp index 0069819..313e672 100644 --- a/src/mergeutil.cpp +++ b/src/mergeutil.cpp @@ -1,149 +1,149 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "mergeutil.h" #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs); static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs); static bool isSameLodingBusiness(const LodgingBusiness &lhs, const LodgingBusiness &rhs); bool MergeUtil::isSameReservation(const QVariant& lhs, const QVariant& rhs) { - if (lhs.isNull() || rhs.isNull()) { + if (lhs.isNull() || rhs.isNull() || !JsonLd::canConvert(lhs) || !JsonLd::canConvert(rhs)) { return false; } if (lhs.userType() != rhs.userType()) { return false; } // flight: booking ref, flight number and departure day match if (lhs.userType() == qMetaTypeId()) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber() || lhsRes.reservationNumber().isEmpty()) { return false; } const auto lhsFlight = lhsRes.reservationFor().value(); const auto rhsFlight = rhsRes.reservationFor().value(); if (!isSameFlight(lhsFlight, rhsFlight)) { return false; } } // train: booking ref, train number and depature day match if (lhs.userType() == qMetaTypeId()) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } const auto lhsTrip = lhsRes.reservationFor().value(); const auto rhsTrip = rhsRes.reservationFor().value(); if (!isSameTrainTrip(lhsTrip, rhsTrip)) { return false; } } // bus: booking ref, number and depature time match if (lhs.userType() == qMetaTypeId()) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } const auto lhsTrip = lhsRes.reservationFor().value(); const auto rhsTrip = rhsRes.reservationFor().value(); if (!isSameBusTrip(lhsTrip, rhsTrip)) { return false; } } // hotel: booking ref, checkin day, name match if (lhs.userType() == qMetaTypeId()) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } const auto lhsHotel = lhsRes.reservationFor().value(); const auto rhsHotel = rhsRes.reservationFor().value(); if (!isSameLodingBusiness(lhsHotel, rhsHotel) || lhsRes.checkinTime().date() != rhsRes.checkinTime().date()) { return false; } } // for all: underName either matches or is not set - const auto lhsUN = JsonLdDocument::readProperty(lhs, "underName"); - const auto rhsUN = JsonLdDocument::readProperty(rhs, "underName"); + const auto lhsUN = JsonLd::convert(lhs).underName(); + const auto rhsUN = JsonLd::convert(rhs).underName(); return lhsUN.isNull() || rhsUN.isNull() || isSamePerson(lhsUN.value(), rhsUN.value()); } bool MergeUtil::isSameFlight(const Flight& lhs, const Flight& rhs) { if (lhs.flightNumber().isEmpty() || rhs.flightNumber().isEmpty()) { return false; } return lhs.flightNumber() == rhs.flightNumber() && lhs.airline().iataCode() == rhs.airline().iataCode() && lhs.departureDay() == rhs.departureDay(); } static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs) { if (lhs.trainNumber().isEmpty() || rhs.trainNumber().isEmpty()) { return false; } return lhs.trainName() == rhs.trainName() && lhs.trainNumber() == rhs.trainNumber() && lhs.departureTime().date() == rhs.departureTime().date(); } static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs) { if (lhs.busNumber().isEmpty() || rhs.busNumber().isEmpty()) { return false; } return lhs.busName() == rhs.busName() && lhs.busNumber() == rhs.busNumber() && lhs.departureTime() == rhs.departureTime(); } static bool isSameLodingBusiness(const LodgingBusiness &lhs, const LodgingBusiness &rhs) { if (lhs.name().isEmpty() || rhs.name().isEmpty()) { return false; } return lhs.name() == rhs.name(); } bool MergeUtil::isSamePerson(const Person& lhs, const Person& rhs) { // TODO: extend this once Person has familiy and given name fields return lhs.name().compare(rhs.name(), Qt::CaseInsensitive) == 0; }