This commit is contained in:
2015-05-22 01:46:40 +02:00
parent d63423aee9
commit feff4efc35
63 changed files with 290 additions and 4761 deletions
+10 -125
View File
@@ -1,93 +1,28 @@
cmake_minimum_required(VERSION 2.8.3)
project(hos_cosgi_core)
## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS
hos_celix
roscpp
hos_celix
hos_cpp_utils
hos_deps
)
## System dependencies are found with CMake's conventions
find_package(Boost REQUIRED)
find_package(APR REQUIRED)
find_package(libvariant REQUIRED)
## Uncomment this if the package has a setup.py. This macro ensures
## modules and global scripts declared therein get installed
## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html
# catkin_python_setup()
################################################
## Declare ROS messages, services and actions ##
################################################
## To declare and build messages, services or actions from within this
## package, follow these steps:
## * Let MSG_DEP_SET be the set of packages whose message types you use in
## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...).
## * In the file package.xml:
## * add a build_depend and a run_depend tag for each package in MSG_DEP_SET
## * If MSG_DEP_SET isn't empty the following dependencies might have been
## pulled in transitively but can be declared for certainty nonetheless:
## * add a build_depend tag for "message_generation"
## * add a run_depend tag for "message_runtime"
## * In this file (CMakeLists.txt):
## * add "message_generation" and every package in MSG_DEP_SET to
## find_package(catkin REQUIRED COMPONENTS ...)
## * add "message_runtime" and every package in MSG_DEP_SET to
## catkin_package(CATKIN_DEPENDS ...)
## * uncomment the add_*_files sections below as needed
## and list every .msg/.srv/.action file to be processed
## * uncomment the generate_messages entry below
## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...)
## Generate messages in the 'msg' folder
# add_message_files(
# FILES
# Message1.msg
# Message2.msg
# )
## Generate services in the 'srv' folder
# add_service_files(
# FILES
# Service1.srv
# Service2.srv
# )
## Generate actions in the 'action' folder
# add_action_files(
# FILES
# Action1.action
# Action2.action
# )
## Generate added messages and services with any dependencies listed here
# generate_messages(
# DEPENDENCIES
# std_msgs # Or other packages containing msgs
# )
###################################
## catkin specific configuration ##
###################################
## The catkin_package macro generates cmake config files for your package
## Declare things to be passed to dependent projects
## INCLUDE_DIRS: uncomment this if you package contains header files
## LIBRARIES: libraries you create in this project that dependent projects also need
## CATKIN_DEPENDS: catkin_packages dependent projects also need
## DEPENDS: system dependencies of this project that dependent projects also need
catkin_package(
INCLUDE_DIRS include/${PROJECT_NAME}/
# LIBRARIES hos_cosgi_launcher
CATKIN_DEPENDS hos_celix
# DEPENDS system_lib
)
###########
## Build ##
###########
@@ -96,70 +31,39 @@ add_definitions(
-std=c++11
)
## Specify additional locations of header files
## Your package locations should be listed before other locations
# include_directories(include)
include_directories(
include/${PROJECT_NAME}/
${catkin_INCLUDE_DIRS}
${APR_INCLUDE_DIR}
${APR_INCLUDE_DIR}/apr-1.0
include/${PROJECT_NAME}/
${libvariant_INCLUDE_DIRS}
)
add_executable(hos_cosgi_launcher
src/launcher.cpp
src/hos/cosgi/service/core/core_serviceimpl.cpp
src/hos/cosgi/service/core/CoreServiceImpl.cpp
)
target_link_libraries(hos_cosgi_launcher
${catkin_LIBRARIES}
${APR_LIBRARIES}
${libvariant_LIBRARIES}
)
## Declare a cpp library
# add_library(hos_osgi_launcher
# src/${PROJECT_NAME}/hos_osgi_launcher.cpp
# )
## Declare a cpp executable
# add_executable(hos_osgi_launcher_node src/hos_osgi_launcher_node.cpp)
## Add cmake target dependencies of the executable/library
## as an example, message headers may need to be generated before nodes
# add_dependencies(hos_osgi_launcher_node hos_osgi_launcher_generate_messages_cpp)
## Specify libraries to link a library or executable target against
# target_link_libraries(hos_osgi_launcher_node
# ${catkin_LIBRARIES}
# )
#############
## Install ##
#############
# all install targets should use catkin DESTINATION variables
# See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html
## Mark executable scripts (Python etc.) for installation
## in contrast to setup.py, you can choose the destination
# install(PROGRAMS
# scripts/my_python_script
# DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
# )
## Mark executables and/or libraries for installation
install(TARGETS hos_cosgi_launcher
ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
## Mark cpp header files for installation
install(DIRECTORY include/${PROJECT_NAME}/
DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
FILES_MATCHING PATTERN "*.h"
PATTERN ".svn" EXCLUDE
)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc
@@ -167,26 +71,6 @@ install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc
OPTIONAL
)
## Mark other files for installation (e.g. launch and bag files, etc.)
# install(FILES
# # myfile1
# # myfile2
# DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
# )
#############
## Testing ##
#############
## Add gtest based cpp test target and link libraries
# catkin_add_gtest(${PROJECT_NAME}-test test/test_hos_osgi_launcher.cpp)
# if(TARGET ${PROJECT_NAME}-test)
# target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME})
# endif()
## Add folders to be run by python nosetests
# catkin_add_nosetests(test)
###################
## Documentation ##
@@ -200,3 +84,4 @@ if(DOXYGEN_FOUND)
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
endif(DOXYGEN_FOUND)
@@ -0,0 +1,12 @@
#pragma once
/*
* Celix does not provide C++ ready headers, and seems to be under heavy development.
* Therefore we don't directly include celix but use this file als proxy.
*/
extern "C" {
#include "celix/framework.h"
#include "celix/linked_list_iterator.h"
}
@@ -76,6 +76,15 @@ template <class... _Args> class Observable {};
/**
* Convenience template merging the Service class and the Observable template into a single template.
*/
template<class _Service, class... _Args> class ObservableService
: public virtual Service<_Service>,
public virtual Observable<_Args...> {};
/**
* Implementation of interface::Observable<_Event>.
*
@@ -4,11 +4,13 @@
#include <hos/cosgi/service/Interface.h>
#include <chrono>
// Forward declarations
namespace ros { class NodeHandle; }
namespace hos { namespace utils { class SchemaLoader; } };
@@ -24,24 +26,14 @@ namespace core {
* Base class for events emitted by the CoreService.
*/
class CoreServiceEvent {
public:
/**
* CoreService event types.
*/
typedef std::chrono::system_clock::time_point timestamp_t;
enum class EventType : int { STARTUP_FINISHED = 1, SHUTDOWN = 2 };
/**
* Event type.
*/
const EventType eventType;
const timestamp_t timestamp;
/**
* Constructor.
*/
CoreServiceEvent(EventType type) : eventType(type) {}
CoreServiceEvent(EventType type, const timestamp_t timestamp = std::chrono::system_clock::now())
: eventType(type), timestamp(timestamp) {}
};
@@ -116,6 +108,13 @@ public:
*/
virtual void requestShutdown() = 0;
/**
* Returns a reference to the schema loader.
*
* \return schema loader.
*/
virtual utils::SchemaLoader& getSchemaLoader() = 0;
};
@@ -1,34 +1,30 @@
/*
* hos_core_service_impl.cpp
*
* Created on: 05.04.2015
* Author: matzman
*/
#include <ros/ros.h>
#include "core_serviceimpl.h"
#include "CoreServiceImpl.h"
// Service Namespace
namespace hos {
namespace cosgi{
namespace service {
namespace core {
// Service information
template<> const char* implementation::ServiceInfo<CoreServiceImpl>::name = service::core::CoreService::SERVICE_NAME;
template<> const int implementation::ServiceInfo<CoreServiceImpl>::major = 0;
template<> const int implementation::ServiceInfo<CoreServiceImpl>::minor = 3;
template<> const int implementation::ServiceInfo<CoreServiceImpl>::major = 1;
template<> const int implementation::ServiceInfo<CoreServiceImpl>::minor = 0;
CoreServiceImpl::CoreServiceImpl(ros::NodeHandle& publicNodeHandle, ros::NodeHandle& privateNodeHandle)
: publicNodeHandle(publicNodeHandle), privateNodeHandle(privateNodeHandle) {}
ros::NodeHandle& CoreServiceImpl::getPublicNodeHandle() {
return publicNodeHandle;
}
ros::NodeHandle& CoreServiceImpl::getPrivateNodeHandle() {
return privateNodeHandle;
}
void CoreServiceImpl::requestShutdown() {
ROS_INFO("Received shutdown request");
@@ -36,6 +32,7 @@ void CoreServiceImpl::requestShutdown() {
}
} // namespace core
} // namespace service
} // namespace cosgi
@@ -0,0 +1,85 @@
#pragma once
#include <hos/cosgi/service/core/CoreService.h>
#include <hos/cosgi/service/Implementation.h>
#include <hos/utils/SchemaLoader.h>
#include <memory>
// Service Namespace
namespace hos {
namespace cosgi{
namespace service {
namespace core {
/**
* Implementation of CoreService.
*/
class CoreServiceImpl
: public virtual CoreService,
public virtual implementation::ObservableService<CoreServiceImpl, CoreServiceEvent> {
private:
ros::NodeHandle publicNodeHandle;
ros::NodeHandle privateNodeHandle;
std::unique_ptr<utils::SchemaLoader> schemaLoader;
public:
/**
* Constructor.
*/
CoreServiceImpl(ros::NodeHandle& publicNodeHandle, ros::NodeHandle& privateNodeHandle);
////////////////////////////////////////////////////////////
////////////// Public Service Implementations //////////////
////////////////////////////////////////////////////////////
/**
* Returns a reference to a node handle with public namespace.
*
* \return public node handle
*/
virtual ros::NodeHandle& getPublicNodeHandle() override {
return publicNodeHandle;
}
/**
* Returns a reference to a node handle with private namespace.
*
* \return private node handle
*/
virtual ros::NodeHandle& getPrivateNodeHandle() override {
return privateNodeHandle;
}
/**
* Request an application shutdown
*/
virtual void requestShutdown() override;
/**
* Returns a reference to the schema loader.
*
* \return schema loader.
*/
virtual utils::SchemaLoader& getSchemaLoader() override {
return *schemaLoader;
}
};
} // namespace core
} // namespace service
} // namespace cosgi
} // namespace hos
@@ -1,42 +0,0 @@
/*
* hos_core_service_private.h
*
* Created on: 05.04.2015
* Author: matzman
*/
#ifndef SRC_CORE_SERVICEIMPL_H_
#define SRC_CORE_SERVICEIMPL_H_
#include <hos/cosgi/service/core/CoreService.h>
#include <hos/cosgi/service/Implementation.h>
// Service Namespace
namespace hos {
namespace cosgi{
namespace service {
namespace core {
class CoreServiceImpl;
class CoreServiceImpl : public virtual CoreService, public virtual implementation::Service<CoreServiceImpl>, public virtual implementation::Observable<CoreServiceEvent> {
private:
ros::NodeHandle publicNodeHandle;
ros::NodeHandle privateNodeHandle;
public:
CoreServiceImpl(ros::NodeHandle& publicNodeHandle, ros::NodeHandle& privateNodeHandle);
virtual ros::NodeHandle& getPublicNodeHandle();
virtual ros::NodeHandle& getPrivateNodeHandle();
virtual void requestShutdown();
};
} // namespace core
} // namespace service
} // namespace cosgi
} // namespace hos
#endif /* SRC_CORE_SERVICEIMPL_H_ */
+38 -23
View File
@@ -1,53 +1,64 @@
// std
#include <signal.h>
#include <cstdlib>
#include <string>
#include <sstream>
#include <exception>
// Boost
#include <boost/format.hpp>
// ROS
#include "ros/ros.h"
// APR
#include <apr_general.h>
#include <apr_strings.h>
#include "hos/cosgi/service/core/core_serviceimpl.h"
extern "C" { // celix does not provide C++ ready headers
#include "celix/framework.h"
#include "celix/linked_list_iterator.h"
}
#include <exception>
#include <sstream>
#include <boost/format.hpp>
#include <signal.h>
#include <string>
// Celix
#include <hos/cosgi/celix.h>
// CoreService
#include "hos/cosgi/service/core/CoreServiceImpl.h"
using namespace hos::cosgi::service::core;
// forward declaration of signal handler
std::function<void(int)> signalHandler;
/**
* hos_cosgi_launcher entry point.
*
* Command line argument parsing is handled by ROS.
*/
int main(int argc, char **argv) {
// Public Parameters
int loop_frequency = 100;
std::string autostart_bundles;
std::string config_properties;
// Internal State
bool apr_initialized = false;
apr_pool_t* memoryPool = nullptr;
framework_pt framework = nullptr;
bundle_pt fwBundle = nullptr;
properties_pt properties = nullptr;
int loop_frequency = 100;
std::string autostart_bundles;
try {
// Initialize ROS
ros::init(argc, argv, "hos_osgi_launcher", ros::init_options::AnonymousName | ros::init_options::NoSigintHandler);
// Create public node-handle
ros::NodeHandle rosPublicNodeHandle;
// Create private node-handle
ros::NodeHandle rosPrivateNodeHandle("~");
if (!ros::ok()) {
throw std::runtime_error(boost::format("Could not initialize ROS.").str());
}
ROS_INFO("ROS initialized");
// Retrieve ROS parameter
// Retrieve public parameters
rosPrivateNodeHandle.getParam("loop_frequency", loop_frequency);
rosPrivateNodeHandle.getParam("autostart_bundles", autostart_bundles);
rosPrivateNodeHandle.getParam("config_properties", config_properties);
// Initialize APR and APR memory pool
apr_status_t astatus = apr_initialize();
@@ -64,12 +75,10 @@ int main(int argc, char **argv) {
// Create celix properties
properties = properties_create();
//properties = properties_load((char*)"config.properties");
if (properties == nullptr) {
throw std::runtime_error(boost::format("Could not create celix properties.").str());
}
properties_store(properties, (char*)"config.properties", nullptr);
properties_store(properties, (char*)config_properties.c_str(), nullptr);
// Create and initialize celix framework
celix_status_t cstatus = framework_create(&framework, memoryPool, properties);
@@ -183,17 +192,23 @@ int main(int argc, char **argv) {
hosCoreService.notifyObservers(&event);
}
// main loop
ROS_INFO("Entering main loop");
// Install signal handler
signalHandler = [&](int signum) {
std::cerr << "Caught SIGINT, exiting..." << std::endl;
hosCoreService.requestShutdown();
};
signal(SIGINT, [](int signum) { signalHandler(signum); });
// main loop
ROS_INFO("Entering main loop");
ros::Rate loop_rate(loop_frequency);
while (ros::ok()) {
ros::spinOnce();
loop_rate.sleep();
}
{
ShutdownEvent event;
hosCoreService.notifyObservers(&event);
}
ROS_INFO("Exiting main loop");
} catch (std::exception &ex) {
@@ -6,6 +6,7 @@
#include <Variant/Variant.h>
#include <string>
#include <set>
#include <chrono>
#include <vector>
#include <memory>
@@ -17,76 +18,106 @@ namespace cosgi {
namespace service {
namespace devicerepository {
// Forward declarations
class Device;
using namespace ::libvariant;
using namespace libvariant;
/**
* Base class for device events
**/
* Base class for device events.
*/
class DeviceEvent {
public:
typedef std::chrono::system_clock::time_point timestamp_t;
enum class EventType : int { DEVICE_ADDED = 1, DEVICE_REMOVED = 2, STATUS_UPDATED = 3, COMMAND_RECEIVED = 4 };
const EventType eventType;
Device* const device;
DeviceEvent(Device* device, EventType type) : device(device), eventType(type) {}
const timestamp_t timestamp;
const EventType eventType;
DeviceEvent(Device* device, EventType type, const timestamp_t timestamp)
: device(device), timestamp(timestamp), eventType(type) {}
};
/**
* Device added
**/
class DeviceAddedEvent : public DeviceEvent {
* Device added event.
*/
class DeviceAddedEvent
: public DeviceEvent {
public:
DeviceAddedEvent(Device* device) : DeviceEvent(device, DeviceEvent::EventType::DEVICE_ADDED) {}
DeviceAddedEvent(Device* device, const timestamp_t timestamp)
: DeviceEvent(device, DeviceEvent::EventType::DEVICE_ADDED, timestamp) {}
};
/**
* Device removed
**/
class DeviceRemovedEvent : public DeviceEvent {
public:
DeviceRemovedEvent(Device* device) : DeviceEvent(device, DeviceEvent::EventType::DEVICE_REMOVED) {}
};
/**
* Device status message received
**/
class DeviceStatusMsgEvent : public DeviceEvent {
* Device removed event.
*/
class DeviceRemovedEvent
: public DeviceEvent {
public:
DeviceStatusMsgEvent(Device* device, const Variant statusMsg)
: DeviceEvent(device, DeviceEvent::EventType::STATUS_UPDATED), statusMsg(statusMsg) {}
DeviceRemovedEvent(Device* device, const timestamp_t timestamp)
: DeviceEvent(device, DeviceEvent::EventType::DEVICE_REMOVED, timestamp) {}
};
/**
* Device status message received event.
*/
class DeviceStatusMsgEvent
: public DeviceEvent {
public:
DeviceStatusMsgEvent(Device* device, const Variant statusMsg, const timestamp_t timestamp)
: DeviceEvent(device, DeviceEvent::EventType::STATUS_UPDATED, timestamp), statusMsg(statusMsg) {}
const Variant statusMsg;
};
/**
* Device command message received
**/
class DeviceCmdMsgEvent : public DeviceEvent {
* Device command message received event.
*/
class DeviceCmdMsgEvent
: public DeviceEvent {
public:
DeviceCmdMsgEvent(Device* device, const Variant cmdMsg)
: DeviceEvent(device, DeviceEvent::EventType::COMMAND_RECEIVED), cmdMsg(cmdMsg) {}
DeviceCmdMsgEvent(Device* device, const Variant cmdMsg, const timestamp_t timestamp)
: DeviceEvent(device, DeviceEvent::EventType::COMMAND_RECEIVED, timestamp), cmdMsg(cmdMsg) {}
const Variant cmdMsg;
};
/**
* Public interface of an device object
**/
class Device : public virtual interface::ObservableObject<DeviceEvent> {
* Public COSGI interface of an device object.
*/
class Device
: public virtual interface::ObservableObject<DeviceEvent> {
public:
typedef std::chrono::system_clock::time_point timestamp_t;
virtual const std::string& getDeviceID() = 0;
virtual const std::string& getDeviceType() = 0;
virtual const std::string& getCmdTopic() = 0;
virtual const std::string& getStatusTopic() = 0;
virtual const Variant& getCurrentState() = 0;
virtual const timestamp_t& getCurrentStateTimestamp() = 0;
virtual const Variant& getLastState() = 0;
virtual const timestamp_t& getLastStateTimestamp() = 0;
virtual void sendCommand(const Variant cmd) = 0;
virtual bool stateChanged() = 0;
};
@@ -94,7 +125,8 @@ public:
/**
* Public Interface of the device repository service
**/
class DeviceRepositoryService : public virtual interface::ObservableService<DeviceEvent> {
class DeviceRepositoryService
: public virtual interface::ObservableService<DeviceEvent> {
public:
static constexpr const char* SERVICE_NAME = "hos_devicerepository_service";
@@ -32,17 +32,17 @@ DeviceImpl::DeviceImpl(DeviceRepositoryServiceImpl* parent, std::string deviceID
std::cout << "GenericDeviceImpl(" << deviceID << ", " << type << ", " << statusTopic << ", " << cmdTopic << ")" << std::endl;
}
void DeviceImpl::handleStatusMsg(Variant& msg) {
void DeviceImpl::handleStatusMsg(Variant& msg, const timestamp_t& stamp) {
std::lock_guard<std::mutex> lock(objMutex);
lastState = currentState.Copy();
currentState.Merge(msg);
DeviceStatusMsgEvent event(this, msg);
DeviceStatusMsgEvent event(this, msg, stamp);
notifyObservers(&event);
}
void DeviceImpl::handleCommandMsg(Variant& msg) {
void DeviceImpl::handleCommandMsg(Variant& msg, const timestamp_t& stamp) {
std::lock_guard<std::mutex> lock(objMutex);
DeviceCmdMsgEvent event(this, msg);
DeviceCmdMsgEvent event(this, msg, stamp);
notifyObservers(&event);
}
@@ -51,10 +51,6 @@ void DeviceImpl::sendCommand(const Variant cmd) {
parent->sendDeviceCommand(this, cmd);
}
bool DeviceImpl::stateChanged() {
return currentState == lastState;
}
void DeviceRepositoryServiceImpl::setCoreService(service::core::CoreService* coreService) {
if (coreService && !this->coreService) {
this->coreService = coreService;
@@ -150,7 +146,7 @@ void DeviceRepositoryServiceImpl::addDevice(std::string name, XmlRpc::XmlRpcValu
subscripedTopics[dev->getCmdTopic()] = entry2;
// send device added event
DeviceAddedEvent event(dev.get());
DeviceAddedEvent event(dev.get(), std::chrono::system_clock::now());
notifyObservers(&event);
}
@@ -159,7 +155,7 @@ void DeviceRepositoryServiceImpl::removeDevice(std::string name) {
std::lock_guard<std::recursive_mutex> lock(serviceMutex);
auto& dev = devices[name];
if (dev) {
DeviceRemovedEvent event(dev.get());
DeviceRemovedEvent event(dev.get(), std::chrono::system_clock::now());
notifyObservers(&event);
dev->notifyObservers(&event);
devices.erase(name);
@@ -205,8 +201,9 @@ void DeviceRepositoryServiceImpl::statusMsgCallback(const hos_core_msgs::DeviceS
if (!sr.Error()) {
std::shared_ptr<DeviceImpl> dev = std::static_pointer_cast<DeviceImpl>(devices[msg->device_id]);
if (dev) {
dev->handleStatusMsg(m);
DeviceStatusMsgEvent event(dev.get(), m);
auto stamp = std::chrono::system_clock::now();
dev->handleStatusMsg(m, stamp);
DeviceStatusMsgEvent event(dev.get(), m, stamp);
notifyObservers(&event);
}
} else {
@@ -219,12 +216,14 @@ void DeviceRepositoryServiceImpl::cmdMsgCallback(const hos_core_msgs::DeviceComm
std::cout << "HOSDeviceRepositoryServiceImpl::cmdMsgCallback" << std::endl;
Variant s = schemaLoader->GetSchema(std::string("hos:schema://").append(msg->device_type).append("/command"));
Variant m = Deserialize(msg->msg_body, SERIALIZE_JSON);
auto test = msg->stamp;
SchemaResult sr = SchemaValidate(s, m, schemaLoader.get());
if (!sr.Error()) {
std::shared_ptr<DeviceImpl> dev = std::static_pointer_cast<DeviceImpl>(devices[msg->device_id]);
if (dev != nullptr) {
dev->handleCommandMsg(m);
DeviceCmdMsgEvent event(dev.get(), m);
auto stamp = std::chrono::system_clock::now();
dev->handleCommandMsg(m, stamp);
DeviceCmdMsgEvent event(dev.get(), m, stamp);
notifyObservers(&event);
}
} else {
@@ -62,7 +62,9 @@ private:
std::string statusTopic;
std::string cmdTopic;
Variant currentState;
timestamp_t currentStamp;
Variant lastState;
timestamp_t lastStamp;
public:
DeviceImpl(DeviceRepositoryServiceImpl* parent, std::string deviceID, std::string deviceType, std::string statusTopic, std::string cmdTopic);
@@ -75,17 +77,16 @@ public:
virtual const std::string& getStatusTopic() override { return statusTopic; }
virtual const Variant& getCurrentState() override { return currentState; }
virtual const timestamp_t& getCurrentStateTimestamp() { return currentStamp; }
virtual const Variant& getLastState() override { return lastState; }
virtual const timestamp_t& getLastStateTimestamp() { return lastStamp; }
virtual void sendCommand(const Variant cmd) override;
virtual bool stateChanged() override;
/* Internal Stuff */
void handleStatusMsg(Variant& msg);
void handleCommandMsg(Variant& msg);
void sendDeviceCommand(Device* device, Variant& cmd);
void handleStatusMsg(Variant& msg, const timestamp_t& stamp);
void handleCommandMsg(Variant& msg, const timestamp_t& stamp);
};
@@ -13,7 +13,7 @@ namespace hos {
namespace utils {
class SchemaLoader: public libvariant::SchemaLoader {
private:
std::string ros_param_ns;
@@ -20,4 +20,4 @@ find_library(libmicrohttpd_LIBRARY microhttpd
)
set(libhttpserver_INCLUDE_DIRS "${libhttpserver_INCLUDE_DIR}" "${libmicrohttpd_INCLUDE_DIR}")
set(libhttpserver_LIBRARIES "${libhttpserver_LIBRARY}" "${libmicrohttpd_LIBRARY}")
set(libhttpserver_LIBRARIES "${libhttpserver_LIBRARY}" "${libmicrohttpd_LIBRARY}")
@@ -8,3 +8,6 @@ find_library(libvariant_LIBRARY Variant
PATHS $ENV{HOS_PREFIX}/libvariant
PATH_SUFFIXES lib lib64
)
set(libvariant_INCLUDE_DIRS "${libvariant_INCLUDE_DIR}")
set(libvariant_LIBRARIES "${libvariant_LIBRARY}")
+3 -2
View File
@@ -6,7 +6,7 @@ project(hos_dummy)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS
hos_core_msgs
hos_python_json
hos_python_utils
rospy
)
@@ -82,7 +82,7 @@ catkin_python_setup()
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES hos_dummy
CATKIN_DEPENDS hos_core_msgs hos_python_json rospy
CATKIN_DEPENDS hos_core_msgs hos_python_utils rospy
# DEPENDS system_lib
)
@@ -122,6 +122,7 @@ include_directories(
#And this will install python nodes
catkin_install_python(PROGRAMS
nodes/hos_dummy_devices.py
nodes/hos_dummy_controller.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})
# all install targets should use catkin DESTINATION variables
+2 -2
View File
@@ -41,10 +41,10 @@
<!-- <test_depend>gtest</test_depend> -->
<buildtool_depend>catkin</buildtool_depend>
<build_depend>hos_core_msgs</build_depend>
<build_depend>hos_python_json</build_depend>
<build_depend>hos_python_utils</build_depend>
<build_depend>rospy</build_depend>
<run_depend>hos_core_msgs</run_depend>
<run_depend>hos_python_json</run_depend>
<run_depend>hos_python_utils</run_depend>
<run_depend>rospy</run_depend>
@@ -8,7 +8,7 @@ from hos_pyhue import pyhue
from hos_core_msgs.msg import DeviceCommand
from hos_core_msgs.msg import DeviceStatusUpdate
from hos_core_msgs.msg import HosStatusUpdate
from jsonspec_hos.reference.providers import HOSParamServerProvider
from hos.utils.schemaloader import SchemaLoader
from jsonspec.validators import load
from time import sleep
@@ -55,7 +55,7 @@ class HosDummyController(tk.Frame):
rospy.set_param(key_base + "/cmd_topic", self.cmd_topic)
print "Added dummy dimmer \"" + name + "\""
# Create JSON schema provider
self.schemaProvider = HOSParamServerProvider()
self.schemaProvider = SchemaLoader()
# Create message publisher
self.commandPublisher = rospy.Publisher(self.cmd_topic, DeviceCommand, queue_size=10)
# Init GUI
@@ -8,7 +8,7 @@ from hos_pyhue import pyhue
from hos_core_msgs.msg import DeviceCommand
from hos_core_msgs.msg import DeviceStatusUpdate
from hos_core_msgs.msg import HosStatusUpdate
from jsonspec_hos.reference.providers import HOSParamServerProvider
from hos.utils.schemaloader import SchemaLoader
from jsonspec.validators import load
from time import sleep
@@ -58,7 +58,7 @@ class HosDummyDevices(tk.Frame):
print "Added dummy dimmer \"" + name + "\""
new_devices.append(name)
# Create JSON schema provider
self.schemaProvider = HOSParamServerProvider()
self.schemaProvider = SchemaLoader()
# Create message publisher
self.statusPublisher = rospy.Publisher(self.status_topic, DeviceStatusUpdate, queue_size=10)
self.hosUpdatePublisher = rospy.Publisher(self.hos_update_topic, HosStatusUpdate, queue_size=10)
+2 -2
View File
@@ -7,7 +7,7 @@ project(hos_philips_hue)
find_package(catkin REQUIRED COMPONENTS
hos_core_msgs
rospy
hos_python_json
hos_python_utils
)
## System dependencies are found with CMake's conventions
@@ -82,7 +82,7 @@ catkin_python_setup()
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES hos_philips_hue
CATKIN_DEPENDS hos_core_msgs rospy hos_python_json
CATKIN_DEPENDS hos_core_msgs rospy hos_python_utils
# DEPENDS system_lib
)
+2 -2
View File
@@ -7,7 +7,7 @@ import rospy
import json
import traceback
from hos_core_msgs.msg import DeviceCommand
from jsonspec_hos.reference.providers import HOSParamServerProvider
from hos.utils.schemaloader import SchemaLoader
from jsonspec.validators import load
from time import sleep
@@ -18,7 +18,7 @@ if __name__ == '__main__':
rospy.init_node('test', argv=sys.argv, anonymous=True)
cmdPublisher = rospy.Publisher("/hos/devices/cmd", DeviceCommand, queue_size=10)
schemaProvider = HOSParamServerProvider()
schemaProvider = SchemaLoader()
stamp = rospy.get_rostime()
msg = DeviceCommand()
+2 -2
View File
@@ -42,10 +42,10 @@
<buildtool_depend>catkin</buildtool_depend>
<build_depend>hos_core_msgs</build_depend>
<build_depend>rospy</build_depend>
<build_depend>hos_python_json</build_depend>
<build_depend>hos_python_utils</build_depend>
<run_depend>hos_core_msgs</run_depend>
<run_depend>rospy</run_depend>
<run_depend>hos_python_json</run_depend>
<run_depend>hos_python_utils</run_depend>
<!-- The export tag contains other, unspecified, tags -->
@@ -10,7 +10,7 @@ from hos_pyhue import pyhue
from hos_core_msgs.msg import DeviceCommand
from hos_core_msgs.msg import DeviceStatusUpdate
from hos_core_msgs.msg import HosStatusUpdate
from jsonspec_hos.reference.providers import HOSParamServerProvider
from hos.utils.schemaloader import SchemaLoader
from jsonspec.validators import load
from curses.has_key import has_key
from time import sleep
@@ -55,7 +55,7 @@ class HueController(object):
rospy.set_param(key_base + "/status_topic", self.status_topic)
rospy.set_param(key_base + "/cmd_topic", self.cmd_topic)
# Create JSON schema provider
self.schemaProvider = HOSParamServerProvider()
self.schemaProvider = SchemaLoader()
# Create message publisher
self.statusPublisher = rospy.Publisher(self.status_topic, DeviceStatusUpdate, queue_size=10)
self.hosUpdatePublisher = rospy.Publisher(self.hos_update_topic, HosStatusUpdate, queue_size=10)
@@ -1,5 +0,0 @@
"""
jsonspec
~~~~~~~~
"""
@@ -1,508 +0,0 @@
"""
jsonspec.cli
~~~~~~~~~~~~
"""
from __future__ import print_function
import argparse
import logging
import os
import stat
import sys
import pkg_resources
from functools import wraps
from jsonspec import driver
from textwrap import dedent
try:
from termcolor import colored
except ImportError:
def colored(string, *args, **kwargs):
return string
def disable_logging(func):
return func
"""
Temporary disable logging.
"""
handler = logging.NullHandler()
@wraps(func)
def wrapper(*args, **kwargs):
logger = logging.getLogger()
logger.addHandler(handler)
resp = func(*args, **kwargs)
logger.removeHandler(handler)
return resp
return wrapper
def format_output(func):
return func
"""
Format output.
"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
response = func(*args, **kwargs)
except Exception as error:
print(colored(error, 'red'), file=sys.stderr)
sys.exit(1)
else:
print(response)
sys.exit(0)
return wrapper
def JSONStruct(string):
return driver.loads(string)
class JSONFile(argparse.FileType):
def __call__(self, string):
file = super(JSONFile, self).__call__(string)
return driver.load(file)
def document_arguments(parser):
group = parser.add_mutually_exclusive_group()
group.add_argument('--document-json', type=JSONStruct, help='json structure', dest='document_json', metavar='<doc>')
group.add_argument('--document-file', type=JSONFile('r'), help='json filename', dest='document_file', metavar='<doc>')
def schema_arguments(parser):
group = parser.add_mutually_exclusive_group()
group.add_argument('--schema-json', type=JSONStruct, help='json structure', dest='schema_json', metavar='<schema>')
group.add_argument('--schema-file', type=JSONFile('r'), help='json filename', dest='schema_file', metavar='<schema>')
def fragment_arguments(parser):
group = parser.add_mutually_exclusive_group()
group.add_argument('--fragment-json', type=JSONStruct, help='json structure', dest='fragment_json', metavar='<fragment>')
group.add_argument('--fragment-file', type=JSONFile('r'), help='json filename', dest='fragment_file', metavar='<fragment>')
def indentation_arguments(parser):
parser.add_argument('--indent', type=int, help='return an indented json', metavar='<indentation>')
def pointer_arguments(parser):
parser.add_argument('pointer', type=str, help='json pointer', metavar='<pointer>')
def target_arguments(parser):
parser.add_argument('-t', '--target-pointer', help='target pointer', dest='target', metavar='<target>')
def parse_document(args):
document = None
if args.document_json:
document = args.document_json
elif args.document_file:
document = args.document_file
else:
mode = os.fstat(0).st_mode
if stat.S_ISFIFO(mode):
# cat doc.json | cmd
document = driver.load(sys.stdin)
elif stat.S_ISREG(mode):
# cmd < doc.json
document = driver.load(sys.stdin)
setattr(args, 'document', document)
return args
def parse_fragment(args):
setattr(args, 'fragment', args.fragment_json or args.fragment_file)
return args
def parse_pointer(args):
target = args.pointer
if target.startswith('#'):
target = target[1:]
setattr(args, 'pointer', target)
def parse_schema(args):
setattr(args, 'schema', args.schema_json or args.schema_file)
return args
def parse_target(args):
target = args.target
if target.startswith('#'):
target = target[1:]
setattr(args, 'target', target)
if not target:
raise ValueError('target is required')
class Command(object):
help = None
description = None
epilog = None
def __init__(self, parser=None):
self.parser = parser or argparse.ArgumentParser(description=self.description,
epilog=self.epilog)
self.parser.set_defaults(func=self)
self.arguments(self.parser)
def arguments(self, parser):
return
def run(self, args=None):
raise NotImplementedError
def parse_args(self, args):
return self.parser.parse_args(args)
@disable_logging
@format_output
def __call__(self, args=None):
return self.run(args)
class AddCommand(Command):
"""Add a fragment to a json document.
examples::
%(prog)s '#/foo/1' --fragment-file=fragment.json --document-json='{"foo": ["bar", "baz"]}'
echo '{"foo": ["bar", "baz"]}' | %(prog)s '#/foo/1' --fragment-file=fragment.json
%(prog)s '#/foo/1' --fragment-file=fragment.json --document-file=doc.json
%(prog)s '#/foo/1' --fragment-file=fragment.json < doc.json
"""
help = 'add fragment to a document'
def arguments(self, parser):
pointer_arguments(parser)
document_arguments(parser)
fragment_arguments(parser)
indentation_arguments(parser)
def run(self, args):
parse_pointer(args)
parse_document(args)
parse_fragment(args)
from jsonspec.operations import add, Error
from jsonspec.pointer import ParseError
try:
response = add(args.document, args.pointer, args.fragment)
return driver.dumps(response, indent=args.indent)
except Error as error:
raise Exception(error)
except ParseError as error:
raise Exception('{} is not a valid pointer'.format(args.pointer))
class CheckCommand(Command):
"""Tests that a value at the target location is equal to a specified value.
examples::
%(prog)s '#/foo/1' --fragment-file=fragment.json --document-json='{"foo": ["bar", "baz"]}'
echo '{"foo": ["bar", "baz"]}' | %(prog)s '#/foo/1' --fragment-file=fragment.json
%(prog)s '#/foo/1' --fragment-file=fragment.json --document-file=doc.json
%(prog)s '#/foo/1' --fragment-file=fragment.json < doc.json
"""
help = 'check member of a document'
def arguments(self, parser):
pointer_arguments(parser)
document_arguments(parser)
fragment_arguments(parser)
def run(self, args):
parse_pointer(args)
parse_document(args)
parse_fragment(args)
from jsonspec.operations import check, Error
from jsonspec.pointer import ParseError
try:
if check(args.document, args.pointer, args.fragment):
return 'It validates'
else:
raise Exception('It does not validate')
except Error as error:
raise Exception('It does not validate')
except ParseError as error:
raise Exception('{} is not a valid pointer'.format(args.pointer))
class CopyCommand(Command):
"""Copies the value at a specified location to the target location.
examples::
%(prog)s '#/foo/1' --target='#/foo/2' --document-json='{"foo": ["bar", "baz"]}'
echo '{"foo": ["bar", "baz"]}' | %(prog)s '#/foo/1' --target='#/foo/2'
%(prog)s '#/foo/1' --target='#/foo/2' --document-file=doc.json
%(prog)s '#/foo/1' --target='#/foo/2' < doc.json
"""
help = 'copy a member of a document'
def arguments(self, parser):
pointer_arguments(parser)
document_arguments(parser)
target_arguments(parser)
indentation_arguments(parser)
def run(self, args):
parse_pointer(args)
parse_document(args)
parse_target(args)
from jsonspec.operations import copy, Error
from jsonspec.pointer import ParseError
try:
response = copy(args.document, args.target, args.pointer)
return driver.dumps(response, indent=args.indent)
except Error as error:
raise Exception(error)
except ParseError as error:
raise Exception('{} is not a valid pointer'.format(args.pointer))
class ExtractCommand(Command):
"""Extract a fragment from a json document.
examples::
%(prog)s '#/foo/1' --document-json='{"foo": ["bar", "baz"]}'
echo '{"foo": ["bar", "baz"]}' | %(prog)s '#/foo/1'
%(prog)s '#/foo/1' --document-file=doc.json
%(prog)s '#/foo/1' < doc.json
"""
help = 'extract a member of a document'
def arguments(self, parser):
pointer_arguments(parser)
document_arguments(parser)
indentation_arguments(parser)
def run(self, args):
parse_pointer(args)
parse_document(args)
from jsonspec.pointer import extract
from jsonspec.pointer import ExtractError, ParseError
try:
response = extract(args.document, args.pointer)
return driver.dumps(response, indent=args.indent)
except ExtractError:
raise Exception(args)
raise Exception('{} does not match'.format(args.pointer))
except ParseError:
raise Exception('{} is not a valid pointer'.format(args.pointer))
class MoveCommand(Command):
"""Removes the value at a specified location and adds it to the target location.
examples::
%(prog)s '#/foo/2' --target='#/foo/1' --document-json='{"foo": ["bar", "baz"]}'
echo '{"foo": ["bar", "baz"]}' | %(prog)s '#/foo/2' --target='#/foo/1'
%(prog)s '#/foo/2' --target='#/foo/1' --document-file=doc.json
%(prog)s '#/foo/2' --target='#/foo/1' < doc.json
"""
help = 'move a member of a document'
def arguments(self, parser):
pointer_arguments(parser)
document_arguments(parser)
target_arguments(parser)
indentation_arguments(parser)
def run(self, args):
parse_pointer(args)
parse_target(args)
parse_document(args)
from jsonspec.operations import move, Error
from jsonspec.pointer import ParseError
try:
response = move(args.document, args.target, args.pointer)
return driver.dumps(response, indent=args.indent)
except Error as error:
raise Exception(error)
except ParseError as error:
raise Exception('{} is not a valid pointer'.format(args.pointer))
class RemoveCommand(Command):
"""Replace the value of pointer.
examples:
%(prog)s '#/foo/1' --document-json='{"foo": ["bar", "baz"]}'
echo '{"foo": ["bar", "baz"]}' | %(prog)s '#/foo/1'
%(prog)s '#/foo/1' --document-file=doc.json
%(prog)s '#/foo/1' < doc.json
"""
help = 'remove a member of a document'
def arguments(self, parser):
pointer_arguments(parser)
document_arguments(parser)
indentation_arguments(parser)
def run(self, args):
parse_pointer(args)
parse_document(args)
from jsonspec.operations import remove, Error
from jsonspec.pointer import ParseError
try:
response = remove(args.document, args.pointer)
return driver.dumps(response, indent=args.indent)
except Error:
raise Exception('{} does not match'.format(args.pointer))
except ParseError:
raise Exception('{} is not a valid pointer'.format(args.pointer))
class ReplaceCommand(Command):
"""Replace a fragment to a json document.
examples::
%(prog)s '#/foo/1' --fragment-file=fragment.json --document-json='{"foo": ["bar", "baz"]}'
echo '{"foo": ["bar", "baz"]}' | %(prog)s '#/foo/1' --fragment-file=fragment.json
%(prog)s '#/foo/1' --fragment-file=fragment.json --document-file=doc.json
%(prog)s '#/foo/1' --fragment-file=fragment.json < doc.json
"""
help = 'replace a member of a document'
def arguments(self, parser):
pointer_arguments(parser)
document_arguments(parser)
fragment_arguments(parser)
indentation_arguments(parser)
def run(self, args):
parse_pointer(args)
parse_document(args)
parse_fragment(args)
from jsonspec.operations import replace, Error
from jsonspec.pointer import ParseError
try:
response = replace(args.document, args.pointer, args.fragment)
return driver.dumps(response, indent=args.indent)
except Error as error:
raise Exception(error)
except ParseError as error:
raise Exception('{} is not a valid pointer'.format(args.pointer))
class ValidateCommand(Command):
"""Validate document against a schema.
examples::
%(prog)s --schema-file=schema.json --document-json='{"foo": ["bar", "baz"]}'
echo '{"foo": ["bar", "baz"]}' | %(prog)s --schema-file=schema.json
%(prog)s --schema-file=schema.json --document-file=doc.json
%(prog)s --schema-file=schema.json < doc.json
"""
help = 'validate a document against a schema'
def arguments(self, parser):
document_arguments(parser)
schema_arguments(parser)
indentation_arguments(parser)
def run(self, args):
parse_document(args)
parse_schema(args)
from jsonspec.validators import load
from jsonspec.validators import ValidationError
try:
validated = load(args.schema).validate(args.document)
return driver.dumps(validated, indent=args.indent)
except ValidationError as error:
msg = 'document does not validate with schema.\n\n'
for pointer, reasons in error.flatten.items():
msg += ' {}\n'.format(pointer)
for reason in reasons:
msg += ' - reason {}\n'.format(reason)
msg += '\n'
raise Exception(msg)
def get_parser():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter)
subparsers = parser.add_subparsers(help='choose one of these actions',
dest='action',
metavar='<action>')
subparsers.required = True
cmds = []
for entrypoint in pkg_resources.iter_entry_points('jsonspec.cli.commands'):
logging.debug('loaded %s from %s', entrypoint, entrypoint.dist)
cmds.append((entrypoint.name, entrypoint.load()))
for name, command_class in sorted(cmds):
description, help, epilog = None, None, None
if command_class.__doc__:
description, _, epilog = command_class.__doc__.lstrip().partition('\n\n')
if description:
description = description.replace('\n', ' ')
if epilog:
epilog = dedent(epilog).replace(' ', ' ').replace('::\n\n', ':\n')
description = command_class.description or description
epilog = command_class.epilog or epilog
help = command_class.help or description
subparser = subparsers.add_parser(name,
help=help,
description=description,
epilog=epilog,
formatter_class=argparse.RawDescriptionHelpFormatter)
command = command_class(subparser)
return parser
def main():
logging.basicConfig()
parser = get_parser()
args = parser.parse_args()
args.func(args)
if __name__ == '__main__':
main()
@@ -1,35 +0,0 @@
"""
jsonspec.driver
~~~~~~~~~~~~~~~
"""
try:
from simplejson import * # noqa
except ImportError:
from json import * # noqa
from functools import wraps
_load = load
_loads = loads
@wraps(_load)
def load(fp, *args, **kwargs):
return _load(fp, *args, **kwargs)
load.original = _load
@wraps(_loads)
def loads(s, *args, **kwargs):
# if not encoding:
# try:
# encoding = 'utf-16'
# return _loads(s, encoding, *args, **kwargs)
# except UnicodeDecodeError:
# encoding = 'utf-8'
# return _loads(s, encoding, *args, **kwargs)
return _loads(s, *args, **kwargs)
loads.original = _loads
@@ -1,17 +0,0 @@
{
"description": "An Address following the convention of http://microformats.org/wiki/hcard",
"type": "object",
"properties": {
"post-office-box": { "type": "string" },
"extended-address": { "type": "string" },
"street-address": { "type": "string" },
"locality":{ "type": "string", "required": true },
"region": { "type": "string", "required": true },
"postal-code": { "type": "string" },
"country-name": { "type": "string", "required": true}
},
"dependencies": {
"post-office-box": "street-address",
"extended-address": "street-address"
}
}
@@ -1,37 +0,0 @@
{
"description": "A representation of an event",
"type": "object",
"properties": {
"dtstart": {
"format": "date-time",
"type": "string",
"description": "Event starting time",
"required": true
},
"dtend": {
"format": "date-time",
"type": "string",
"description": "Event ending time"
},
"summary": { "type": "string", "required": true },
"location": { "type": "string" },
"url": { "type": "string", "format": "uri" },
"duration": {
"format": "time",
"type": "string",
"description": "Event duration"
},
"rdate": {
"format": "date-time",
"type": "string",
"description": "Recurrence date"
},
"rrule": {
"type": "string",
"description": "Recurrence rule"
},
"category": { "type": "string" },
"description": { "type": "string" },
"geo": { "$ref": "http: //json-schema.org/geo" }
}
}
@@ -1,48 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"description": "A representation of a person, company, organization, or place",
"type": "object",
"properties": {
"fn": {
"description": "Formatted Name",
"type": "string"
},
"familyName": { "type": "string", "required": true },
"givenName": { "type": "string", "required": true },
"additionalName": { "type": "array", "items": { "type": "string" } },
"honorificPrefix": { "type": "array", "items": { "type": "string" } },
"honorificSuffix": { "type": "array", "items": { "type": "string" } },
"nickname": { "type": "string" },
"url": { "type": "string", "format": "uri" },
"email": {
"type": "object",
"properties": {
"type": { "type": "string" },
"value": { "type": "string", "format": "email" }
}
},
"tel": {
"type": "object",
"properties": {
"type": { "type": "string" },
"value": { "type": "string", "format": "phone" }
}
},
"adr": { "$ref": "http://json-schema.org/address" },
"geo": { "$ref": "http://json-schema.org/geo" },
"tz": { "type": "string" },
"photo": { "type": "string" },
"logo": { "type": "string" },
"sound": { "type": "string" },
"bday": { "type": "string", "format": "date" },
"title": { "type": "string" },
"role": { "type": "string" },
"org": {
"type": "object",
"properties": {
"organizationName": { "type": "string" },
"organizationUnit": { "type": "string" }
}
}
}
}
@@ -1,50 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/hyper-schema#",
"extends": {"$ref": "http://json-schema.org/draft-03/schema#"},
"id": "http://json-schema.org/draft-03/hyper-schema#",
"properties": {
"links": {
"type": "array",
"items": {"$ref": "http://json-schema.org/draft-03/links#"}
},
"fragmentResolution": {
"type": "string",
"default": "slash-delimited"
},
"root": {
"type": "boolean",
"default": false
},
"readonly": {
"type": "boolean",
"default": false
},
"contentEncoding": {
"type": "string"
},
"pathStart": {
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"format": "media-type"
}
},
"links": [
{
"href": "{id}",
"rel": "self"
},
{
"href": "{$ref}",
"rel": "full"
},
{
"href": "{$schema}",
"rel": "describedby"
}
],
"fragmentResolution": "slash-delimited"
}
@@ -1,21 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/hyper-schema#",
"id": "http://json-schema.org/draft-03/json-ref#",
"additionalItems": {"$ref": "#"},
"additionalProperties": {"$ref": "#"},
"links": [
{
"href": "{id}",
"rel": "self"
},
{
"href": "{$ref}",
"rel": "full"
},
{
"href": "{$schema}",
"rel": "describedby"
}
],
"fragmentResolution": "dot-delimited"
}
@@ -1,21 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/hyper-schema#",
"id": "http://json-schema.org/draft-03/json-ref#",
"additionalItems": {"$ref": "#"},
"additionalProperties": {"$ref": "#"},
"links": [
{
"href": "{id}",
"rel": "self"
},
{
"href": "{$ref}",
"rel": "full"
},
{
"href": "{$schema}",
"rel": "describedby"
}
],
"fragmentResolution": "dot-delimited"
}
@@ -1,142 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"id": "http://json-schema.org/draft-03/schema#",
"type": "object",
"properties": {
"type": {
"type": ["string", "array"],
"items": {
"type": ["string", {"$ref": "#"}]
},
"uniqueItems": true,
"default": "any"
},
"properties": {
"type": "object",
"additionalProperties": {"$ref": "#"},
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": {"$ref": "#"},
"default": {}
},
"additionalProperties": {
"type": [{"$ref": "#"}, "boolean"],
"default": {}
},
"items": {
"type": [{"$ref": "#"}, "array"],
"items": {"$ref": "#"},
"default": {}
},
"additionalItems": {
"type": [{"$ref": "#"}, "boolean"],
"default": {}
},
"required": {
"type": "boolean",
"default": false
},
"dependencies": {
"type": "object",
"additionalProperties": {
"type": ["string", "array", {"$ref": "#"}],
"items": {
"type": "string"
}
},
"default": {}
},
"minimum": {
"type": "number"
},
"maximum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minItems": {
"type": "integer",
"minimum": 0,
"default": 0
},
"maxItems": {
"type": "integer",
"minimum": 0
},
"uniqueItems": {
"type": "boolean",
"default": false
},
"pattern": {
"type": "string",
"format": "regex"
},
"minLength": {
"type": "integer",
"minimum": 0,
"default": 0
},
"maxLength": {
"type": "integer"
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"default": {
"type": "any"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"format": {
"type": "string"
},
"divisibleBy": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"disallow": {
"type": ["string", "array"],
"items": {
"type": ["string", {"$ref": "#"}]
},
"uniqueItems": true
},
"extends": {
"type": [{"$ref": "#"}, "array"],
"items": {"$ref": "#"},
"default": {}
},
"id": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri"
},
"$schema": {
"type": "string",
"format": "uri"
}
},
"dependencies": {
"exclusiveMinimum": "minimum",
"exclusiveMaximum": "maximum"
},
"default": {}
}
@@ -1,168 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema#",
"id": "http://json-schema.org/draft-04/hyper-schema#",
"title": "JSON Hyper-Schema",
"allOf": [
{
"$ref": "http://json-schema.org/draft-04/schema#"
}
],
"properties": {
"additionalItems": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#"
}
]
},
"additionalProperties": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#"
}
]
},
"dependencies": {
"additionalProperties": {
"anyOf": [
{
"$ref": "#"
},
{
"type": "array"
}
]
}
},
"items": {
"anyOf": [
{
"$ref": "#"
},
{
"$ref": "#/definitions/schemaArray"
}
]
},
"definitions": {
"additionalProperties": {
"$ref": "#"
}
},
"patternProperties": {
"additionalProperties": {
"$ref": "#"
}
},
"properties": {
"additionalProperties": {
"$ref": "#"
}
},
"allOf": {
"$ref": "#/definitions/schemaArray"
},
"anyOf": {
"$ref": "#/definitions/schemaArray"
},
"oneOf": {
"$ref": "#/definitions/schemaArray"
},
"not": {
"$ref": "#"
},
"links": {
"type": "array",
"items": {
"$ref": "#/definitions/linkDescription"
}
},
"fragmentResolution": {
"type": "string"
},
"media": {
"type": "object",
"properties": {
"type": {
"description": "A media type, as described in RFC 2046",
"type": "string"
},
"binaryEncoding": {
"description": "A content encoding scheme, as described in RFC 2045",
"type": "string"
}
}
},
"pathStart": {
"description": "Instances' URIs must start with this value for this schema to apply to them",
"type": "string",
"format": "uri"
}
},
"definitions": {
"schemaArray": {
"type": "array",
"items": {
"$ref": "#"
}
},
"linkDescription": {
"title": "Link Description Object",
"type": "object",
"required": [ "href", "rel" ],
"properties": {
"href": {
"description": "a URI template, as defined by RFC 6570, with the addition of the $, ( and ) characters for pre-processing",
"type": "string"
},
"rel": {
"description": "relation to the target resource of the link",
"type": "string"
},
"title": {
"description": "a title for the link",
"type": "string"
},
"targetSchema": {
"description": "JSON Schema describing the link target",
"$ref": "#"
},
"mediaType": {
"description": "media type (as defined by RFC 2046) describing the link target",
"type": "string"
},
"method": {
"description": "method for requesting the target of the link (e.g. for HTTP this might be \"GET\" or \"DELETE\")",
"type": "string"
},
"encType": {
"description": "The media type in which to submit data along with the request",
"type": "string",
"default": "application/json"
},
"schema": {
"description": "Schema describing the data to submit along with the request",
"$ref": "#"
}
}
}
},
"links": [
{
"rel": "self",
"href": "{+id}"
},
{
"rel": "full",
"href": "{+($ref)}"
}
]
}
@@ -1,150 +0,0 @@
{
"id": "http://json-schema.org/draft-04/schema#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
},
"simpleTypes": {
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uri"
},
"$schema": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"maxLength": { "$ref": "#/definitions/positiveInteger" },
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/positiveInteger" },
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"dependencies": {
"exclusiveMaximum": [ "maximum" ],
"exclusiveMinimum": [ "minimum" ]
},
"default": {}
}
@@ -1,8 +0,0 @@
{
"description": "A geographical coordinate",
"type": "object",
"properties": {
"latitude": { "type": "number" },
"longitude": { "type": "number" }
}
}
@@ -1,98 +0,0 @@
"""
jsonspec.operations
~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
__all__ = ['check', 'remove', 'add', 'replace', 'move', 'copy',
'Error', 'NonexistentTarget', 'Target']
from .exceptions import Error, NonexistentTarget
from .bases import Target
def check(doc, pointer, expected, raise_onerror=False):
"""Check if value exists into object.
:param doc: the document base
:param pointer: the path to search in
:param expected: the expected value
:param raise_onerror: should raise on error?
:return: boolean
"""
return Target(doc).check(pointer, expected, raise_onerror)
def remove(doc, pointer):
"""Remove element from sequence, member from mapping.
:param doc: the document base
:param pointer: the path to search in
:return: the new object
"""
return Target(doc).remove(pointer).document
def add(doc, pointer, value):
"""Add element to sequence, member to mapping.
:param doc: the document base
:param pointer: the path to add in it
:param value: the new value
:return: the new object
"""
return Target(doc).add(pointer, value).document
def replace(doc, pointer, value):
"""Replace element from sequence, member from mapping.
:param doc: the document base
:param pointer: the path to search in
:param value: the new value
:return: the new object
.. note::
This operation is functionally identical to a "remove" operation for
a value, followed immediately by an "add" operation at the same
location with the replacement value.
"""
return Target(doc).replace(pointer, value).document
def move(doc, dest, src):
"""Move element from sequence, member from mapping.
:param doc: the document base
:param dest: the destination
:type dest: Pointer
:param src: the source
:type src: Pointer
:return: the new object
.. note::
it delete then it add to the new location
soo the dest must refer to the middle object.
"""
return Target(doc).move(dest, src).document
def copy(doc, dest, src):
"""Copy element from sequence, member from mapping.
:param doc: the document base
:param dest: the destination
:type dest: Pointer
:param src: the source
:type src: Pointer
:return: the new object
"""
return Target(doc).copy(dest, src).document
@@ -1,202 +0,0 @@
"""
jsonspec.operations.bases
~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
__all__ = ['Target']
from copy import deepcopy
import logging
from jsonspec.pointer import Pointer
from jsonspec.pointer import ExtractError, OutOfBounds, OutOfRange, LastElement
from .exceptions import Error, NonexistentTarget
logger = logging.getLogger(__name__)
class Target(object):
"""
:ivar document: the document base
"""
def __init__(self, document):
self.document = document
def check(self, pointer, expected, raise_onerror=False):
"""Check if value exists into object.
:param pointer: the path to search in
:param expected: the expected value
:param raise_onerror: should raise on error?
:return: boolean
"""
obj = self.document
for token in Pointer(pointer):
try:
obj = token.extract(obj, bypass_ref=True)
except ExtractError as error:
if raise_onerror:
raise Error(*error.args)
logger.exception(error)
return False
return obj == expected
def remove(self, pointer):
"""Remove element from sequence, member from mapping.
:param pointer: the path to search in
:return: resolved document
:rtype: Target
"""
doc = deepcopy(self.document)
parent, obj = None, doc
try:
# fetching
for token in Pointer(pointer):
parent, obj = obj, token.extract(obj, bypass_ref=True)
# removing
if isinstance(parent, dict):
del parent[token]
if isinstance(parent, list):
parent.pop(int(token))
except Exception as error:
raise Error(*error.args)
return Target(doc)
def add(self, pointer, value):
"""Add element to sequence, member to mapping.
:param pointer: the path to add in it
:param value: the new value
:return: resolved document
:rtype: Target
The pointer must reference one of:
- The root of the target document - whereupon the specified value
becomes the entire content of the target document.
- A member to add to an existing mapping - whereupon the supplied
value is added to that mapping at the indicated location. If the
member already exists, it is replaced by the specified value.
- An element to add to an existing sequence - whereupon the supplied
value is added to the sequence at the indicated location.
Any elements at or above the specified index are shifted one
position to the right.
The specified index must no be greater than the number of elements
in the sequence.
If the "-" character is used to index the end of the sequence, this
has the effect of appending the value to the sequence.
"""
doc = deepcopy(self.document)
parent, obj = None, doc
try:
for token in Pointer(pointer):
parent, obj = obj, token.extract(obj, bypass_ref=True)
else:
if isinstance(parent, list):
raise OutOfRange(parent)
if isinstance(parent, dict):
raise OutOfBounds(parent)
raise Error('already setted')
except (OutOfBounds, OutOfRange, LastElement) as error:
if not token.last:
raise NonexistentTarget(obj)
value = deepcopy(value)
if isinstance(error, OutOfBounds):
error.obj[str(token)] = value
elif isinstance(error, OutOfRange):
error.obj.insert(int(token), value)
elif isinstance(error, LastElement):
error.obj.append(value)
return Target(doc)
def replace(self, pointer, value):
"""Replace element from sequence, member from mapping.
:param pointer: the path to search in
:param value: the new value
:return: resolved document
:rtype: Target
"""
doc = deepcopy(self.document)
parent, obj = None, doc
try:
# fetching
for token in Pointer(pointer):
parent, obj = obj, token.extract(obj, bypass_ref=True)
# replace
value = deepcopy(value)
if isinstance(parent, dict):
parent[token] = value
if isinstance(parent, list):
parent[int(token)] = value
except Exception as error:
raise Error(*error.args)
return Target(doc)
def move(self, dest, src):
"""Move element from sequence, member from mapping.
:param dest: the destination
:type dest: Pointer
:param src: the source
:type src: Pointer
:return: resolved document
:rtype: Target
.. note::
This operation is functionally identical to a "remove" operation on
the "from" location, followed immediately by an "add" operation at
the target location with the value that was just removed.
The "from" location MUST NOT be a proper prefix of the "path"
location; i.e., a location cannot be moved into one of its children
"""
doc = deepcopy(self.document)
# delete
parent, fragment = None, doc
for token in Pointer(src):
parent, fragment = fragment, token.extract(fragment,
bypass_ref=True)
if isinstance(parent, dict):
del parent[token]
if isinstance(parent, list):
parent.pop(int(token))
# insert
return Target(doc).add(dest, fragment)
def copy(self, dest, src):
"""Copy element from sequence, member from mapping.
:param dest: the destination
:type dest: Pointer
:param src: the source
:type src: Pointer
:return: resolved document
:rtype: Target
"""
doc = fragment = deepcopy(self.document)
for token in Pointer(src):
fragment = token.extract(fragment, bypass_ref=True)
return Target(doc).add(dest, fragment)
@@ -1,17 +0,0 @@
"""
jsonspec.operations.exceptions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
__all__ = ['Error', 'NonexistentTarget']
class Error(LookupError):
pass
class NonexistentTarget(Error):
"""Raised when trying to get a non existent target"""
pass
@@ -1,33 +0,0 @@
"""
jsonspec.pointer
~~~~~~~~~~~~
JSON Pointer defines a string syntax for identifying a specific value
within a JavaScript Object Notation (JSON) document.
"""
from __future__ import absolute_import, print_function, unicode_literals
__all__ = ['extract', 'stage', 'DocumentPointer', 'Pointer', 'PointerToken',
'ExtractError', 'RefError', 'LastElement', 'OutOfBounds', 'OutOfRange'] # noqa
import logging
from .bases import DocumentPointer, Pointer, PointerToken
from .exceptions import ExtractError, RefError, LastElement, OutOfBounds, OutOfRange, WrongType, UnstagedError, ParseError # noqa
from .stages import stage
logger = logging.getLogger(__name__)
def extract(obj, pointer, bypass_ref=False):
"""Extract member or element of obj according to pointer.
:param obj: the object source
:param pointer: the pointer
:type pointer: Pointer, str
:param bypass_ref: bypass JSON Reference event
:type bypass_ref: boolean
"""
return Pointer(pointer).extract(obj, bypass_ref)
@@ -1,264 +0,0 @@
"""
jsonspec.pointer.bases
~~~~~~~~~~~~~~~~~~~~~~
"""
__all__ = ['DocumentPointer', 'Pointer', 'PointerToken']
import logging
from abc import abstractmethod, ABCMeta
from six import add_metaclass, string_types
from .exceptions import ExtractError, RefError, LastElement, OutOfBounds, OutOfRange, WrongType, UnstagedError, ParseError # noqa
logger = logging.getLogger(__name__)
class DocumentPointer(object):
"""Defines a document pointer
:ivar document: document name
:ivar pointer: pointer
"""
def __init__(self, pointer):
"""
:param pointer: a string or DocumentPointer instance
"""
if isinstance(pointer, DocumentPointer):
document, path = pointer
elif '#' not in pointer:
logger.debug('# is missing %r', pointer)
document, path = pointer, ''
else:
document, path = pointer.split('#', 1)
self.document = document
self.pointer = Pointer(path)
def extract(self, obj, bypass_ref=False):
"""
Extract subelement from obj, according to pointer.
It assums that document is the object.
:param obj: the object source
:param bypass_ref: disable JSON Reference errors
"""
return self.pointer.extract(obj, bypass_ref)
def is_inner(self):
"""Tells if pointer refers to an inner document
"""
return self.document == ''
def endswith(self, txt):
"""used by os.path.join"""
return str(self).endswith(txt)
def __iadd__(self, txt):
"""append fragments"""
data = str(self) + txt
return DocumentPointer(data)
def __iter__(self):
"""Return document and pointer.
"""
return iter([self.document, self.pointer])
def __eq__(self, other):
if isinstance(other, string_types):
return other == self.__str__()
return super(Pointer, self).__eq__(other)
def __str__(self):
return '{}#{}'.format(self.document, self.pointer)
def __repr__(self):
return '<DocumentPointer({})'.format(self)
class Pointer(object):
"""Defines a pointer
:ivar tokens: list of PointerToken
"""
def __init__(self, pointer):
"""
:param pointer: a string or Pointer instance
"""
self.tokens = self.parse(pointer)
if self.tokens:
self.tokens[-1].last = True
def parse(self, pointer):
"""parse pointer into tokens"""
if isinstance(pointer, Pointer):
return pointer.tokens[:]
elif pointer == '':
return []
tokens = []
staged, _, children = pointer.partition('/')
if staged:
try:
token = StagesToken(staged)
token.last = False
tokens.append(token)
except ValueError:
raise ParseError('pointer must start with / or int', pointer)
if _:
for part in children.split('/'):
part = part.replace('~1', '/')
part = part.replace('~0', '~')
token = ChildToken(part)
token.last = False
tokens.append(token)
return tokens
def extract(self, obj, bypass_ref=False):
"""
Extract subelement from obj, according to tokens.
:param obj: the object source
:param bypass_ref: disable JSON Reference errors
"""
for token in self.tokens:
obj = token.extract(obj, bypass_ref)
return obj
def __iter__(self):
"""Walk thru tokens.
"""
return iter(self.tokens)
def __eq__(self, other):
if isinstance(other, string_types):
return other == self.__str__()
return super(Pointer, self).__eq__(other)
def __str__(self):
output = ''
for part in self.tokens:
if isinstance(part, StagesToken):
output += part
continue
part = part.replace('~', '~0')
part = part.replace('/', '~1')
output += '/' + part
return output
def __repr__(self):
return '<{}({!r})>'.format(self.__class__.__name__, self.__str__())
@add_metaclass(ABCMeta)
class PointerToken(str):
"""
A single token
"""
@abstractmethod
def extract(self, obj, bypass_ref=False):
"""
Extract parents or subelement from obj, according to current token.
:param obj: the object source
:param bypass_ref: disable JSON Reference errors
"""
pass
class StagesToken(PointerToken):
"""
A parent token
"""
def __init__(self, value, *args, **kwargs):
value = str(value)
member = False
if value.endswith('#'):
value = value[:-1]
member = True
self.stages = int(value)
self.member = member
def extract(self, obj, bypass_ref=False):
"""
Extract parent of obj, according to current token.
:param obj: the object source
:param bypass_ref: not used
"""
for i in range(0, self.stages):
try:
obj = obj.parent_obj
except AttributeError:
raise UnstagedError(obj, '{!r} must be staged before '
'exploring its parents'.format(obj))
if self.member:
return obj.parent_member
return obj
class ChildToken(PointerToken):
"""
A child token
"""
def extract(self, obj, bypass_ref=False):
"""
Extract subelement from obj, according to current token.
:param obj: the object source
:param bypass_ref: disable JSON Reference errors
"""
try:
if isinstance(obj, dict):
if not bypass_ref and '$ref' in obj:
raise RefError(obj, 'presence of a $ref member')
obj = self.extract_mapping(obj)
elif isinstance(obj, (list, tuple)):
obj = self.extract_sequence(obj)
else:
raise WrongType(obj, '{!r} does not apply '
'for {!r}'.format(str(self), obj))
if isinstance(obj, dict):
if not bypass_ref and '$ref' in obj:
raise RefError(obj, 'presence of a $ref member')
return obj
except ExtractError as error:
logger.exception(error)
raise
except Exception as error:
logger.exception(error)
args = [arg for arg in error.args if arg not in (self, obj)]
raise ExtractError(obj, *args)
def extract_mapping(self, obj):
if self in obj:
return obj[self]
if self.isdigit():
key = int(self)
if key in obj:
return obj[key]
raise OutOfBounds(obj, 'member {!r} not found'.format(str(self)))
def extract_sequence(self, obj):
if self == '-':
raise LastElement(obj, 'last element is needed')
if not self.isdigit():
raise WrongType(obj, '{!r} does not apply '
'for sequence'.format(str(self)))
try:
return obj[int(self)]
except IndexError:
raise OutOfRange(obj, 'element {!r} not found'.format(str(self)))
def __repr__(self):
return '<{}({!r})>'.format(self.__class__.__name__, str(self))
@@ -1,72 +0,0 @@
"""
jsonspec.pointer.exceptions
~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
__all__ = ['ExtractError', 'RefError', 'LastElement', 'OutOfBounds',
'OutOfRange', 'WrongType', 'UnstagedError']
class ParseError(ValueError):
"""Raised when pointer is not well formatted.
:ivar pointer: the faulty pointer
"""
def __init__(self, pointer, *args):
super(ParseError, self).__init__(*args)
self.pointer = pointer
class ExtractError(Exception):
"""Raised for any errors.
:ivar obj: the object that raised this event
"""
def __init__(self, obj, *args):
super(ExtractError, self).__init__(*args)
self.obj = obj
class RefError(ExtractError):
"""Raised when encoutered a JSON Ref.
:ivar obj: the object that raised this event
"""
class WrongType(ExtractError, ValueError):
"""Raised when a member or a sequence is needed.
:ivar obj: the object that raised this event
"""
class OutOfBounds(ExtractError, KeyError):
"""Raised when a member of a mapping does not exists.
:ivar obj: the object that raised this event
"""
class OutOfRange(ExtractError, IndexError):
"""Raised when an element of a sequence does not exists.
:ivar obj: the object that raised this event
"""
class LastElement(ExtractError):
"""Raised when refers to the last element of a sequence.
:ivar obj: the object that raised this event
"""
class UnstagedError(ExtractError, ValueError):
"""Raised when obj is not staged.
:ivar obj: the object that raised this event
"""
@@ -1,67 +0,0 @@
"""
jsonspec.pointer.stages
~~~~~~~~~~~~~~~~~~~~~~~
"""
class Staged(object):
obj = None
parent_obj = None
parent_member = None
def __init__(self, obj, parent=None, member=None):
self.obj = obj
self.parent_obj = parent
self.parent_member = member
def __getattribute__(self, name):
if name in ('obj', 'parent_obj', 'parent_member'):
return object.__getattribute__(self, name)
return getattr(object.__getattribute__(self, 'obj'), name)
def __delattr__(self, name):
delattr(object.__getattribute__(self, 'obj'), name)
def __setattr__(self, name, value):
if name in ('obj', 'parent_obj', 'parent_member'):
object.__setattr__(self, name, value)
else:
setattr(object.__getattribute__(self, 'obj'), name, value)
def __iter__(self):
return object.__getattribute__(self, 'obj').__iter__()
def __getitem__(self, key):
value = object.__getattribute__(self, 'obj').__getitem__(key)
return Staged(value, self, key)
def __len__(self):
return object.__getattribute__(self, 'obj').__len__()
def __eq__(self, other):
return object.__getattribute__(self, 'obj') == other
def __str__(self):
return object.__getattribute__(self, 'obj').__str__()
def stage(obj, parent=None, member=None):
"""
Prepare obj to be staged.
This is almost used for relative JSON Pointers.
"""
obj = Staged(obj, parent, member)
if isinstance(obj, dict):
for key, value in obj.items():
stage(value, obj, key)
elif isinstance(obj, (list, tuple)):
for index, value in enumerate(obj):
stage(value, obj, index)
elif isinstance(obj, set):
for value in obj:
stage(value, obj, None)
return obj
@@ -1,62 +0,0 @@
"""
jsonspec.reference
~~~~~~~~~~~~~~~~~~
A JSON Reference is a JSON object, which contains a member named
"$ref", which has a JSON string value. Example:
{ "$ref": "http://example.com/example.json#/foo/bar" }
If a JSON value does not have these characteristics, then it SHOULD
NOT be interpreted as a JSON Reference.
"""
from __future__ import absolute_import, print_function, unicode_literals
__all__ = ['resolve', 'Registry', 'LocalRegistry', 'NotFound', 'Forbidden']
from .bases import Registry, LocalRegistry
from .exceptions import NotFound, Forbidden
from jsonspec.pointer import DocumentPointer
def resolve(obj, pointer, registry=None):
"""resolve a local object
:param obj: the local object.
:param pointer: the pointer
:type pointer: DocumentPointer, str
:param registry: the registry.
It mays be omited if inner json references
document don't refer to other documents.
:type registry: Provider, dict
.. warning::
Once pointer is extracted, it won't follow sub mapping /element!
For example, the value of::
value = resolve({
'foo': {'$ref': '#/bar'},
'bar': [{'$ref': '#/baz'}],
'baz': 'quux',
}, '#/foo')
is::
assert value == [{'$ref': '#/baz'}]
and not::
assert value == ['quux']
"""
registry = LocalRegistry(obj, registry or {})
local = DocumentPointer(pointer)
if local.document:
registry[local.document] = obj
local.document = '<local>'
return registry.resolve(local)
@@ -1,119 +0,0 @@
"""
jsonspec.reference.bases
~~~~~~~~~~~~~~~~~~~~~~~~
"""
__all__ = ['LocalRegistry', 'Registry']
import logging
from .exceptions import NotFound, Forbidden
from .util import ref, MutableMapping, Mapping
from jsonspec.pointer import DocumentPointer
logger = logging.getLogger(__name__)
class Provider(Mapping):
"""Defines a generic way to provide external documents"""
pass
class Registry(Provider, MutableMapping):
"""Register all documents.
:ivar provider: all documents
:ivar provider: Provider, dict
"""
def __init__(self, provider=None):
self.provider = provider or {}
super(Registry, self).__init__()
def prototype(self, dp):
obj = self[dp.document]
return self[dp.document], LocalRegistry(obj, self)
def resolve(self, pointer):
"""Resolve from documents.
:param pointer: foo
:type pointer: DocumentPointer
"""
dp = DocumentPointer(pointer)
obj, fetcher = self.prototype(dp)
for token in dp.pointer:
obj = token.extract(obj, bypass_ref=True)
reference = ref(obj)
if reference:
obj = fetcher.resolve(reference)
return obj
def __getitem__(self, uri):
try:
return self.provider[uri]
except KeyError:
raise NotFound('{!r} not registered'.format(uri))
def __setitem__(self, uri, obj):
self.provider[uri] = obj
def __delitem__(self, uri):
del self.provider[uri]
def __len__(self):
return len(self.provider)
def __iter__(self):
return iter(self.provider)
class LocalRegistry(Registry):
"""Scoped registry to a local document.
:ivar doc: the local document
:ivar provider: all documents
:ivar provider: Provider, dict
:ivar key: current document identifier
"""
key = '<local>'
def __init__(self, doc, provider=None):
self.doc = doc
self.provider = provider or {}
def prototype(self, dp):
if dp.is_inner():
return self.doc, self
else:
obj = self[dp.document]
return self[dp.document], LocalRegistry(obj, self)
def __getitem__(self, uri):
try:
return self.doc if uri == self.key else self.provider[uri]
except (NotFound, KeyError):
raise NotFound('{!r} not registered'.format(uri))
def __setitem__(self, uri, obj):
if uri == self.key:
raise Forbidden('setting {} is forbidden'.format(self.key))
if uri not in self.provider:
self.provider[uri] = obj
def __delitem__(self, uri):
if uri == self.key:
raise Forbidden('deleting {} is forbidden'.format(self.key))
del self.provider[uri]
def __len__(self):
return len(set(list(self.provider.keys()) + [self.key]))
def __iter__(self):
yield self.key
for key in self.provider.keys():
yield key
@@ -1,15 +0,0 @@
"""
jsonspec.reference.exceptions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
class NotFound(Exception):
"""raises when a document is not found"""
pass
class Forbidden(object):
"""raises when a trying to replace <local> document"""
pass
@@ -1,172 +0,0 @@
"""
jsonspec.reference.providers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
__all__ = ['Provider', 'FilesystemProvider', 'PkgProvider', 'SpecProvider']
import json
import logging
import os
import pkg_resources
from .bases import Provider
from .exceptions import NotFound
from .util import loop
logger = logging.getLogger(__name__)
class PkgProvider(Provider):
"""
Autoload providers declared into setuptools ``entry_points``.
For example, with this setup.cfg:
.. code-block:: ini
[entry_points]
jsonspec.reference.contributions =
spec = jsonspec.misc.providers:SpecProvider
"""
namespace = 'jsonspec.reference.contributions'
def __init__(self, namespace=None, configuration=None):
self.namespace = namespace or self.namespace
self.configuration = configuration or {}
self.loaded = False
def load(self):
providers = {}
for entrypoint in pkg_resources.iter_entry_points(self.namespace):
kwargs = self.configuration.get(entrypoint.name, {})
providers[entrypoint.name] = entrypoint.load()(**kwargs)
logger.debug('loaded %s from %s', entrypoint, entrypoint.dist)
self.providers = providers
self.loaded = True
def __getitem__(self, uri):
if not self.loaded:
self.load()
for name, provider in self.providers.items():
try:
value = provider[uri]
logger.info('got %s from %s', uri, name)
return value
except (KeyError, NotFound):
pass
raise NotFound('no providers could return {!r}'.format(uri))
def __iter__(self):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
class FilesystemProvider(Provider):
"""
Exposes json documents stored into filesystem.
for example, with ``prefix=my:pref:`` and ``directory=my/directory``,
this filesystem will be loaded as::
my/directory/
foo.json -> my:pref:foo#
bar.json -> my:pref:bar#
baz/
quux.json -> my:pref:baz/quux#
"""
def __init__(self, directory, prefix=None):
self.directory = directory
if not self.directory.endswith('/'):
self.directory += '/'
self.prefix = prefix or ''
self.loaded = False
@property
def data(self):
if not self.loaded:
data = {}
l = len(self.directory)
for filename in loop(self.directory, '*.json'):
spec = filename[l:-5]
with open(filename, 'r') as file:
schema = json.load(file)
data[spec] = schema
# set the fallbacks
for spec in sorted(data.keys(), reverse=True):
if spec.startswith('draft-'):
metaspec = spec.split('/', 1)[1]
if metaspec not in data:
data[metaspec] = data[spec]
self._data = data
self.loaded = True
return self._data
def __getitem__(self, uri):
spec = uri
if uri.startswith(self.prefix):
spec = uri[len(self.prefix):]
if spec.endswith('#'):
spec = spec[:-1]
try:
return self.data[spec]
except (KeyError, UnboundLocalError):
raise NotFound(uri)
def __iter__(self):
for spec in self.data.keys():
yield '{}{}#'.format(self.prefix, spec)
def __len__(self):
return len(self.data.keys())
class SpecProvider(FilesystemProvider):
"""
Provides specs of http://json-schema.org/
"""
def __init__(self):
from jsonspec.misc import __file__ as misc
base = os.path.realpath(os.path.dirname(misc))
src = os.path.join(base, 'schemas/')
prefix = 'http://json-schema.org/'
super(SpecProvider, self).__init__(src, prefix)
class ProxyProvider(Provider):
def __init__(self, provider):
self.provider = provider
self.local = {}
def __getitem__(self, uri):
try:
return self.local[uri]
except KeyError:
return self.provider[uri]
def __setitem__(self, uri, schema):
self.local[uri] = schema
def __iter__(self):
keys = set(self.local.keys())
keys.update(self.provider.keys())
for key in sorted(keys):
yield key
def __len__(self):
keys = set(self.local.keys())
keys.update(self.provider.keys())
return len(keys)
@@ -1,42 +0,0 @@
"""
jsonspec.reference.util
~~~~~~~~~~~~~~~~~~~~~~~
"""
__all__ = ['ref', 'loop', 'Mapping', 'MutableMapping']
import fnmatch
import os
import re
def ref(obj):
"""Extracts $ref of object."""
try:
return obj['$ref']
except (KeyError, TypeError):
return None
def loop(directory, pattern='*'):
regex = fnmatch.translate(pattern)
matcher = re.compile(regex)
def _loop(directory):
for file in os.listdir(directory):
path = os.path.join(directory, file)
if os.path.isfile(path) and matcher.match(path):
yield path
elif os.path.isdir(path):
for subpath in loop(path):
yield subpath
return _loop(directory)
try:
# py3
from collections.abc import MutableMapping, Mapping
except ImportError:
# py2
from collections import MutableMapping, Mapping
@@ -1,37 +0,0 @@
"""
jsonspec.validators
~~~~~~~~~~~~~~~~~~~
"""
__all__ = ['load', 'register', 'Factory', 'Context',
'Validator', 'ReferenceValidator',
'Draft03Validator', 'Draft04Validator',
'CompilationError', 'ReferenceError', 'ValidationError']
from .bases import Validator, ReferenceValidator
from .exceptions import CompilationError, ReferenceError, ValidationError
from .factorize import register, Factory, Context
from . import draft04 # noqa
from . import draft03 # noqa
from .draft03 import Draft03Validator # noqa
from .draft04 import Draft04Validator # noqa
def load(schema, uri=None, spec=None, provider=None):
"""Scaffold a validator against a schema.
:param schema: the schema to compile into a Validator
:type schema: Mapping
:param uri: the uri of the schema.
it may be ignored in case of not cross
referencing.
:type uri: Pointer, str
:param spec: fallback to this spec if the schema does not provides ts own
:type spec: str
:param provider: the other schemas, in case of cross
referencing
:type provider: Mapping, Provider...
"""
factory = Factory(provider, spec)
return factory(schema, uri or '#')
@@ -1,104 +0,0 @@
"""
jsonspec.validators.bases
~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
__all__ = ['ValidationError', 'Validator', 'ReferenceValidator']
import logging
from abc import abstractmethod, ABCMeta
from six import add_metaclass
from jsonspec.pointer import DocumentPointer
from .exceptions import ValidationError
logger = logging.getLogger(__name__)
@add_metaclass(ABCMeta)
class Validator(object):
"""
The mother of Validators.
"""
#: indicates current uri
uri = None
default = None
def __init__(self, **attrs):
self.uri = attrs.pop('uri', None)
@abstractmethod
def has_default(self):
pass
@abstractmethod
def is_optional(self):
"""
Indicates if the instance must be defined.
"""
pass
@abstractmethod
def validate(self, obj, pointer=None):
"""
Validate object.
:param obj: the object to validate
:param pointer: the object pointer
"""
pass
def __call__(self, obj, pointer=None):
"""shortcut for validate()"""
return self.validate(obj, pointer)
class ReferenceValidator(Validator):
"""
Reference a validator to his pointer.
:ivar pointer: the pointer to the validator
:ivar context: the context object
:ivar default: return the default validator
:ivar validator: return the lazy loaded validator
>>> validator = ReferenceValidator('http://json-schema.org/geo#', context)
>>> assert validator({
>>> 'latitude': 0.0124,
>>> 'longitude': 1.2345
>>> })
"""
def __init__(self, pointer, context):
super(ReferenceValidator, self).__init__()
self.pointer = DocumentPointer(pointer)
self.context = context
self.uri = str(self.pointer)
@property
def validator(self):
if not hasattr(self, '_validator'):
self._validator = self.context.resolve(self.pointer)
return self._validator
def has_default(self):
return self.validator.has_default()
@property
def default(self):
return self.validator.default
def is_optional(self):
return self.validator.is_optional()
def validate(self, obj, pointer=None):
"""
Validate object against validator.
:param obj: the object to validate
:param pointer: the object pointer
"""
return self.validator.validate(obj, pointer)
@@ -1,666 +0,0 @@
"""
jsonspec.validators.draft03
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements JSON Schema draft03.
"""
from __future__ import absolute_import
__all__ = ['compile', 'Draft03Validator']
import logging
import os.path
import re
from copy import deepcopy
from decimal import Decimal
from six import integer_types, string_types
from six.moves.urllib.parse import urljoin
from .bases import ReferenceValidator, Validator
from .exceptions import CompilationError
from .factorize import register
from jsonspec.validators.exceptions import ValidationError
from jsonspec.validators.util import uncamel
from jsonspec.validators.pointer_util import pointer_join
from jsonspec import driver as json
sequence_types = (list, set, tuple)
number_types = (integer_types, float, Decimal)
logger = logging.getLogger(__name__)
@register(spec='http://json-schema.org/draft-03/schema#')
def compile(schema, pointer, context, scope=None):
"""
Compiles schema with `JSON Schema`_ draft-03.
:param schema: obj to compile
:type schema: Mapping
:param pointer: uri of the schema
:type pointer: Pointer, str
:param context: context of this schema
:type context: Context
.. _`JSON Schema`: http://json-schema.org
"""
schm = deepcopy(schema)
scope = urljoin(scope or str(pointer), schm.pop('id', None))
if '$ref' in schema:
return ReferenceValidator(urljoin(scope, schema['$ref']), context)
attrs = {}
if 'additionalItems' in schm:
subpointer = os.path.join(pointer, 'additionalItems')
attrs['additional_items'] = schm.pop('additionalItems')
if isinstance(attrs['additional_items'], dict):
compiled = compile(attrs['additional_items'],
subpointer,
context,
scope)
attrs['additional_items'] = compiled
elif not isinstance(attrs['additional_items'], bool):
raise CompilationError('wrong type for {}'.format('additional_items'), schema) # noqa
if 'additionalProperties' in schm:
attrs['additional_properties'] = schm.pop('additionalProperties')
if isinstance(attrs['additional_properties'], dict):
subpointer = os.path.join(pointer, 'additionalProperties')
value = attrs['additional_properties']
attrs['additional_properties'] = compile(value,
subpointer,
context,
scope)
elif not isinstance(attrs['additional_properties'], bool):
raise CompilationError('additionalProperties must be an object or boolean', schema) # noqa
if 'dependencies' in schm:
attrs['dependencies'] = schm.pop('dependencies')
if not isinstance(attrs['dependencies'], dict):
raise CompilationError('dependencies must be an object', schema)
for key, value in attrs['dependencies'].items():
if isinstance(value, dict):
subpointer = os.path.join(pointer, 'dependencies', key)
attrs['dependencies'][key] = compile(value,
subpointer,
context,
scope)
elif isinstance(value, sequence_types):
continue
elif not isinstance(value, string_types):
raise CompilationError('dependencies must be an array, object or string', schema) # noqa
if 'disallow' in schm:
attrs['disallow'] = schm.pop('disallow')
if isinstance(attrs['disallow'], sequence_types):
for index, value in enumerate(attrs['disallow']):
if isinstance(value, dict):
subpointer = os.path.join(pointer, 'disallow', str(index))
attrs['disallow'][index] = compile(value,
subpointer,
context,
scope)
elif not isinstance(value, string_types):
raise CompilationError('disallow must be an object or string', schema) # noqa
elif not isinstance(attrs['disallow'], string_types):
raise CompilationError('disallow must be an array or string', schema) # noqa
if 'divisibleBy' in schm:
attrs['divisible_by'] = schm.pop('divisibleBy')
if not isinstance(attrs['divisible_by'], number_types):
raise CompilationError('divisibleBy must be a number', schema)
if 'enum' in schm:
attrs['enum'] = schm.pop('enum')
if not isinstance(attrs['enum'], sequence_types):
raise CompilationError('enum must be a sequence', schema)
if 'exclusiveMaximum' in schm:
attrs['exclusive_maximum'] = schm.pop('exclusiveMaximum')
if not isinstance(attrs['exclusive_maximum'], bool):
raise CompilationError('exclusiveMaximum must be a boolean', schema) # noqa
if 'exclusiveMinimum' in schm:
attrs['exclusive_minimum'] = schm.pop('exclusiveMinimum')
if not isinstance(attrs['exclusive_minimum'], bool):
raise CompilationError('exclusiveMinimum must be a boolean', schema) # noqa
if 'extends' in schm:
attrs['extends'] = schm.pop('extends')
subpointer = os.path.join(pointer, 'extends')
if isinstance(attrs['extends'], dict):
attrs['extends'] = compile(attrs['extends'],
subpointer,
context,
scope)
elif isinstance(attrs['extends'], sequence_types):
for index, value in enumerate(attrs['extends']):
attrs['extends'][index] = compile(value,
subpointer,
context,
scope)
else:
raise CompilationError('extends must be an object or array', schema) # noqa
if 'format' in schm:
attrs['format'] = schm.pop('format')
if not isinstance(attrs['format'], string_types):
raise CompilationError('format must be a string', schema)
if 'items' in schm:
subpointer = os.path.join(pointer, 'items')
attrs['items'] = schm.pop('items')
if isinstance(attrs['items'], (list, tuple)):
# each value must be a json schema
attrs['items'] = [compile(element, subpointer, context, scope) for element in attrs['items']] # noqa
elif isinstance(attrs['items'], dict):
# value must be a json schema
attrs['items'] = compile(attrs['items'], subpointer, context, scope) # noqa
else:
# should be a boolean
raise CompilationError('wrong type for {}'.format('items'), schema) # noqa
if 'maximum' in schm:
attrs['maximum'] = schm.pop('maximum')
if not isinstance(attrs['maximum'], number_types):
raise CompilationError('enum must be an integer', schema)
if 'maxItems' in schm:
attrs['max_items'] = schm.pop('maxItems')
if not isinstance(attrs['max_items'], integer_types):
raise CompilationError('maxItems must be an integer', schema)
if 'maxLength' in schm:
attrs['max_length'] = schm.pop('maxLength')
if not isinstance(attrs['max_length'], integer_types):
raise CompilationError('maxLength must be integer', schema)
if 'minimum' in schm:
attrs['minimum'] = schm.pop('minimum')
if not isinstance(attrs['minimum'], number_types):
raise CompilationError('enum must be a number', schema)
if 'minItems' in schm:
attrs['min_items'] = schm.pop('minItems')
if not isinstance(attrs['min_items'], integer_types):
raise CompilationError('minItems must be an integer', schema)
if 'minLength' in schm:
attrs['min_length'] = schm.pop('minLength')
if not isinstance(attrs['min_length'], integer_types):
raise CompilationError('minLength must be integer', schema)
if 'pattern' in schm:
attrs['pattern'] = schm.pop('pattern')
if not isinstance(attrs['pattern'], string_types):
raise CompilationError('pattern must be a string', schema)
if 'patternProperties' in schm:
attrs['pattern_properties'] = schm.pop('patternProperties')
if not isinstance(attrs['pattern_properties'], dict):
raise CompilationError('patternProperties must be an object', schema) # noqa
for name, value in attrs['pattern_properties'].items():
subpointer = os.path.join(pointer, 'patternProperties', name)
attrs['pattern_properties'][name] = compile(value,
subpointer,
context,
scope)
if 'properties' in schm:
attrs['properties'] = schm.pop('properties')
if not isinstance(attrs['properties'], dict):
raise CompilationError('properties must be an object', schema)
for name, value in attrs['properties'].items():
subpointer = os.path.join(pointer, 'properties', name)
attrs['properties'][name] = compile(value,
subpointer,
context,
scope)
if 'required' in schm:
attrs['required'] = schm.pop('required')
if not isinstance(attrs['required'], bool):
raise CompilationError('required must be a boolean', schema)
if 'type' in schm:
attrs['type'] = schm.pop('type')
if isinstance(attrs['type'], sequence_types):
for index, value in enumerate(attrs['type']):
if isinstance(value, dict):
subpointer = os.path.join(pointer, 'type', str(index))
attrs['type'][index] = compile(value,
subpointer,
context,
scope)
elif not isinstance(value, string_types):
raise CompilationError('type must be an object or string', schema) # noqa
elif not isinstance(attrs['type'], string_types):
raise CompilationError('type must be an array or string', schema) # noqa
if 'uniqueItems' in schm:
attrs['unique_items'] = schm.pop('uniqueItems')
if not isinstance(attrs['unique_items'], bool):
raise CompilationError('type must be boolean', schema)
return Draft03Validator(attrs, scope, context.formats)
class Draft03Validator(Validator):
"""
Implements `JSON Schema`_ draft-03 validation.
:ivar attrs: attributes to validate against
:ivar uri: uri of the current validator
:ivar formats: mapping of available formats
>>> validator = Draft03Validator({'min_length': 4})
>>> assert validator('this is sparta')
.. _`JSON Schema`: http://json-schema.org
"""
def __init__(self, attrs, uri=None, formats=None):
attrs = {uncamel(k): v for k, v in attrs.items()}
self.attrs = attrs
self.attrs.setdefault('additional_items', True)
self.attrs.setdefault('pattern_properties', {})
self.attrs.setdefault('exclusive_maximum', False)
self.attrs.setdefault('exclusive_minimum', False)
self.attrs.setdefault('additional_properties', True)
self.attrs.setdefault('properties', {})
self.uri = uri
self.formats = formats or {}
self.default = self.attrs.get('default', None)
self.fail_fast = True
self.errors = []
def is_array(self, obj):
return isinstance(obj, sequence_types)
def is_boolean(self, obj):
return isinstance(obj, bool)
def is_integer(self, obj):
return isinstance(obj, integer_types) and not isinstance(obj, bool)
def is_null(self, obj):
return obj is None
def is_number(self, obj):
return isinstance(obj, number_types) and not isinstance(obj, bool)
def is_object(self, obj):
return isinstance(obj, dict)
def is_string(self, obj):
return isinstance(obj, string_types)
def validate(self, obj, pointer=None):
"""
Validate object against validator
:param obj: the object to validate
"""
pointer = pointer or '#'
validator = deepcopy(self)
validator.errors = []
validator.fail_fast = False
obj = deepcopy(obj)
obj = validator.validate_enum(obj, pointer)
obj = validator.validate_type(obj, pointer)
obj = validator.validate_disallow(obj, pointer)
obj = validator.validate_extends(obj, pointer)
if validator.is_array(obj):
obj = validator.validate_max_items(obj, pointer)
obj = validator.validate_min_items(obj, pointer)
obj = validator.validate_items(obj, pointer)
obj = validator.validate_unique_items(obj, pointer)
if validator.is_number(obj):
obj = validator.validate_maximum(obj, pointer)
obj = validator.validate_minimum(obj, pointer)
obj = validator.validate_divisible_by(obj, pointer)
if validator.is_object(obj):
obj = validator.validate_dependencies(obj, pointer)
obj = validator.validate_properties(obj, pointer)
if validator.is_string(obj):
obj = validator.validate_max_length(obj, pointer)
obj = validator.validate_min_length(obj, pointer)
obj = validator.validate_pattern(obj, pointer)
obj = validator.validate_format(obj, pointer)
if validator.errors:
raise ValidationError('multiple errors',
obj,
errors=validator.errors)
return obj
def validate_dependencies(self, obj, pointer=None):
if 'dependencies' in self.attrs:
missings = set()
for name, dependencies in self.attrs['dependencies'].items():
if name not in obj:
continue
if isinstance(dependencies, Validator):
obj[name] = dependencies(obj)
elif isinstance(dependencies, sequence_types):
for d in dependencies:
if d not in obj:
missings.add(d)
elif dependencies not in obj:
missings.add(dependencies)
if missings:
missings = sorted(missings)
self.fail('Missing properties', obj, pointer)
return obj
def validate_disallow(self, obj, pointer=None):
if 'disallow' in self.attrs:
disallows = self.attrs['disallow']
if not isinstance(disallows, sequence_types):
disallows = [disallows]
disallowed = 0
for type in disallows:
try:
if isinstance(type, Validator):
type(obj)
disallowed += 1
elif type == "any":
disallowed += 1
elif type == "array" and self.is_array(obj):
disallowed += 1
elif type == "boolean" and self.is_boolean(obj):
disallowed += 1
elif type == "integer" and self.is_integer(obj):
disallowed += 1
elif type == "null" and self.is_null(obj):
disallowed += 1
elif type == "number" and self.is_number(obj):
disallowed += 1
elif type == "object" and self.is_object(obj):
disallowed += 1
elif type == "string" and self.is_string(obj):
disallowed += 1
except ValidationError:
# let it, it may be good
pass
if disallowed:
self.fail('Wrong type', obj, pointer)
return obj
def validate_divisible_by(self, obj, pointer=None):
if 'divisible_by' in self.attrs:
factor = Decimal(str(self.attrs['divisible_by']))
orig = Decimal(str(obj))
if orig % factor != 0:
self.fail('Not a multiple of {}', obj, pointer)
return obj
def validate_enum(self, obj, pointer=None):
if 'enum' in self.attrs:
if not obj in self.attrs['enum']:
self.fail('Forbidden value', obj, pointer)
return obj
def validate_extends(self, obj, pointer=None):
if 'extends' in self.attrs:
extends = self.attrs['extends']
if not isinstance(extends, sequence_types):
extends = [extends]
for type in extends:
obj = type(obj)
return obj
def validate_format(self, obj, pointer=None):
"""
================= ============
Expected draft03 Alias of
----------------- ------------
color css.color
date-time utc.datetime
date utc.date
time utc.time
utc-millisec utc.millisec
regex regex
style css.style
phone phone
uri uri
email email
ip-address ipv4
ipv6 ipv6
host-name hostname
================= ============
"""
if 'format' in self.attrs:
substituted = {
'color': 'css.color',
'date-time': 'utc.datetime',
'date': 'utc.date',
'time': 'utc.time',
'utc-millisec': 'utc.millisec',
'regex': 'regex',
'style': 'css.style',
'phone': 'phone',
'uri': 'uri',
'email': 'email',
'ip-address': 'ipv4',
'ipv6': 'ipv6',
'host-name': 'hostname',
}.get(self.attrs['format'], self.attrs['format'])
logger.debug('use %s', substituted)
return self.formats[substituted](obj)
return obj
def validate_items(self, obj, pointer=None):
if 'items' in self.attrs:
items = self.attrs['items']
if isinstance(items, Validator):
validator = items
for index, element in enumerate(obj):
with self.catch_fail():
obj[index] = validator(element, pointer_join(pointer, index)) # noqa
return obj
elif isinstance(items, (list, tuple)):
additionals = self.attrs['additional_items']
validators = items
for index, element in enumerate(obj):
with self.catch_fail():
try:
validator = validators[index]
except IndexError:
if additionals is True:
return obj
elif additionals is False:
self.fail('Additional elements are forbidden',
obj,
pointer_join(pointer, index))
continue
validator = additionals
obj[index] = validator(element, pointer_join(pointer, index)) # noqa
return obj
else:
raise NotImplementedError(items)
return obj
def validate_max_items(self, obj, pointer=None):
if 'max_items' in self.attrs:
count = len(obj)
if count > self.attrs['max_items']:
self.fail('Too many items', obj, pointer)
return obj
def validate_max_length(self, obj, pointer=None):
if 'max_length' in self.attrs:
length = len(obj)
if length > self.attrs['max_length']:
self.fail('Too long', obj, pointer)
return obj
def validate_maximum(self, obj, pointer=None):
if 'maximum' in self.attrs:
if obj > self.attrs['maximum']:
self.fail('Too big number', obj, pointer)
if self.attrs['exclusive_maximum'] and obj == self.attrs['maximum']: # noqa
self.fail('Too big number', obj, pointer)
return obj
def validate_min_items(self, obj, pointer=None):
if 'min_items' in self.attrs:
count = len(obj)
if count < self.attrs['min_items']:
self.fail('Too few items', obj, pointer)
return obj
def validate_min_length(self, obj, pointer=None):
if 'min_length' in self.attrs:
length = len(obj)
if length < self.attrs['min_length']:
self.fail('Too short', obj, pointer)
return obj
def validate_minimum(self, obj, pointer=None):
if 'minimum' in self.attrs:
if obj < self.attrs['minimum']:
self.fail('Too low number', obj, pointer)
if self.attrs['exclusive_minimum'] and obj == self.attrs['minimum']: # noqa
self.fail('Too low number',
obj,
pointer)
return obj
def validate_pattern(self, obj, pointer=None):
if 'pattern' in self.attrs:
regex = re.compile(self.attrs['pattern'])
if not regex.search(obj):
self.fail('Does not match pattern', obj, pointer)
return obj
def validate_properties(self, obj, pointer=None):
validated = set()
pending = set(obj.keys())
for name, validator in self.attrs['properties'].items():
if name in obj:
with self.catch_fail():
pending.discard(name)
obj[name] = validator(obj[name], pointer_join(pointer, name)) # noqa
validated.add(name)
elif not validator.is_optional():
self.fail('Required property', obj, pointer)
for pattern, validator in self.attrs['pattern_properties'].items():
regex = re.compile(pattern)
for name, value in obj.items():
if regex.search(name):
with self.catch_fail():
pending.discard(name)
obj[name] = validator(obj[name], pointer_join(pointer, name)) # noqa
validated.add(name)
if not pending:
return obj
if self.attrs['additional_properties'] is True:
return obj
if self.attrs['additional_properties'] is False:
if len(obj) > len(validated):
self.fail('Additional properties are forbidden', obj, pointer) # noqa
return obj
validator = self.attrs['additional_properties']
for name, value in obj.items():
if name not in validated:
obj[name] = validator(value, pointer_join(pointer, name))
validated.add(name)
return obj
def validate_type(self, obj, pointer=None):
if 'type' in self.attrs:
types = self.attrs['type']
if not isinstance(types, sequence_types):
types = [types]
for type in types:
try:
if isinstance(type, Validator):
return type(obj)
elif type == "any":
return obj
elif type == "array" and self.is_array(obj):
return obj
elif type == "boolean" and self.is_boolean(obj):
return obj
elif type == "integer" and self.is_integer(obj):
return obj
elif type == "null" and self.is_null(obj):
return obj
elif type == "number" and self.is_number(obj):
return obj
elif type == "object" and self.is_object(obj):
return obj
elif type == "string" and self.is_string(obj):
return obj
except ValidationError:
# let it, it may be good
pass
self.fail('Wrong type', obj, pointer)
return obj
def validate_unique_items(self, obj, pointer=None):
if self.attrs.get('unique_items'):
if len(obj) > len(set(json.dumps(element) for element in obj)):
self.fail('Elements must be unique', obj, pointer)
return obj
def has_default(self):
"""docstring for has_default"""
return False
def is_optional(self):
"""
True by default.
"""
return not self.attrs.get('required', False)
def fail(self, reason, obj, pointer=None):
"""
Called when validation fails.
"""
pointer = pointer_join(pointer)
err = ValidationError(reason, obj, pointer)
if self.fail_fast:
raise err
else:
self.errors.append(err)
return err
def catch_fail(self):
return FailCatcher(self)
class FailCatcher(object):
def __init__(self, validator):
self.validator = validator
def __enter__(self):
return self
def __exit__(self, type, value, tb):
if isinstance(value, ValidationError) and not self.validator.fail_fast:
self.validator.errors.append(value)
return True
return False
@@ -1,678 +0,0 @@
"""
jsonspec.validators.draft04
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements JSON Schema draft04.
"""
from __future__ import absolute_import
__all__ = ['compile', 'Draft04Validator']
import logging
import os.path
import re
from copy import deepcopy
from decimal import Decimal
from six import integer_types, string_types
from six.moves.urllib.parse import urljoin
from .bases import ReferenceValidator, Validator
from .exceptions import CompilationError
from .factorize import register
from jsonspec.validators.exceptions import ValidationError
from jsonspec.validators.util import uncamel
from jsonspec.validators.pointer_util import pointer_join
from jsonspec import driver as json
sequence_types = (list, set, tuple)
number_types = (integer_types, float, Decimal)
logger = logging.getLogger(__name__)
@register(spec='http://json-schema.org/draft-04/schema#')
def compile(schema, pointer, context, scope=None):
"""
Compiles schema with `JSON Schema`_ draft-04.
:param schema: obj to compile
:type schema: Mapping
:param pointer: uri of the schema
:type pointer: Pointer, str
:param context: context of this schema
:type context: Context
.. _`JSON Schema`: http://json-schema.org
"""
schm = deepcopy(schema)
scope = urljoin(scope or str(pointer), schm.pop('id', None))
if '$ref' in schema:
return ReferenceValidator(urljoin(scope, schema['$ref']), context)
attrs = {}
if 'additionalItems' in schm:
subpointer = os.path.join(pointer, 'additionalItems')
attrs['additional_items'] = schm.pop('additionalItems')
if isinstance(attrs['additional_items'], dict):
compiled = compile(attrs['additional_items'],
subpointer,
context,
scope)
attrs['additional_items'] = compiled
elif not isinstance(attrs['additional_items'], bool):
raise CompilationError('wrong type for {}'.format('additional_items'), schema) # noqa
if 'additionalProperties' in schm:
subpointer = os.path.join(pointer, 'additionalProperties')
attrs['additional_properties'] = schm.pop('additionalProperties')
if isinstance(attrs['additional_properties'], dict):
compiled = compile(attrs['additional_properties'],
subpointer,
context,
scope)
attrs['additional_properties'] = compiled
elif not isinstance(attrs['additional_properties'], bool):
raise CompilationError('wrong type for {}'.format('additional_properties'), schema) # noqa
if 'allOf' in schm:
subpointer = os.path.join(pointer, 'allOf')
attrs['all_of'] = schm.pop('allOf')
if isinstance(attrs['all_of'], (list, tuple)):
attrs['all_of'] = [compile(element, subpointer, context, scope) for element in attrs['all_of']] # noqa
else:
# should be a boolean
raise CompilationError('wrong type for {}'.format('allOf'), schema) # noqa
if 'anyOf' in schm:
subpointer = os.path.join(pointer, 'anyOf')
attrs['any_of'] = schm.pop('anyOf')
if isinstance(attrs['any_of'], (list, tuple)):
attrs['any_of'] = [compile(element, subpointer, context, scope) for element in attrs['any_of']] # noqa
else:
# should be a boolean
raise CompilationError('wrong type for {}'.format('anyOf'), schema) # noqa
if 'default' in schm:
attrs['default'] = schm.pop('default')
if 'dependencies' in schm:
attrs['dependencies'] = schm.pop('dependencies')
if not isinstance(attrs['dependencies'], dict):
raise CompilationError('dependencies must be an object', schema)
for key, value in attrs['dependencies'].items():
if isinstance(value, dict):
subpointer = os.path.join(pointer, 'dependencies', key)
attrs['dependencies'][key] = compile(value,
subpointer,
context,
scope)
elif not isinstance(value, sequence_types):
raise CompilationError('dependencies must be an array or object', schema) # noqa
if 'enum' in schm:
attrs['enum'] = schm.pop('enum')
if not isinstance(attrs['enum'], sequence_types):
raise CompilationError('enum must be a sequence', schema)
if 'exclusiveMaximum' in schm:
attrs['exclusive_maximum'] = schm.pop('exclusiveMaximum')
if not isinstance(attrs['exclusive_maximum'], bool):
raise CompilationError('exclusiveMaximum must be a boolean', schema)
if 'exclusiveMinimum' in schm:
attrs['exclusive_minimum'] = schm.pop('exclusiveMinimum')
if not isinstance(attrs['exclusive_minimum'], bool):
raise CompilationError('exclusiveMinimum must be a boolean', schema)
if 'format' in schm:
attrs['format'] = schm.pop('format')
if not isinstance(attrs['format'], string_types):
raise CompilationError('format must be a string', schema)
if 'items' in schm:
subpointer = os.path.join(pointer, 'items')
attrs['items'] = schm.pop('items')
if isinstance(attrs['items'], (list, tuple)):
# each value must be a json schema
attrs['items'] = [compile(element, subpointer, context, scope) for element in attrs['items']] # noqa
elif isinstance(attrs['items'], dict):
# value must be a json schema
attrs['items'] = compile(attrs['items'], subpointer, context, scope) # noqa
else:
# should be a boolean
raise CompilationError('wrong type for {}'.format('items'), schema) # noqa
if 'maximum' in schm:
attrs['maximum'] = schm.pop('maximum')
if not isinstance(attrs['maximum'], number_types):
raise CompilationError('maximum must be a number', schema)
if 'maxItems' in schm:
attrs['max_items'] = schm.pop('maxItems')
if not isinstance(attrs['max_items'], integer_types):
raise CompilationError('maxItems must be integer', schema)
if 'maxLength' in schm:
attrs['max_length'] = schm.pop('maxLength')
if not isinstance(attrs['max_length'], integer_types):
raise CompilationError('maxLength must be integer', schema)
if 'maxProperties' in schm:
attrs['max_properties'] = schm.pop('maxProperties')
if not isinstance(attrs['max_properties'], integer_types):
raise CompilationError('maxProperties must be integer', schema)
if 'minimum' in schm:
attrs['minimum'] = schm.pop('minimum')
if not isinstance(attrs['minimum'], number_types):
raise CompilationError('minimum must be a number', schema)
if 'minItems' in schm:
attrs['min_items'] = schm.pop('minItems')
if not isinstance(attrs['min_items'], integer_types):
raise CompilationError('minItems must be integer', schema)
if 'minLength' in schm:
attrs['min_length'] = schm.pop('minLength')
if not isinstance(attrs['min_length'], integer_types):
raise CompilationError('minLength must be integer', schema)
if 'minProperties' in schm:
attrs['min_properties'] = schm.pop('minProperties')
if not isinstance(attrs['min_properties'], integer_types):
raise CompilationError('minProperties must be integer', schema)
if 'multipleOf' in schm:
attrs['multiple_of'] = schm.pop('multipleOf')
if not isinstance(attrs['multiple_of'], number_types):
raise CompilationError('multipleOf must be a number', schema)
if 'not' in schm:
attrs['not'] = schm.pop('not')
if not isinstance(attrs['not'], dict):
raise CompilationError('not must be an object', schema)
subpointer = os.path.join(pointer, 'not')
attrs['not'] = compile(attrs['not'], subpointer, context, scope)
if 'oneOf' in schm:
subpointer = os.path.join(pointer, 'oneOf')
attrs['one_of'] = schm.pop('oneOf')
if isinstance(attrs['one_of'], (list, tuple)):
# each value must be a json schema
attrs['one_of'] = [compile(element, subpointer, context, scope) for element in attrs['one_of']] # noqa
else:
# should be a boolean
raise CompilationError('wrong type for {}'.format('oneOf'), schema)
if 'pattern' in schm:
attrs['pattern'] = schm.pop('pattern')
if not isinstance(attrs['pattern'], string_types):
raise CompilationError('pattern must be a string', schema)
if 'properties' in schm:
attrs['properties'] = schm.pop('properties')
if not isinstance(attrs['properties'], dict):
raise CompilationError('properties must be an object', schema)
for subname, subschema in attrs['properties'].items():
subpointer = pointer_join(pointer, subname)
compiled = compile(subschema, subpointer, context, scope)
attrs['properties'][subname] = compiled
if 'patternProperties' in schm:
attrs['pattern_properties'] = schm.pop('patternProperties')
if not isinstance(attrs['pattern_properties'], dict):
raise CompilationError('patternProperties must be an object', schema)
for subname, subschema in attrs['pattern_properties'].items():
subpointer = os.path.join(pointer, 'patternProperties', subname)
compiled = compile(subschema, subpointer, context, scope)
attrs['pattern_properties'][subname] = compiled
if 'required' in schm:
attrs['required'] = schm.pop('required')
if not isinstance(attrs['required'], list):
raise CompilationError('required must be a list', schema)
if len(attrs['required']) < 1:
raise CompilationError('required cannot be empty', schema)
if 'type' in schm:
attrs['type'] = schm.pop('type')
if isinstance(attrs['type'], string_types):
attrs['type'] = [attrs['type']]
elif not isinstance(attrs['type'], sequence_types):
raise CompilationError('type must be string or sequence', schema)
if 'uniqueItems' in schm:
attrs['unique_items'] = schm.pop('uniqueItems')
if not isinstance(attrs['unique_items'], bool):
raise CompilationError('type must be boolean', schema)
return Draft04Validator(attrs, str(pointer), context.formats)
class Draft04Validator(Validator):
"""
Implements `JSON Schema`_ draft-04 validation.
:ivar attrs: attributes to validate against
:ivar uri: uri of the current validator
:ivar formats: mapping of available formats
>>> validator = Draft04Validator({'min_length': 4})
>>> assert validator('this is sparta')
.. _`JSON Schema`: http://json-schema.org
"""
def __init__(self, attrs, uri=None, formats=None):
attrs = {uncamel(k): v for k, v in attrs.items()}
self.formats = formats or {}
self.attrs = attrs
self.attrs.setdefault('additional_items', True)
self.attrs.setdefault('additional_properties', True)
self.attrs.setdefault('exclusive_maximum', False),
self.attrs.setdefault('exclusive_minimum', False),
self.attrs.setdefault('pattern_properties', {})
self.attrs.setdefault('properties', {})
self.uri = uri
self.default = self.attrs.get('default', None)
self.fail_fast = True
self.errors = []
def validate(self, obj, pointer=None):
"""
Validate object against validator
:param obj: the object to validate
"""
pointer = pointer or '#'
validator = deepcopy(self)
validator.errors = []
validator.fail_fast = False
obj = deepcopy(obj)
obj = validator.validate_enum(obj, pointer)
obj = validator.validate_type(obj, pointer)
obj = validator.validate_not(obj, pointer)
obj = validator.validate_all_of(obj, pointer)
obj = validator.validate_any_of(obj, pointer)
obj = validator.validate_one_of(obj, pointer)
if self.is_array(obj):
obj = validator.validate_items(obj, pointer)
obj = validator.validate_max_items(obj, pointer)
obj = validator.validate_min_items(obj, pointer)
obj = validator.validate_unique_items(obj, pointer)
elif self.is_number(obj):
obj = validator.validate_maximum(obj, pointer)
obj = validator.validate_minimum(obj, pointer)
obj = validator.validate_multiple_of(obj, pointer)
elif self.is_object(obj):
obj = validator.validate_required(obj, pointer)
obj = validator.validate_max_properties(obj, pointer)
obj = validator.validate_min_properties(obj, pointer)
obj = validator.validate_dependencies(obj, pointer)
obj = validator.validate_properties(obj, pointer)
obj = validator.validate_default_properties(obj, pointer)
elif self.is_string(obj):
obj = validator.validate_max_length(obj, pointer)
obj = validator.validate_min_length(obj, pointer)
obj = validator.validate_pattern(obj, pointer)
obj = validator.validate_format(obj, pointer)
if validator.errors:
print(validator.errors)
raise ValidationError('multiple errors',
obj,
errors=validator.errors)
return obj
def is_array(self, obj):
return isinstance(obj, sequence_types)
def is_boolean(self, obj):
return isinstance(obj, bool)
def is_integer(self, obj):
return isinstance(obj, integer_types) and not isinstance(obj, bool)
def is_number(self, obj):
return isinstance(obj, number_types) and not isinstance(obj, bool)
def is_object(self, obj):
return isinstance(obj, dict)
def is_string(self, obj):
return isinstance(obj, string_types)
def has_default(self):
return 'default' in self.attrs
def validate_all_of(self, obj, pointer=None):
for validator in self.attrs.get('all_of', []):
obj = validator(obj)
return obj
def validate_any_of(self, obj, pointer=None):
if 'any_of' in self.attrs:
for validator in self.attrs['any_of']:
try:
obj = validator(obj)
return obj
except ValidationError:
pass
self.fail('Not in any_of', obj, pointer)
return obj
def validate_default_properties(self, obj, pointer=None):
# Reinject defaults from properties.
for name, validator in self.attrs.get('properties', {}).items():
if name not in obj and validator.has_default():
obj[name] = deepcopy(validator.default)
return obj
def validate_dependencies(self, obj, pointer=None):
for key, dependencies in self.attrs.get('dependencies', {}).items():
if key in obj:
if isinstance(dependencies, sequence_types):
for dep in set(dependencies) - set(obj.keys()):
self.fail('Missing property', obj, pointer)
else:
dependencies(obj)
return obj
def validate_enum(self, obj, pointer=None):
if 'enum' in self.attrs:
if obj not in self.attrs['enum']:
self.fail('Forbidden value', obj, pointer)
return obj
def validate_format(self, obj, pointer=None):
"""
================= ============
Expected draft04 Alias of
----------------- ------------
date-time rfc3339.datetime
email email
hostname hostname
ipv4 ipv4
ipv6 ipv6
uri uri
================= ============
"""
if 'format' in self.attrs:
substituted = {
'date-time': 'rfc3339.datetime',
'email': 'email',
'hostname': 'hostname',
'ipv4': 'ipv4',
'ipv6': 'ipv6',
'uri': 'uri',
}.get(self.attrs['format'], self.attrs['format'])
logger.debug('use %s', substituted)
try:
return self.formats[substituted](obj)
except ValidationError as error:
logger.error(error)
self.fail('Forbidden value', obj, pointer)
return obj
def validate_items(self, obj, pointer=None):
if 'items' in self.attrs:
items = self.attrs['items']
if isinstance(items, Validator):
validator = items
for index, element in enumerate(obj):
with self.catch_fail():
obj[index] = validator(element, pointer_join(pointer, index)) # noqa
return obj
elif isinstance(items, (list, tuple)):
additionals = self.attrs['additional_items']
validators = items
for index, element in enumerate(obj):
with self.catch_fail():
try:
validator = validators[index]
except IndexError:
if additionals is True:
return obj
elif additionals is False:
self.fail('Forbidden value',
obj,
pointer=pointer_join(self.uri, index))
continue
validator = additionals
obj[index] = validator(element, pointer_join(pointer, index)) # noqa
return obj
else:
raise NotImplementedError(items)
return obj
def validate_maximum(self, obj, pointer=None):
if 'maximum' in self.attrs:
m = self.attrs['maximum']
if obj < m:
return obj
exclusive = self.attrs['exclusive_maximum']
if not exclusive and (obj == m):
return obj
self.fail('Exceeded maximum', obj, pointer)
return obj
def validate_max_items(self, obj, pointer=None):
if 'max_items' in self.attrs:
count = len(obj)
if count > self.attrs['max_items']:
self.fail('Too many elements', obj, pointer)
return obj
def validate_max_length(self, obj, pointer=None):
if 'max_length' in self.attrs:
length = len(obj)
if length > self.attrs['max_length']:
self.fail('Too long', obj, pointer)
return obj
def validate_max_properties(self, obj, pointer=None):
if 'max_properties' in self.attrs:
count = len(obj)
if count > self.attrs['max_properties']:
self.fail('Too many properties', obj, pointer)
return obj
def validate_minimum(self, obj, pointer=None):
if 'minimum' in self.attrs:
m = self.attrs['minimum']
if obj > m:
return obj
exclusive = self.attrs['exclusive_minimum']
if not exclusive and (obj == m):
return obj
self.fail('Too small', obj, pointer)
return obj
def validate_min_items(self, obj, pointer=None):
if 'min_items' in self.attrs:
count = len(obj)
if count < self.attrs['min_items']:
self.fail('Too few elements', obj, pointer)
return obj
def validate_min_length(self, obj, pointer=None):
if 'min_length' in self.attrs:
length = len(obj)
if length < self.attrs['min_length']:
self.fail('Too short', obj, pointer)
return obj
def validate_min_properties(self, obj, pointer=None):
if 'min_properties' in self.attrs:
count = len(obj)
if count < self.attrs['min_properties']:
self.fail('Too few properties', obj, pointer)
return obj
def validate_multiple_of(self, obj, pointer=None):
if 'multiple_of' in self.attrs:
factor = Decimal(str(self.attrs['multiple_of']))
orig = Decimal(str(obj))
if orig % factor != 0:
self.fail('Forbidden value', obj, pointer)
return obj
def validate_not(self, obj, pointer=None):
if 'not' in self.attrs:
try:
validator = self.attrs['not']
validator(obj)
except ValidationError:
return obj
else:
self.fail('Forbidden value', obj, pointer)
return obj
def validate_one_of(self, obj, pointer=None):
if 'one_of' in self.attrs:
validated = 0
for validator in self.attrs['one_of']:
try:
validated_obj = validator(obj)
validated += 1
except ValidationError:
pass
if not validated:
self.fail('Validates noone', obj)
if validated == 1:
return validated_obj
else:
self.fail('Validates more than once', obj)
return obj
def validate_pattern(self, obj, pointer=None):
if 'pattern' in self.attrs:
pattern = self.attrs['pattern']
if re.search(pattern, obj):
return obj
self.fail('Forbidden value', obj, pointer)
return obj
def validate_properties(self, obj, pointer=None):
validated = set()
pending = set(obj.keys())
response = {}
if not obj:
return response
for name, validator in self.attrs['properties'].items():
if name in obj:
with self.catch_fail():
pending.discard(name)
obj[name] = validator(obj[name], pointer_join(pointer, name)) # noqa
validated.add(name)
for pattern, validator in self.attrs['pattern_properties'].items():
for name in sorted(obj.keys()):
if re.search(pattern, name):
with self.catch_fail():
pending.discard(name)
obj[name] = validator(obj[name], pointer_join(pointer, name)) # noqa
validated.add(name)
if not pending:
return obj
additionals = self.attrs['additional_properties']
if additionals is True:
return obj
if additionals is False:
self.fail('Forbidden additional properties', obj, pointer)
return obj
validator = additionals
for name in sorted(pending):
obj[name] = validator(obj.pop(name), pointer_join(pointer, name)) # noqa
validated.add(name)
return obj
def validate_required(self, obj, pointer=None):
if 'required' in self.attrs:
for name in self.attrs['required']:
if name not in obj:
self.fail('Missing property', obj, pointer)
return obj
def validate_type(self, obj, pointer=None):
if 'type' in self.attrs:
types = self.attrs['type']
if isinstance(types, string_types):
types = [types]
for t in types:
if t == 'array' and self.is_array(obj):
return obj
if t == 'boolean' and self.is_boolean(obj):
return obj
if t == 'integer' and self.is_integer(obj):
return obj
if t == 'number' and self.is_number(obj):
return obj
if t == 'null' and obj is None:
return obj
if t == 'object' and self.is_object(obj):
return obj
if t == 'string' and self.is_string(obj):
return obj
self.fail('Wrong type', obj, pointer)
return obj
def validate_unique_items(self, obj, pointer=None):
if self.attrs.get('unique_items'):
if len(obj) > len(set(json.dumps(element) for element in obj)):
self.fail('Elements must be unique', obj, pointer)
return obj
def is_optional(self):
"""
Returns True, beceause it is meaningless in draft04.
"""
logger.warn('asking for is_optional')
return True
def fail(self, reason, obj, pointer=None):
"""
Called when validation fails.
"""
pointer = pointer_join(pointer)
err = ValidationError(reason, obj, pointer)
if self.fail_fast:
raise err
else:
self.errors.append(err)
return err
def catch_fail(self):
return FailCatcher(self)
class FailCatcher(object):
def __init__(self, validator):
self.validator = validator
def __enter__(self):
return self
def __exit__(self, type, value, tb):
if isinstance(value, ValidationError) and not self.validator.fail_fast:
self.validator.errors.append(value)
return True
return False
@@ -1,70 +0,0 @@
"""
jsonspec.validators.exceptions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
__all__ = ['CompilationError', 'ReferenceError', 'ValidationError']
from collections import defaultdict
class CompilationError(Exception):
"""Raised while schema parsing"""
def __init__(self, message, schema):
super(CompilationError, self).__init__(message, schema)
self.schema = schema
class ReferenceError(Exception):
"""Raised while reference error"""
def __init__(self, *args):
super(ReferenceError, self).__init__(*args)
class ValidationError(ValueError):
"""Raised when validation fails"""
def __init__(self, reason, obj=None, pointer=None, errors=None):
"""
:param reason: the reason failing
:param obj: the obj that fails
:param errors: sub errors, if they exists
"""
super(ValidationError, self).__init__(reason, obj)
self.obj = obj
self.pointer = pointer
self.errors = set()
if isinstance(errors, (list, tuple, set)):
self.errors.update(errors)
elif isinstance(errors, Exception):
self.errors.add(errors)
def flatten(self):
"""
Flatten nested errors.
{pointer: reasons}
"""
return flatten(self)
def flatten(error):
def iter_it(src):
if isinstance(src, (list, set, tuple)):
for error in src:
for pointer, reason in iter_it(error):
yield pointer, reason
if isinstance(src, ValidationError):
if src.errors:
for pointer, reason in iter_it(src.errors):
yield pointer, reason
if src.pointer:
yield src.pointer, src.args[0]
data = defaultdict(set)
for pointer, reason in iter_it(error):
data[pointer].add(reason)
return dict(data)
@@ -1,131 +0,0 @@
"""
jsonspec.validators.factorize
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
__all__ = ['Context', 'Factory', 'register']
import logging
from functools import partial
from jsonspec.pointer import DocumentPointer
from jsonspec.pointer.exceptions import ExtractError
from jsonspec.reference import LocalRegistry
from .exceptions import CompilationError
from .formats import FormatRegistry
logger = logging.getLogger(__name__)
class Context(object):
"""
:ivar factory: global factory
:ivar registry: the current registry
:ivar spec: the current spec
:ivar formats: the current formats exposed
"""
def __init__(self, factory, registry, spec=None, formats=None):
self.factory = factory
self.registry = registry
self.spec = spec
self.formats = formats
def __call__(self, schema, pointer):
return self.factory(schema, pointer, self.spec)
def resolve(self, pointer):
try:
dp = DocumentPointer(pointer)
if dp.is_inner():
logger.debug('resolve inner %s', pointer)
return self.factory.local(self.registry.resolve(pointer),
pointer,
self.registry,
self.spec)
logger.debug('resolve outside %s', pointer)
return self.factory(self.registry.resolve(pointer),
pointer,
self.spec)
except ExtractError as error:
raise CompilationError({}, error)
class Factory(object):
"""
:ivar provider: global registry
:ivar spec: default spec
"""
spec = 'http://json-schema.org/draft-04/schema#'
compilers = {}
def __init__(self, provider=None, spec=None, formats=None):
self.provider = provider or {}
self.spec = spec or self.spec
if not isinstance(formats, FormatRegistry):
formats = FormatRegistry(formats)
self.formats = formats
def __call__(self, schema, pointer, spec=None):
try:
spec = schema.get('$schema', spec or self.spec)
compiler = self.compilers[spec]
except KeyError:
raise CompilationError('{!r} not registered'.format(spec), schema)
registry = LocalRegistry(schema, self.provider)
local = DocumentPointer(pointer)
if local.document:
registry[local.document] = schema
local.document = '<local>'
context = Context(self, registry, spec, self.formats)
return compiler(schema, pointer, context)
def local(self, schema, pointer, registry, spec=None):
try:
spec = schema.get('$schema', spec or self.spec)
compiler = self.compilers[spec]
except KeyError:
raise CompilationError('{!r} not registered'.format(spec))
context = Context(self, registry, spec, self.formats)
return compiler(schema, pointer, context)
@classmethod
def register(cls, spec, compiler):
cls.compilers[spec] = compiler
return compiler
def register(compiler=None, spec=None):
"""
Expose compiler to factory.
:param compiler: the callable to expose
:type compiler: callable
:param spec: name of the spec
:type spec: str
It can be used as a decorator::
@register(spec='my:first:spec')
def my_compiler(schema, pointer, context):
return Validator(schema)
or as a function::
def my_compiler(schema, pointer, context):
return Validator(schema)
register(my_compiler, 'my:second:spec')
"""
if not spec:
raise CompilationError('Spec is required')
if not compiler:
return partial(register, spec=spec)
return Factory.register(spec, compiler)
@@ -1,143 +0,0 @@
"""
jsonspec.validators.formats
~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
__all__ = ['register', 'FormatRegistry']
import logging
from functools import partial
from pkg_resources import iter_entry_points, DistributionNotFound
from .exceptions import CompilationError
logger = logging.getLogger(__name__)
class FormatRegistry(object):
"""
Declare callables that must validate strings.
Callables are be injected in two ways:
- Using the instance register method:
.. code-block:: python
registry = FormatRegistry()
registry.register('foo', bar)
- Using the class register method:
.. code-block:: python
FormatRegistry.register('foo', bar)
registry = FormatRegistry()
assert 'foo' in registry
- Every callables declared into setuptools ``entry_points``
are automatically loaded.
For example, with this setup.cfg:
.. code-block:: ini
[entry_points]
jsonspec.validators.formats =
date-time = jsonspec.validators.util:validate_datetime
email = jsonspec.validators.util:validate_email
hostname = jsonspec.validators.util:validate_hostname
ipv4 = jsonspec.validators.util:validate_ipv4
ipv6 = jsonspec.validators.util:validate_ipv6
uri = jsonspec.validators.util:validate_uri
.. code-block:: python
registry = FormatRegistry()
assert 'date-time' in registry
"""
namespace = 'jsonspec.validators.formats'
custom = {}
def __init__(self, data=None, namespace=None):
self.custom = data or self.custom
self.loaded = {}
self.fallback = {}
self.namespace = namespace or self.namespace
def __getitem__(self, name):
if name in self.custom:
return self.custom[name]
if name in self.loaded:
return self.loaded[name]
if name in self.fallback:
return self.fallback[name]
return self.load(name)
def __contains__(self, name):
return name in self.custom or name in self.loaded
def load(self, name):
error = None
for entrypoint in iter_entry_points(self.namespace):
try:
if entrypoint.name == name:
self.loaded[name] = entrypoint.load()
return self.loaded[name]
except DistributionNotFound as error:
pass
if error:
logger.warn('Unable to load %s: %s is missing', name, error)
else:
logger.warn('%s is not defined', name)
def fallback(obj):
logger.info('Unable to validate %s: %s is missing', name, error)
return obj
fallback.__doc__ = 'fallback for {!r} validation'.format(name)
self.fallback[name] = fallback
return self.fallback[name]
@classmethod
def register(cls, name, func):
cls.custom[name] = func
def register(func=None, name=None):
"""
Expose compiler to factory.
:param func: the callable to expose
:type func: callable
:param name: name of format
:type name: str
It can be used as a decorator::
@register(name='my:validator')
def my_validator(obj):
if obj is True:
return obj
raise ValidationError('obj is not true')
or as a function::
def my_validator(obj):
if obj is True:
return obj
raise ValidationError('obj is not true')
@register(name='my:validator')
"""
if not name:
raise CompilationError('Name is required')
if not func:
return partial(register, name=name)
return FormatRegistry.register(name, func)
@@ -1,20 +0,0 @@
"""
jsonspec.validators.pointer_util
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
def pointer_join(pre, *parts):
resp = str(pre or '#')
if resp == '#/':
resp == '#'
for part in parts:
if not resp.endswith('/'):
resp += '/'
resp += str(part)
if resp == '#':
resp = '#/'
return resp
@@ -1,244 +0,0 @@
"""
jsonspec.validators.util
~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import
__all__ = []
import logging
import re
import time
from copy import deepcopy
from decimal import Decimal
from datetime import tzinfo, timedelta, datetime, date
from six import text_type
from six import integer_types
from six.moves.urllib.parse import urlparse
from .exceptions import ValidationError
number_types = (integer_types, float, Decimal)
logger = logging.getLogger(__name__)
HOSTNAME_TOKENS = re.compile('(?!-)[a-z\d-]{1,63}(?<!-)$', re.IGNORECASE)
HOSTNAME_LAST_TOKEN = re.compile('[a-z]+$', re.IGNORECASE)
EMAIL = re.compile('[^@]+@[^@]+\.[^@]+')
CSS_COLORS = set([
'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige',
'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown',
'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral',
'cornflowerblue', 'cornsilk', 'crimson', 'darkblue', 'darkcyan',
'darkgoldenrod', 'darkgray', 'darkgreen', 'darkkhaki', 'darkmagenta',
'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon',
'darkseagreen', 'darkslateblue', 'darkslategray', 'darkturquoise',
'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dodgerblue',
'feldspar', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia',
'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green',
'greenyellow', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory',
'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon',
'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow',
'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen',
'lightskyblue', 'lightslateblue', 'lightslategray', 'lightsteelblue',
'lightyellow', 'lime', 'limegreen', 'linen', 'maroon', 'mediumaquamarine',
'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen',
'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange',
'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise',
'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum',
'powderblue', 'purple', 'red', 'rosybrown', 'royalblue', 'saddlebrown',
'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver',
'skyblue', 'slateblue', 'slategray', 'snow', 'springgreen', 'steelblue',
'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violetred', 'wheat',
'white', 'whitesmoke', 'yellow', 'yellowgreen'])
def uncamel(name):
"""converts camelcase to underscore
>>> uncamel('fooBar')
'foo_bar'
>>> uncamel('FooBar')
'foo_bar'
>>> uncamel('_fooBar')
'_foo_bar'
>>> uncamel('_FooBar')
'__foo_bar'
"""
response, name = name[0].lower(), name[1:]
for n in name:
if n.isupper():
response += '_' + n.lower()
else:
response += n
return response
class offset(tzinfo):
def __init__(self, value):
self.value = value
# super(tzinfo, self).__init__()
def utcoffset(self, dt):
hours, minutes = self.value.split(':', 1)
return timedelta(hours=int(hours), minutes=int(minutes))
def tzname(self, dt):
return '{}'.format(self.value)
def rfc3339_to_datetime(data):
"""convert a rfc3339 date representation into a Python datetime"""
try:
ts = time.strptime(data, '%Y-%m-%d')
return date(*ts[:3])
except ValueError:
pass
try:
dt, _, tz = data.partition('Z')
if tz:
tz = offset(tz)
else:
tz = offset('00:00')
if '.' in dt and dt.rsplit('.', 1)[-1].isdigit():
ts = time.strptime(dt, '%Y-%m-%dT%H:%M:%S.%f')
else:
ts = time.strptime(dt, '%Y-%m-%dT%H:%M:%S')
return datetime(*ts[:6], tzinfo=tz)
except ValueError:
raise ValueError('date-time {!r} is not a valid rfc3339 date representation'.format(data)) # noqa
def validate_css_color(obj):
color = obj.lower()
if len(color) == 7 and re.match('^#[0-9a-f]{6}$', color):
return obj
elif len(color) == 4 and re.match('^#[0-9a-f]{3}$', color):
return obj
elif color not in CSS_COLORS:
raise ValidationError('Not a css color {!r}'.format(obj))
return obj
def validate_rfc3339_datetime(obj):
try:
rfc3339_to_datetime(obj)
except ValueError:
raise ValidationError('{!r} is not a valid datetime', obj)
return obj
def validate_utc_datetime(obj):
if not obj.endswith('Z'):
raise ValidationError('{!r} is not a valid datetime', obj)
obj = obj[:-1]
if '.' in obj:
obj, milli = obj.split('.', 1)
if not milli.isdigit():
raise ValidationError('{!r} is not a valid datetime', obj)
try:
time.strptime(obj, '%Y-%m-%dT%H:%M:%S')
except ValueError:
raise ValidationError('{!r} is not a valid datetime', obj)
return obj
def validate_utc_date(obj):
try:
time.strptime(obj, '%Y-%m-%d')
except ValueError:
raise ValidationError('{!r} is not a valid date', obj)
return obj
def validate_utc_time(obj):
try:
time.strptime(obj, '%H:%M:%S')
except ValueError:
raise ValidationError('{!r} is not a valid time', obj)
return obj
def validate_utc_millisec(obj):
try:
if not isinstance(obj, number_types):
raise TypeError
datetime.utcfromtimestamp(obj/1000)
except (TypeError, ValueError):
raise ValidationError('{!r} is not a valid utc millis', obj)
return obj
def validate_email(obj):
if not EMAIL.match(obj):
raise ValidationError('{!r} is not defined')
return obj
def validate_hostname(obj):
try:
host = deepcopy(obj)
if len(host) > 255:
raise ValueError
if host[-1] == '.':
host = host[:-1]
tokens = host.split('.')
if not all(HOSTNAME_TOKENS.match(x) for x in tokens):
raise ValueError
if not HOSTNAME_LAST_TOKEN.search(tokens[-1]):
raise ValueError
except ValueError:
raise ValidationError('{!r} is not a valid hostname'.format(obj))
return obj
def validate_ipv4(obj):
try:
import ipaddress
obj = text_type(obj)
ipaddress.IPv4Address(obj)
except ImportError:
raise ValidationError('IPv4 relies on ipaddress package', obj)
except (ipaddress.AddressValueError, ipaddress.NetmaskValueError):
raise ValidationError('{!r} does not appear to '
'be an IPv4 address'.format(obj))
return obj
def validate_ipv6(obj):
try:
import ipaddress
obj = text_type(obj)
ipaddress.IPv6Address(obj)
except ImportError:
raise ValidationError('IPv6 relies on ipaddress package', obj)
except (ipaddress.AddressValueError, ipaddress.NetmaskValueError):
raise ValidationError('{!r} does not appear to '
'be an IPv6 address'.format(obj))
return obj
def validate_regex(obj):
# TODO implement ECMA 262 regex
import re
try:
re.compile(obj)
except:
raise ValidationError('Not a regex', obj)
return obj
def validate_uri(obj):
try:
if ':' not in obj:
raise ValueError('missing scheme')
urlparse(obj)
except Exception as error:
logger.exception(error)
raise ValidationError('{!r} is not an uri'.format(obj))
return obj
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 2.8.3)
project(hos_python_json)
project(hos_python_utils)
## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<package>
<name>hos_python_json</name>
<name>hos_python_utils</name>
<version>0.0.0</version>
<description>The hos_python_json package</description>
@@ -7,7 +7,7 @@ import json
from jsonspec.reference.bases import Provider
class HOSParamServerProvider(Provider):
class SchemaLoader(Provider):
def __init__(self, ros_param_ns = "/hos/types/repository", prefix = "hos:schema://"):
self.ros_param_ns = ros_param_ns
@@ -36,8 +36,7 @@ class HOSParamServerProvider(Provider):
raise exceptions.KeyError(type)
else:
return schema
# Provider inherits from Mapping, therefore we need to implement __iter__ and __len__,
# They don't really fit our use case but we need to return at least one item here to avoid problems with jsonspec.
def __iter__(self):
@@ -4,7 +4,7 @@ from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup
package_info = generate_distutils_setup(
packages=['jsonspec', 'jsonspec_hos'],
packages=['hos'],
package_dir={'': 'python'},
)