.
This commit is contained in:
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
+14
-17
@@ -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_ */
|
||||
@@ -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) {
|
||||
|
||||
+63
-31
@@ -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";
|
||||
|
||||
|
||||
+13
-14
@@ -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 {
|
||||
|
||||
+6
-5
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-3
@@ -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'},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user