diff --git a/src/Makefile b/src/Makefile index 4a885d52a..d36afb0e2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -60,6 +60,7 @@ INC += pvxs/unittest.h INC += pvxs/util.h INC += pvxs/sharedArray.h INC += pvxs/data.h +INC += pvxs/json.h INC += pvxs/nt.h INC += pvxs/netcommon.h INC += pvxs/server.h @@ -80,6 +81,7 @@ LIB_SRCS += bitmask.cpp LIB_SRCS += type.cpp LIB_SRCS += data.cpp LIB_SRCS += datafmt.cpp +LIB_SRCS += json.cpp LIB_SRCS += pvrequest.cpp LIB_SRCS += dataencode.cpp LIB_SRCS += nt.cpp diff --git a/src/data.cpp b/src/data.cpp index 8dcb7ddfe..58ea4507b 100644 --- a/src/data.cpp +++ b/src/data.cpp @@ -493,6 +493,33 @@ void Value::copyOut(void *ptr, StoreType type) const break; } case StoreType::Null: + if(desc->id=="enum_t") { + // special case handling for NTEnum + auto index((*this).lookup("index")); + switch(type) { + case StoreType::Integer: + *reinterpret_cast(ptr) = index.as(); + return; + + case StoreType::UInteger: + *reinterpret_cast(ptr) = index.as(); + return; + + case StoreType::String: { + auto idx = index.as(); + auto choices = (*this).lookup("choices").as>(); + if(idx < choices.size()) { + *reinterpret_cast(ptr) = choices[idx]; + return; + + } else { + throw NoConvert(SB()<<"enum_t index "<id=="enum_t") { + auto index((*this).lookup("index")); + switch(type) { + case StoreType::Integer: + index = *reinterpret_cast(ptr); + return; + case StoreType::UInteger: + index = *reinterpret_cast(ptr); + return; + case StoreType::String: { + auto& choice(*reinterpret_cast(ptr)); + auto choices((*this).lookup("choices").as>()); + for(auto idx : range(choices.size())) { + if(choice == choices[idx]) { + index = idx; + return; + } + } + // try to parse as string + index = parseTo(choice); + return; + } + break; + default: + break; + } } throw NoConvert(SB()<<"Unable to assign "<code<<" with "<[0-9a-zA-Z_]+[.\[-$] - maybedot = false; - if(expr.size()-pos >= 2 && expr[pos]=='-' && expr[pos+1]=='>') { pos += 2; // skip past "->" - if(desc->code.code==TypeCode::Any) { - // select member of Any (may be Null) - *this = store->as(); + } else if(pos>0u || desc->code.code!=TypeCode::Union) { + // expected "->" + // allow omission at the beginning of an expression when starting from a Union + store.reset(); + desc = nullptr; + if(dothrow) + throw LookupError(SB()<<"expected -> in '"<mlookup)::const_iterator it; - auto& fld = store->as(); + if(desc->code.code==TypeCode::Any) { + // select member of Any (may be Null) + *this = store->as(); - if(sep>0 && (it=desc->mlookup.find(expr.substr(pos, sep-pos)))!=desc->mlookup.end()) { - // found it. + } else { + // select member of Union + size_t sep = expr.find_first_of("<[-.", pos); - if(modify || fld.desc==&desc->members[it->second]) { - // will select, or already selected - if(fld.desc!=&desc->members[it->second]) { - // select - std::shared_ptr mtype(store->top->desc, &desc->members[it->second]); - fld = Value(mtype, *this); - } - pos = sep; - *this = fld; - maybedot = true; + decltype (desc->mlookup)::const_iterator it; + auto& fld = store->as(); - } else { - // traversing const Value, can't select Union - store.reset(); - desc = nullptr; - if(dothrow) - throw LookupError(SB()<<"traversing const Value, can't select Union in '"<0 && (it=desc->mlookup.find(expr.substr(pos, sep-pos)))!=desc->mlookup.end()) { + // found it. - } else if(fld.desc) { - // deref selected + if(modify || fld.desc==&desc->members[it->second]) { + // will select, or already selected + if(fld.desc!=&desc->members[it->second]) { + // select + std::shared_ptr mtype(store->top->desc, &desc->members[it->second]); + fld = Value(mtype, *this); + } + pos = sep; *this = fld; + maybedot = true; } else { + // traversing const Value, can't select Union store.reset(); desc = nullptr; if(dothrow) - throw LookupError(SB()<<"can't deref. empty Union '"<" - store.reset(); - desc = nullptr; - if(dothrow) - throw LookupError(SB()<<"expected -> in '"<code.isarray() && desc->code.kind()==Kind::Compound) { diff --git a/src/json.cpp b/src/json.cpp new file mode 100644 index 000000000..1110fa3fa --- /dev/null +++ b/src/json.cpp @@ -0,0 +1,470 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include +#include +#include + +#include +#include + +#include + +#include "utilpvt.h" + +namespace pvxs { +namespace json { + +namespace { +// undef implies API version 0 +#ifndef EPICS_YAJL_VERSION +typedef long integer_arg; +typedef unsigned size_arg; +#else +typedef long long integer_arg; +typedef size_t size_arg; +#endif + +struct JAny; +// use std::list so that insertion does not invalidate references +typedef std::list > JMap; +typedef std::list JList; + +struct JAny { + aligned_union<8, bool, double, int64_t, uint64_t, std::string, JMap, JList>::type store; + enum type_t { + Placeholder, + Null, + Bool, + Double, + Int64, + UInt64, + String, + List, + Map, + } type = Null; + + template + T& as() { return *reinterpret_cast(&store); } + template + const T& as() const { return *reinterpret_cast(&store); } + + JAny() : store({}), type(Placeholder) {} + JAny(const JAny&) = delete; + JAny& operator=(const JAny&) = delete; + + ~JAny() { + switch(type) { + case Placeholder: + case Null: + case Bool: + case Double: + case Int64: + case UInt64: + break; // nothing to do for POD + case String: + as().~basic_string(); + break; + case List: + as().~JList(); + break; + case Map: + as().~JMap(); + break; + } + } +}; + +constexpr size_t stkLimit = 10; + +struct JContext { + std::vector stk; + std::ostringstream msg; + + void exc(std::exception& e) noexcept { + try { + msg<<"\nError: "<(); + list.emplace_back(); + auto& newtop = list.back(); + stk.push_back(&newtop); + return newtop; + } else { + throw std::logic_error("invalid stack state for array"); + } + } + void consume_top() { + assert(!stk.empty()); + stk.pop_back(); + } +}; + +struct YHandle { + yajl_handle handle; + YHandle(yajl_handle handle) + :handle(handle) + { + if(!handle) + throw std::runtime_error("yajl_alloc fails"); + } + ~YHandle() { + yajl_free(handle); + } + operator yajl_handle() { return handle; } +}; + +#define TRY \ + auto ctx = static_cast(raw); \ + try + +#define CATCH() \ + catch(std::exception& e){ \ + ctx->exc(e); \ + return 0; \ + } + +int jvalue_null(void * raw) noexcept { + TRY { + auto& top = ctx->setup_top(); + top.type = JAny::Null; + ctx->consume_top(); + + return 1; + }CATCH() +} + +int jvalue_boolean(void * raw, int val) noexcept { + TRY { + auto& top = ctx->setup_top(); + top.type = JAny::Bool; + top.as() = val; + ctx->consume_top(); + + return 1; + }CATCH() +} + +int jvalue_integer(void * raw, integer_arg val) noexcept { + TRY { + auto& top = ctx->setup_top(); + top.type = JAny::Int64; + top.as() = val; + ctx->consume_top(); + + return 1; + }CATCH() +} + +int jvalue_real(void * raw, double val) noexcept { + TRY { + auto& top = ctx->setup_top(); + top.type = JAny::Double; + top.as() = val; + ctx->consume_top(); + + return 1; + }CATCH() +} + +int jvalue_string(void * raw, const unsigned char * val, size_arg len) { + TRY { + auto& top = ctx->setup_top(); + new (&top.store) std::string((const char*)val, len); + top.type = JAny::String; + ctx->consume_top(); + + return 1; + }CATCH() +} + +int jvalue_start_map(void * raw) noexcept { + TRY { + if(ctx->stk.size() >= stkLimit) + throw std::runtime_error("JSON structure too deep!"); + + auto& top = ctx->setup_top(); + new (&top.store) JMap(); + top.type = JAny::Map; + + return 1; + }CATCH() +} + +int jvalue_map_key(void * raw, const unsigned char * val, size_arg len) { + TRY { + assert(!ctx->stk.empty()); + auto& top = *ctx->stk.back(); + assert(top.type==JAny::Map); + + auto& map = top.as(); + map.emplace_back(std::piecewise_construct, + std::forward_as_tuple((const char*)val, len), + std::forward_as_tuple()); + ctx->stk.push_back(&map.back().second); + + return 1; + }CATCH() +} + +int jvalue_end_map(void * raw) noexcept { + TRY { + assert(!ctx->stk.empty()); + auto& top = ctx->stk.back(); + assert(top->type==JAny::Map); + + ctx->consume_top(); + + return 1; + }CATCH() +} + +int jvalue_start_array(void * raw) noexcept { + TRY { + if(ctx->stk.size() >= stkLimit) + throw std::runtime_error("JSON structure too deep!"); + + auto& top = ctx->stk.back(); + assert(top->type==JAny::Placeholder); + new (&top->store) JList(); + top->type = JAny::List; + + return 1; + }CATCH() +} + +int jvalue_end_array(void * raw) noexcept { + TRY { + assert(!ctx->stk.empty()); + auto& top = ctx->stk.back(); + assert(top->type==JAny::List); + + ctx->consume_top(); + + return 1; + }CATCH() +} + +const yajl_callbacks jvalue_cbs = { + jvalue_null, + jvalue_boolean, + jvalue_integer, + jvalue_real, + nullptr, + jvalue_string, + jvalue_start_map, + jvalue_map_key, + jvalue_end_map, + jvalue_start_array, + jvalue_end_array, +}; + +Value infer_from_ast(const JAny& src) { + throw std::logic_error("Not implemented"); +} + +void apply_ast(Value& dest, const JAny& src) +{ + if(dest.type()==TypeCode::Any) { + Value node(infer_from_ast(src)); + apply_ast(node, src); + dest.from(node); + return; + } + + switch(src.type) { + case JAny::Placeholder: + throw std::logic_error("placeholder in JSON AST"); + case JAny::Null: + dest = unselect; + break; + case JAny::Bool: + dest.from(src.as()); + break; + case JAny::Double: + dest.from(src.as()); + break; + case JAny::Int64: + dest.from(src.as()); + break; + case JAny::UInt64: + dest.from(src.as()); + break; + case JAny::String: + dest.from(src.as()); + break; + case JAny::Map: { + auto& map = src.as(); + for(auto& pair : map) { + auto node(dest.lookup(pair.first)); + apply_ast(node, pair.second); + } + } + break; + case JAny::List: { + auto& list = src.as(); + auto dtype(dest.type()); + + if(dtype==TypeCode::StructA || dtype==TypeCode::UnionA) { + shared_array elems(list.size()); + size_t i=0; + for(auto& elem : list) { + if(elem.type!=JAny::Null) { + auto& eval = elems[i] = dest.allocMember(); + apply_ast(eval, elem); + } + i++; + } + dest.from(elems.freeze()); + + } else if(dtype==TypeCode::AnyA) { + shared_array elems(list.size()); + size_t i=0; + for(auto& elem : list) { + if(elem.type!=JAny::Null) { + auto& eval = elems[i] = infer_from_ast(elem); + apply_ast(eval, elem); + } + i++; + } + dest.from(elems.freeze()); + + } else { // array of scalar type + auto arr(allocArray(dtype.arrayType(), list.size())); + auto dtype(arr.original_type()); + auto esize(elementSize(dtype)); + auto cur(arr.data()); + + for(auto& elem : list) { + ArrayType stype; + switch(elem.type) { + case JAny::Bool: stype = ArrayType::Bool; break; + case JAny::Double: stype = ArrayType::Float64; break; + case JAny::Int64: stype = ArrayType::Int64; break; + case JAny::UInt64: stype = ArrayType::UInt64; break; + case JAny::String: stype = ArrayType::String; break; + default: + throw std::runtime_error(SB()<<"Can't assign "<() = parseTo(num); + } else { // integer + top.type = JAny::Int64; + top.as() = parseTo(num); + } + + return; + } + + // parse into AST + JContext ctx; + ctx.stk.push_back(&top); + +#ifndef EPICS_YAJL_VERSION + yajl_parser_config conf; + memset(&conf, 0, sizeof(conf)); + conf.allowComments = 1; + conf.checkUTF8 = 1; + YHandle handle(yajl_alloc(&jvalue_cbs, &conf, NULL, &ctx)); +#else + YHandle handle(yajl_alloc(&jvalue_cbs, NULL, &ctx)); + + yajl_config(handle, yajl_allow_comments, 1); +#endif + + auto sts(yajl_parse(handle, (const unsigned char*)cur, len)); + auto consumed(yajl_get_bytes_consumed(handle)); + + switch(sts) { + case yajl_status_ok: + for(; consumed < len; consumed++) { + if(!isspace(cur[consumed])) + throw std::runtime_error("Trailing junk after JSON"); + } + // success + break; + case yajl_status_client_canceled: + throw std::runtime_error(ctx.msg.str()); +#ifndef EPICS_YAJL_VERSION + case yajl_status_insufficient_data: + throw std::runtime_error("JSON incomplete"); + break; +#endif + case yajl_status_error: { + auto raw(yajl_get_error(handle, 1, (const unsigned char*)cur, len)); + try { + ctx.msg<<"\nJSON Syntax error: "< + +#include +#include + +#include + +namespace pvxs { +class Value; + +namespace json { + +/** Parse JSON string + * + * A character array must remain valid and unmodified for + * the lifetime of the Parse object referencing it. + * + * @since UNRELEASED + */ +struct Parse { + const char* base; + size_t count; + + //! Nil terminated string. + inline + Parse(const char *s) + :base(s) + ,count(strlen(s)) + {} + //! Character array without terminating Nil. + inline + constexpr Parse(const char *s, size_t c) + :base(s) + ,count(c) + {} + //! String + inline + Parse(const std::string& s) + :base(s.c_str()) + ,count(s.size()) + {} + + /** Assign previously created Value from parsed string. + * + * Provided Value may be modified on error. + * + * @pre v.valid() + */ + PVXS_API + void into(Value& v) const; + + //! Infer type from JSON value + PVXS_API + Value as() const; +}; + +} // namespace json +} // namespace pvxs + +#endif // PVXS_JSON_H diff --git a/test/Makefile b/test/Makefile index 9cd3cb321..d4f960fe9 100644 --- a/test/Makefile +++ b/test/Makefile @@ -55,6 +55,10 @@ TESTPROD_HOST += testdata testdata_SRCS += testdata.cpp TESTS += testdata +TESTPROD_HOST += testjson +testjson_SRCS += testjson.cpp +TESTS += testjson + TESTPROD_HOST += testnt testnt_SRCS += testnt.cpp TESTS += testnt diff --git a/test/testjson.cpp b/test/testjson.cpp new file mode 100644 index 000000000..cc4187f5c --- /dev/null +++ b/test/testjson.cpp @@ -0,0 +1,224 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include + +#include + +#include +#include + +#include +#include +#include +#include + +#include "utilpvt.h" + +using namespace pvxs; +namespace { + +void testBad() +{ + testDiag("%s", __func__); + + static const char* inputs[] = { + "", + " 14 x", + " {", + " {} extra", + R"({"value":{"A":[1,2], "B":"[1.5, 2.5]}}")", + }; + + for(size_t i=0; i([inp](){ + Value empty; + json::Parse(inp).into(empty); + })<<" Expected error from "<(), "hello"); + } + + { + Value val(TypeDef(TypeCode::Int16).create()); + json::Parse("42").into(val); + testEq(val.as(), 42); + } + + { + Value val(TypeDef(TypeCode::Int64).create()); + json::Parse(" -42 ").into(val); + testEq(val.as(), -42); + } + + { + Value val(TypeDef(TypeCode::Float64).create()); + json::Parse(" -42.5 ").into(val); + testEq(val.as(), -42.5); + } +} + +void testStruct() +{ + testDiag("%s", __func__); + + { + Value top(nt::NTScalar{TypeCode::UInt16, true}.create()); + json::Parse(R"( {"value":43, "alarm":{"severity":1, "status":2, "message":"hello"}, "display":{}} )").into(top); + testEq(top["value"].as(), 43); + testEq(top["alarm.severity"].as(), 1); + testEq(top["alarm.status"].as(), 2); + testEq(top["alarm.message"].as(), "hello"); + } +} + +void testArrayOfScalar() +{ + testDiag("%s", __func__); + + { + Value val(TypeDef(TypeCode::Int32A).create()); + json::Parse(R"( [1, 2,3 ] )").into(val); + shared_array expect({1, 2, 3}); + testArrEq(val.as>(), expect); + } + + { + Value val(TypeDef(TypeCode::Int32A).create()); + json::Parse(R"( [1, 2.5,3 ] )").into(val); + shared_array expect({1, 2, 3}); + testArrEq(val.as>(), expect); + } + + { + Value val(TypeDef(TypeCode::Float64A).create()); + json::Parse(R"( [1.5, 2,3 ] )").into(val); + shared_array expect({1.5, 2.0, 3.0}); + testArrEq(val.as>(), expect); + } + + { + Value val(TypeDef(TypeCode::StringA).create()); + json::Parse(R"( ["1", "hello", "world" ] )").into(val); + shared_array expect({"1", "hello", "world"}); + testArrEq(val.as>(), expect); + } +} + +void testStructArray() +{ + using namespace pvxs::members; + + testDiag("%s", __func__); + + { + Value val(TypeDef(TypeCode::StructA, { + Int32("ival"), + String("sval"), + Int32A("aval"), + }).create()); + json::Parse("[" + "{\"ival\":1, \"sval\":\"hello\"}," + "null," + "{\"aval\": [4,5,6]}" + "]").into(val); + testStrEq(std::string(SB()<({"A", "B", "C"}); + + auto value(top["value"]); + testEq(value.as(), "C"); + testEq(value.as(), 2); + testEq(value.as(), 2u); + + value = "1"; + testEq(value.as(), "B"); + + value = "A"; + testEq(value.as(), "A"); + + value.from(1u); + testEq(value.as(), "B"); + + value.from(2u); + testEq(value.as(), "C"); } void testNTTable() @@ -100,7 +120,7 @@ void testNTTable() } // namespace MAIN(testnt) { - testPlan(21); + testPlan(28); testNTScalar(); testNTNDArray(); testNTURI(); diff --git a/tools/put.cpp b/tools/put.cpp index 5ee201fda..cf21c17f5 100644 --- a/tools/put.cpp +++ b/tools/put.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "utilpvt.h" #include "evhelper.h" @@ -85,9 +86,10 @@ int main(int argc, char *argv[]) if(argc-optind==1 && std::string(argv[optind]).find_first_of('=')==std::string::npos) { // only one field assignment, and field name omitted. - // implies .value + // if JSON map, treat as entire struct. Others imply .value - values["value"] = argv[optind]; + auto sval(argv[optind]); + values[sval[0]=='{' ? "" : "value"] = sval; } else { for(auto n : range(optind, argc)) { @@ -118,9 +120,18 @@ int main(int argc, char *argv[]) auto val = std::move(prototype); for(auto& pair : values) { try{ - val[pair.first] = pair.second; - }catch(NoConvert& e){ - throw std::runtime_error(SB()<<"Unable to assign "<