Skip to content

Commit 401484e

Browse files
Merge pull request #3 from geometryprocessing/inject_include
2 parents d7d22e3 + 5a9e13b commit 401484e

File tree

9 files changed

+280
-5
lines changed

9 files changed

+280
-5
lines changed

CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ project(JSE
3434
option(JSE_WITH_SANITIZERS "Enable sanitizers in compilation targets" OFF)
3535
# Misc.
3636
option(JSE_WITH_TESTS "Build unit-tests" ${JSE_TOPLEVEL_PROJECT})
37+
option(JSE_WITH_APP "Compiles applications" ${JSE_TOPLEVEL_PROJECT})
3738

3839
include(CMakeDependentOption)
3940

@@ -121,3 +122,18 @@ if(JSE_WITH_TESTS)
121122

122123
add_subdirectory(tests)
123124
endif()
125+
126+
################################################################################
127+
# Applications
128+
################################################################################
129+
130+
if(JSE_WITH_APP)
131+
132+
add_executable(jse_app src/main.cpp)
133+
134+
target_compile_options(jse_app PRIVATE "-rdynamic")
135+
target_link_libraries(jse_app PUBLIC
136+
jse::jse
137+
)
138+
139+
endif()

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,23 @@ Optional parameters:
150150

151151
The entry must be a boolean value.
152152

153+
### "type": "include"
154+
155+
Mandatory parameter:
156+
* spec_file: ["spec.json"]
157+
158+
The entry must be valid with respect to the json spec provided in spec_file. spec_file can be a global path, or relative to the paths provided in the advanced option jse.include_directories
159+
153160
## Defaults
154161

155162
Every rule associated with an optional parameter of an object must specify a default value, using the default field.
156163

157164
# Advanced options
158165
```cpp
159-
jse.strict // DEFAULT: false - if strict == false, a json is valid even if it has entries not validated by a rule
160-
jse.skip_file_check // DEFAULT: true - disable checking for existance of file and folders
161-
jse.boxing_primitive // DEFAULT: true - always try to convert any type t to a list of t for the purpose of finding a valid rule
166+
jse.strict // DEFAULT: false - if strict == false, a json is valid even if it has entries not validated by a rule
167+
jse.skip_file_check // DEFAULT: true - disable checking for existance of file and folders
168+
jse.boxing_primitive // DEFAULT: true - always try to convert any type t to a list of t for the purpose of finding a valid rule
169+
jse.include_directories // DEFAULT: empty - list of strings containing additional paths where json spec are possibly located
162170
```
163171

164172
## Authors

data/rules_02.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"pointer": "/",
4+
"type": "object",
5+
"required": ["field1"]
6+
},
7+
{
8+
"pointer": "/field1",
9+
"type": "float",
10+
"min": 45
11+
}
12+
]

data/rules_03.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[
2+
{
3+
"pointer": "/",
4+
"type": "object",
5+
"required": ["field1"]
6+
},
7+
{
8+
"pointer": "/field1",
9+
"type": "float",
10+
"min": 45
11+
},
12+
{
13+
"pointer": "/object1",
14+
"type": "object",
15+
"required": ["field1"]
16+
},
17+
{
18+
"pointer": "/object1/field1",
19+
"type": "float",
20+
"min": 45
21+
}
22+
]

data/rules_04.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"pointer": "/",
4+
"type": "include",
5+
"spec_file": "rules_02.json"
6+
},
7+
{
8+
"pointer": "/object1",
9+
"type": "include",
10+
"spec_file": "rules_02.json"
11+
}
12+
]

src/jse/jse.cpp

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <filesystem> // C++17
55
#include <sstream>
66
#include <algorithm>
7+
#include <fstream>
78
////////////////////////////////////////////////////////////////////////////////
89

910
namespace jse
@@ -46,19 +47,114 @@ namespace jse
4647

