|
|
# -*- coding: utf-8 -*-
|
|
|
#
|
|
|
# python-json-pointer - An implementation of the JSON Pointer syntax
|
|
|
# https://github.com/stefankoegl/python-json-pointer
|
|
|
#
|
|
|
# Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net>
|
|
|
# All rights reserved.
|
|
|
#
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
|
# modification, are permitted provided that the following conditions
|
|
|
# are met:
|
|
|
#
|
|
|
# 1. Redistributions of source code must retain the above copyright
|
|
|
# notice, this list of conditions and the following disclaimer.
|
|
|
# 2. Redistributions in binary form must reproduce the above copyright
|
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
|
# documentation and/or other materials provided with the distribution.
|
|
|
# 3. The name of the author may not be used to endorse or promote products
|
|
|
# derived from this software without specific prior written permission.
|
|
|
#
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
|
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
|
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
|
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
|
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
|
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
#
|
|
|
|
|
|
""" Identify specific nodes in a JSON document according to
|
|
|
http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-04 """
|
|
|
|
|
|
# Will be parsed by setup.py to determine package metadata
|
|
|
__author__ = 'Stefan Kögl <stefan@skoegl.net>'
|
|
|
__version__ = '0.3'
|
|
|
__website__ = 'https://github.com/stefankoegl/python-json-pointer'
|
|
|
__license__ = 'Modified BSD License'
|
|
|
|
|
|
|
|
|
try:
|
|
|
from urllib import unquote
|
|
|
from itertools import izip
|
|
|
except ImportError: # Python 3
|
|
|
from urllib.parse import unquote
|
|
|
izip = zip
|
|
|
|
|
|
from itertools import tee
|
|
|
|
|
|
|
|
|
class JsonPointerException(Exception):
|
|
|
pass
|
|
|
|
|
|
|
|
|
_nothing = object()
|
|
|
|
|
|
|
|
|
def resolve_pointer(doc, pointer, default=_nothing):
|
|
|
"""
|
|
|
Resolves pointer against doc and returns the referenced object
|
|
|
|
|
|
>>> obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}}
|
|
|
|
|
|
>>> resolve_pointer(obj, '') == obj
|
|
|
True
|
|
|
|
|
|
>>> resolve_pointer(obj, '/foo') == obj['foo']
|
|
|
True
|
|
|
|
|
|
>>> resolve_pointer(obj, '/foo/another%20prop') == obj['foo']['another prop']
|
|
|
True
|
|
|
|
|
|
>>> resolve_pointer(obj, '/foo/another%20prop/baz') == obj['foo']['another prop']['baz']
|
|
|
True
|
|
|
|
|
|
>>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0]
|
|
|
True
|
|
|
|
|
|
>>> resolve_pointer(obj, '/some/path', None) == None
|
|
|
True
|
|
|
|
|
|
"""
|
|
|
|
|
|
pointer = JsonPointer(pointer)
|
|
|
return pointer.resolve(doc, default)
|
|
|
|
|
|
|
|
|
def set_pointer(doc, pointer, value):
|
|
|
"""
|
|
|
Set a field to a given value
|
|
|
|
|
|
The field is indicates by a base location that is given in the constructor,
|
|
|
and an optional relative location in the call to set. If the path doesn't
|
|
|
exist, it is created if possible
|
|
|
|
|
|
>>> obj = {"foo": 2}
|
|
|
>>> pointer = JsonPointer('/bar')
|
|
|
>>> pointer.set(obj, 'one', '0')
|
|
|
>>> pointer.set(obj, 'two', '1')
|
|
|
>>> obj
|
|
|
{'foo': 2, 'bar': ['one', 'two']}
|
|
|
|
|
|
>>> obj = {"foo": 2, "bar": []}
|
|
|
>>> pointer = JsonPointer('/bar')
|
|
|
>>> pointer.set(obj, 5, '0/x')
|
|
|
>>> obj
|
|
|
{'foo': 2, 'bar': [{'x': 5}]}
|
|
|
|
|
|
>>> obj = {'foo': 2, 'bar': [{'x': 5}]}
|
|
|
>>> pointer = JsonPointer('/bar/0')
|
|
|
>>> pointer.set(obj, 10, 'y/0')
|
|
|
>>> obj == {'foo': 2, 'bar': [{'y': [10], 'x': 5}]}
|
|
|
True
|
|
|
"""
|
|
|
|
|
|
pointer = JsonPointer(pointer)
|
|
|
pointer.set(doc, value)
|
|
|
|
|
|
|
|
|
class JsonPointer(object):
|
|
|
""" A JSON Pointer that can reference parts of an JSON document """
|
|
|
|
|
|
def __init__(self, pointer):
|
|
|
parts = pointer.split('/')
|
|
|
if parts.pop(0) != '':
|
|
|
raise JsonPointerException('location must starts with /')
|
|
|
|
|
|
parts = map(unquote, parts)
|
|
|
parts = [part.replace('~1', '/') for part in parts]
|
|
|
parts = [part.replace('~0', '~') for part in parts]
|
|
|
self.parts = parts
|
|
|
|
|
|
|
|
|
|
|
|
def resolve(self, doc, default=_nothing):
|
|
|
"""Resolves the pointer against doc and returns the referenced object"""
|
|
|
|
|
|
for part in self.parts:
|
|
|
|
|
|
try:
|
|
|
doc = self.walk(doc, part)
|
|
|
except JsonPointerException:
|
|
|
if default is _nothing:
|
|
|
raise
|
|
|
else:
|
|
|
return default
|
|
|
|
|
|
return doc
|
|
|
|
|
|
|
|
|
get = resolve
|
|
|
|
|
|
|
|
|
def set(self, doc, value, path=None):
|
|
|
""" Sets a field of doc to value
|
|
|
|
|
|
The location of the field is given by the pointers base location and
|
|
|
the optional path which is relative to the base location """
|
|
|
|
|
|
fullpath = list(self.parts)
|
|
|
|
|
|
if path:
|
|
|
fullpath += path.split('/')
|
|
|
|
|
|
|
|
|
for part, nextpart in pairwise(fullpath):
|
|
|
try:
|
|
|
doc = self.walk(doc, part)
|
|
|
except JsonPointerException:
|
|
|
step_val = [] if nextpart.isdigit() else {}
|
|
|
doc = self._set_value(doc, part, step_val)
|
|
|
|
|
|
self._set_value(doc, fullpath[-1], value)
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
def _set_value(doc, part, value):
|
|
|
part = int(part) if part.isdigit() else part
|
|
|
|
|
|
if isinstance(doc, dict):
|
|
|
doc[part] = value
|
|
|
|
|
|
if isinstance(doc, list):
|
|
|
if len(doc) < part:
|
|
|
doc[part] = value
|
|
|
|
|
|
if len(doc) == part:
|
|
|
doc.append(value)
|
|
|
|
|
|
else:
|
|
|
raise IndexError
|
|
|
|
|
|
return doc[part]
|
|
|
|
|
|
|
|
|
def walk(self, doc, part):
|
|
|
""" Walks one step in doc and returns the referenced part """
|
|
|
|
|
|
# Its not clear if a location "1" should be considered as 1 or "1"
|
|
|
# We prefer the integer-variant if possible
|
|
|
part_variants = self._try_parse(part) + [part]
|
|
|
|
|
|
for variant in part_variants:
|
|
|
try:
|
|
|
return doc[variant]
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
raise JsonPointerException("'%s' not found in %s" % (part, doc))
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
def _try_parse(val, cls=int):
|
|
|
try:
|
|
|
return [cls(val)]
|
|
|
except:
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def pairwise(iterable):
|
|
|
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
|
|
|
a, b = tee(iterable)
|
|
|
for _ in b:
|
|
|
break
|
|
|
return izip(a, b)
|
|
|
|