4748
namespace
4849
{
50+
// adds key after the provided pointer, correctly handling the json special case that root has an ending /
4951
std::string append_pointer(const std::string &pointer, const std::string &key)
5052
{
5153
if (pointer == "/")
5254
return "/" + key;
5355
else
5456
return pointer + "/" + key;
5557
}
58+
59+
// adds key before the provided pointer, correctly handling the json special case that root has an ending /
60+
std::string prepend_pointer(const std::string &pointer, const std::string &key)
61+
{
62+
if (key == "/")
63+
return pointer;
64+
else if (pointer == "/")
65+
return key;
66+
else
67+
return key + pointer;
68+
}
69+
5670
} // namespace
5771

72+
73+
// enriches a given json spec with included json specs
74+
json JSE::inject_include(const json &rules)
75+
{
76+
std::vector<string> dirs = include_directories;
77+
dirs.push_back(""); // adding default path
78+
79+
// max 10 levels of nesting to avoid infinite loops
80+
json current = rules;
81+
82+
for (size_t x = 0; x < 10; x++)
83+
{
84+
// check if the rules have any include
85+
bool include_present = false;
86+
for (const auto& rule : current)
87+
if (rule.at("type") == "include")
88+
include_present = true;
89+
90+
// if there are no includes, return the current ones
91+
if (!include_present)
92+
return current;
93+
94+
json enriched;
95+
// otherwise, do a round of replacement
96+
for (const auto& rule : current)
97+
{
98+
// copy all rules that are not include
99+
if (rule.at("type") != "include")
100+
{
101+
enriched.push_back(rule);
102+
}
103+
// if the rule is an include, expand the node with a copy of the included file
104+
else
105+
{
106+
bool replaced = false;
107+
// the include file could be in any of the include directories
108+
for (const auto& dir : dirs)
109+
{
110+
string spec_file = rule.at("spec_file");
111+
string f = dir + "/" + spec_file;
112+
// check if the file exists
113+
if (std::filesystem::is_regular_file(f))
114+
{
115+
std::ifstream ifs(f);
116+
json include_rules = json::parse(ifs);
117+
118+
// loop over all rules to add the prefix
119+
for (auto& i_rule : include_rules)
120+
{
121+
string prefix = rule.at("pointer");
122+
string pointer = i_rule.at("pointer");
123+
string new_pointer = prepend_pointer(pointer,prefix);
124+
i_rule.at("pointer") = new_pointer;
125+
}
126+
127+
// save modified rules
128+
for (const auto& i_rule : include_rules)
129+
enriched.push_back(i_rule);
130+
131+
// one substitution is enough, give up the search over include dirs
132+
replaced = true;
133+
break;
134+
}
135+
136+
}
137+
138+
if (!replaced)
139+
{
140+
string pointer = rule.at("pointer");
141+
throw std::runtime_error("Failed to replace the include rule: " + pointer);
142+
assert(replaced == true);
143+
}
144+
}
145+
}
146+
147+
// now that we replaced the include, copy it back to current
148+
current = enriched;
149+
}
150+
151+
throw std::runtime_error("Reached maximal 10 levels of include recursion.");
152+
153+
}
154+
155+
58156
bool JSE::verify_json(const string &pointer, json &input, const json &rules)
59157
{
60-
// if (pointer == "/common")
61-
// std::cout << "gotcha" << std::endl;
62158
// Find all rules that apply for the input node
63159
// TODO: accelerate this
64160
std::vector<json> matching_rules = collect_pointer(pointer, rules);
@@ -216,6 +312,8 @@ namespace jse
216312
return verify_rule_object(input, rule);
217313
else if (type == "bool")
218314
return verify_rule_bool(input, rule);
315+
else if (type == "include")
316+
return verify_rule_include(input, rule);
219317
else
220318
{
221319
log.push_back(log_item("error", "Unknown rule type " + type));
@@ -362,6 +460,14 @@ namespace jse
362460
return true;
363461
}
364462

463+
bool JSE::verify_rule_include(const json &input, const json &rule)
464+
{
465+
assert(rule.at("type") == "include");
466+
467+
// An include rule always fails, they should be processed first by the inject_include function
468+
return false;
469+
}
470+
365471
json JSE::collect_default_rules(const json &rules)
366472
{
367473
// Find all rules that apply for the input node

src/jse/jse.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ namespace jse
1818
// enriches a given json with default values
1919
json inject_defaults(const json &input, const json &rules);
2020

21+
// enriches a given json spec with included json specs
22+
json inject_include(const json &rules);
23+
2124
// log to string
2225
std::string log2str();
2326

@@ -26,7 +29,13 @@ namespace jse
2629

2730
// if strict == false, a json is valid even if it has entries not validated by a rule
2831
bool strict = false;
32+
33+
// do not check the existance of the file pointed in file nodes
2934
bool skip_file_check = true;
35+
36+
// additional directories which can be used for relative paths
37+
std::vector<string> include_directories;
38+
3039
// automatic boxing for primitive types
3140
// if all rules fail for a basic type, try boxing it once and try again
3241
bool boxing_primitive = true;
@@ -51,6 +60,7 @@ namespace jse
5160
bool verify_rule_object(const json &input, const json &rule);
5261
bool verify_rule_bool(const json &input, const json &rule);
5362
bool verify_rule_list(const json &input, const json &rule);
63+
bool verify_rule_include(const json &input, const json &rule);
5464

5565
// Collect all rules having a default
5666
json collect_default_rules(const json &rules);

src/main.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#include <iostream>
2+
#include <cstdlib>
3+
#include <jse/jse.h>
4+
#include <string>
5+
#include <vector>
6+
#include <fstream>
7+
8+
9+
using namespace std;
10+
using namespace jse;
11+
12+
int main(int argc, char *argv[]) {
13+
try
14+
{
15+
16+
if (argc < 3) {
17+
cerr << "This utility recursively replaces the include rules with their corresponding rule files." << endl;
18+
cerr << "Usage: " << argv[0] << " input.json output.json folder1 folder2 ..." << endl;
19+
return EXIT_FAILURE;
20+
}
21+
22+
// First parameter is the input json
23+
string sinput = argv[1];
24+
25+
// Second parameter is the output json
26+
string soutput = argv[2];
27+
28+
// Other parameters are optional include directories
29+
// where the parser will look for rule files
30+
vector<std::string> arg;
31+
for (size_t i=3;i<argc;++i)
32+
arg.push_back(string(argv[i]));
33+
34+
// Initialize JSE
35+
JSE jse;
36+
jse.include_directories = arg;
37+
38+
// Load the input json
39+
std::ifstream input_f(sinput);
40+
json rules = json::parse(input_f);
41+
42+
// Try to replace the includes, might throw exceptions
43+
json new_rules = jse.inject_include(rules);
44+
45+
// Write the new rule file
46+
std::ofstream file(soutput);
47+
file << new_rules;
48+
49+
return EXIT_SUCCESS;
50+
}
51+
catch(const std::exception &exc)
52+
{
53+
std::cout << exc.what() << std::endl;
54+
return EXIT_FAILURE;
55+
}
56+
57+
}

tests/test_validator.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,38 @@ TEST_CASE("type_object", "[validator]")
219219
REQUIRE(!jse.verify_json(input, rules));
220220
}
221221

222+
TEST_CASE("include_rule", "[validator]")
223+
{
224+
json rules = R"(
225+
[
226+
{
227+
"pointer": "/",
228+
"type": "include",
229+
"spec_file": "rules_02.json"
230+
},
231+
{
232+
"pointer": "/object1",
233+
"type": "include",
234+
"spec_file": "rules_02.json"
235+
}
236+
]
237+
)"_json;
238+
239+
JSE jse;
240+
jse.include_directories.push_back(root_path);
241+
json new_rules = jse.inject_include(rules);
242+
243+
std::ifstream ifs2(root_path + "/rules_03.json");
244+
json matching = json::parse(ifs2);
245+
246+
INFO(new_rules);
247+
INFO(matching);
248+
249+
INFO(jse.log2str());
250+
REQUIRE(new_rules == matching);
251+
}
252+
253+
222254
TEST_CASE("file_01", "[validator]")
223255
{
224256
std::ifstream ifs1(root_path + "/input_01.json");

0 commit comments

Comments
 (0)