xjson 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/xjson.rb ADDED
@@ -0,0 +1,270 @@
1
+ require 'json'
2
+ # require 'byebug'
3
+
4
+ class Xjson
5
+
6
+ class XjsonIncludeError < RuntimeError; end
7
+ class XjsonReferenceError < RuntimeError; end
8
+
9
+ VERSION = "0.0.2"
10
+ def Xjson.version
11
+ Xjson::VERSION
12
+ end
13
+
14
+ def Xjson.load( filename )
15
+ Marshal.load( File.read( filename ) )
16
+ end
17
+
18
+ attr_reader :ext_data
19
+ attr_reader :data
20
+ attr_reader :dir
21
+
22
+ def initialize( xjson_file )
23
+ @cur_file = []
24
+ @cur_data = []
25
+ @ext_data = {}
26
+ @ext_data = read_json_file( xjson_file )
27
+ @data = expand( @ext_data )
28
+ end
29
+
30
+ # Read xjson file.
31
+ def read_json_file( xjson_file )
32
+ @cur_file.unshift xjson_file
33
+ if xjson_file[0] != "<"
34
+ JSON.parse( File.read( xjson_file ) )
35
+ else
36
+ JSON.parse( STDIN.read )
37
+ end
38
+ end
39
+
40
+ # Write expanded json file.
41
+ def write_json_file( json_file )
42
+ File.write( json_file, JSON.pretty_generate( @data ) + "\n" )
43
+ end
44
+
45
+ # Flatten by one level within array.
46
+ def flatten( data )
47
+ case data
48
+ when Array;
49
+ res = []
50
+ data.each do |i|
51
+ if i.class == Array
52
+ res += i
53
+ else
54
+ res.push i
55
+ end
56
+ end
57
+ res
58
+ else
59
+ data
60
+ end
61
+ end
62
+
63
+ def find_in_array_of_hash( scope, key, value )
64
+ index = 0
65
+ while index < scope.length
66
+ if Regexp.new( value).match( scope[ index ][ key ] )
67
+ return index
68
+ end
69
+ index += 1
70
+ end
71
+ nil
72
+ end
73
+
74
+ def reference_handle( data, ref_desc )
75
+ if ref_desc[0] != ":"
76
+ reference_handle( data, ":#{ref_desc}" )
77
+ else
78
+ # Relative reference from root.
79
+ path = ref_desc.split( ":" )[1..-1]
80
+ scope = data
81
+ while path[0..-2].any?
82
+ if path[0] == "*"
83
+ # Wildcard for array.
84
+ unless path[1] && path[2]
85
+ raise XjsonReferenceError,
86
+ "Invalid reference: \"#{ref_desc}\" in \"#{@cur_file[0]}\", missing match key and value ..."
87
+ end
88
+ index = find_in_array_of_hash( scope, path[1], path[2] )
89
+ unless index
90
+ raise XjsonReferenceError,
91
+ "Invalid reference: \"#{ref_desc}\" in \"#{@cur_file[0]}\", key and value not matched ..."
92
+ end
93
+ scope = scope[ index ]
94
+ path.shift( 2 )
95
+ else
96
+ begin
97
+ index = Integer( path[0] )
98
+ scope = scope[ index ]
99
+ rescue
100
+ scope = scope[ path[0] ]
101
+ end
102
+ end
103
+ path.shift
104
+ unless scope
105
+ raise XjsonReferenceError,
106
+ "Invalid reference: \"#{ref_desc}\" in \"#{@cur_file[0]}\"..."
107
+ end
108
+ end
109
+ [ scope, path[-1] ]
110
+ end
111
+ end
112
+
113
+ def reference( data, ref_desc )
114
+ path, label = reference_handle( data, ref_desc )
115
+ begin
116
+ index = Integer( label )
117
+ scope = path[ index ]
118
+ rescue
119
+ # scope = scope[ path[0] ]
120
+ scope = path[ label ]
121
+ end
122
+ scope
123
+ end
124
+
125
+ def override_desc( data, exp )
126
+ path, label = reference_handle( data, exp[0] )
127
+ { path: path, label: label, value: exp[1] }
128
+ end
129
+
130
+
131
+ def override_apply( desc, overwrite = false )
132
+ if desc[:label] == "*"
133
+ desc[:path].each do |place|
134
+ if not( place[ desc[:value][0] ] ) || overwrite
135
+ place[ desc[:value][0] ] = desc[:value][1]
136
+ end
137
+ end
138
+ else
139
+ if not( desc[:path][desc[:label]] ) || overwrite
140
+ desc[:path][desc[:label]] = desc[:value]
141
+ end
142
+ end
143
+ end
144
+
145
+
146
+ def override( data, exp, overwrite = false )
147
+ desc = override_desc( data, exp )
148
+ override_apply( desc, overwrite )
149
+ end
150
+
151
+
152
+ # Expand json recursively.
153
+ def expand( data )
154
+
155
+ case data
156
+
157
+ when TrueClass; data
158
+
159
+ when FalseClass; data
160
+
161
+ when Float; data
162
+
163
+ when Integer; data
164
+
165
+ when String; data
166
+
167
+ when Array;
168
+ ret = []
169
+ @cur_data.unshift ret
170
+ data.each do |v|
171
+ value = expand( v )
172
+ ret.push( value ) if value
173
+ end
174
+ @cur_data.shift
175
+ ret
176
+
177
+ when Hash
178
+
179
+ if data.size == 1
180
+
181
+ # Most possible extension.
182
+
183
+ k, v = data.first
184
+
185
+ case k
186
+
187
+ when "@eval"
188
+ %x"#{expand(v)}".split("\n")
189
+
190
+ when "@env"
191
+ ENV[expand(v)]
192
+
193
+ when "@join"
194
+ flatten( v[1..-1].map{|i| expand( i )} ).join( v[0] )
195
+
196
+ when "@flat"
197
+ flatten( v[1..-1].map{|i| expand( i )} )
198
+
199
+ when "@self"
200
+ reference( @ext_data, expand(v) )
201
+
202
+ when "@over";
203
+ override( @cur_data[0], expand(v), true )
204
+ nil
205
+
206
+ when "@base";
207
+ override( @cur_data[0], expand(v), false )
208
+ nil
209
+
210
+ when "@null"
211
+ nil
212
+
213
+ when "@include";
214
+ jsonfile = expand(v)
215
+ subdata = read_json_file( jsonfile )
216
+ expdata = expand( subdata )
217
+ if expdata.class == Hash
218
+ if @cur_data[0].class == expdata.class
219
+ expdata.each do |ke,ve|
220
+ @cur_data[0][ ke ] = ve
221
+ end
222
+ else
223
+ raise XjsonIncludeError,
224
+ "Included file (\"#{jsonfile}\") must contain a hash as top level"
225
+ end
226
+ elsif expdata.class == Array
227
+ expdata.each do |ve|
228
+ @cur_data[0].push ve
229
+ end
230
+ else
231
+ raise XjsonIncludeError,
232
+ "Included file (\"#{jsonfile}\") must contain a hash or a an array as top level"
233
+ end
234
+ @cur_file.shift
235
+ nil
236
+
237
+ else
238
+ # Non-extension.
239
+ { k => expand( v ) }
240
+
241
+ end
242
+
243
+ else
244
+ ret = {}
245
+ @cur_data.unshift ret
246
+ data.each do |k,v|
247
+ case k
248
+ when "@null"; nil
249
+ else
250
+ value = expand( v )
251
+ ret[ k ] = value if value
252
+ end
253
+ end
254
+ @cur_data.shift
255
+ ret
256
+ end
257
+ end
258
+ end
259
+
260
+ # Return JSON data as string.
261
+ def to_s
262
+ JSON.pretty_generate( @data )
263
+ end
264
+
265
+ # Dump JSON data as marshal.
266
+ def dump( filename )
267
+ File.write( filename, Marshal.dump( @data ) )
268
+ end
269
+
270
+ end
@@ -0,0 +1,130 @@
1
+ # Overview
2
+
3
+ Xjson is an extension to JSON format. The Xjson library processes the
4
+ extensions and outputs standard JSON.
5
+
6
+ Xjson (as JSON format) is compatible with standard JSON, i.e. the
7
+ syntax is the same and Xjson can be processed with JSON tools. The
8
+ extensions are special Xjson keywords that have semantics which the
9
+ Xjson processor manages.
10
+
11
+ Xjson renders JSON databases more dynamic and easy to maintain. The
12
+ same could be achieved with a separate pre-processor, but Xjson
13
+ provides tighter integration to JSON. This means that JSON aware text
14
+ editors can be utilized and the JSON file (with extensions) looks and
15
+ feels like a "normal" JSON file.
16
+
17
+
18
+ # Extensions
19
+
20
+ The extensions provide dynamic behaviour and modularity support.
21
+
22
+ * `@eval`: Evaluate "system"/"shell" command.
23
+
24
+ * `@env`: Reference an environment variable.
25
+
26
+ * `@join`: Join pieces of strings with given separator.
27
+
28
+ * `@flat`: Flatten the list by one level.
29
+
30
+ * `@self`: Refer to existing key/value pair, i.e. support for variables.
31
+
32
+ * `@over`: Overwrite existing value or create if not existing.
33
+
34
+ * `@base`: Set base (default) value, if not existing.
35
+
36
+ * `@null`: No operation.
37
+
38
+ * `@include`: Expand another Xjson/JSON file inplace.
39
+
40
+
41
+ Example:
42
+
43
+ ```
44
+ {
45
+ "opts": [
46
+ { "@eval": "/prj/sbin/list_opts" },
47
+ { "@join": [ " ",
48
+ "-conf_file",
49
+ { "@join": [ "/",
50
+ { "@self": "workdir" },
51
+ "unit/data.txt" ] }
52
+ ]
53
+ }
54
+ ],
55
+ "sub-defs": { "@include": "submodule.json" }
56
+ }
57
+
58
+ ```
59
+
60
+ Extensions are captured within a Hash with one key/value pair. The
61
+ pair will be recursively processed, and hence the extensions can be
62
+ nested.
63
+
64
+ Value of "opts" is an array, and the extensions can be directly stored
65
+ as array members.
66
+
67
+ "sub-defs" gives a unique label (key) for the extension, and each key
68
+ must be unique in JSON, or it will be silently overwritten and
69
+ disappear.
70
+
71
+ Hence depending on the context of the extension, it should be written
72
+ either with or without the extension label.
73
+
74
+ Extension arguments and results:
75
+
76
+ ```
77
+ @eval: <string> => <string>
78
+ @env: <string> => <string>
79
+ @join: <separator>, <list-of-strings> => <string>
80
+ @flat: <array-of-atoms-or-arrays> => <array>
81
+ @self: <key-reference> => <value>
82
+ @over: <key-reference>, <value> => -
83
+ @base: <key-reference>, <value> => -
84
+ @null: false => -
85
+ @include: <filename-string> => <hash>
86
+
87
+ ```
88
+
89
+ # Key Reference
90
+
91
+ Key Reference (KR) is used to refer the items in the database
92
+ itself. It can be used with `@self`, `@over`, and `@base` extensions.
93
+
94
+ Key Reference is a String, where ":" is used as hierarchy
95
+ separator. The string should start with ":" to designate the root, but
96
+ when if a pure top-entry reference is performed, then a plain key name
97
+ is accepted (e.g. just "opts", and not ":opts").
98
+
99
+ Key Reference is split into path selectors with the ":" character. The
100
+ path selectors are used one by one. Typically the path selector is a
101
+ key and the corresponding value is selected to be used for the next
102
+ path selector.
103
+
104
+ However, for arrays, the path selector can be an index. The index
105
+ selects the Nth item in the array.
106
+
107
+ In addition to selecting keys and indexed items, there is a wildcard
108
+ selector. The "*" character is used as wildcard. Wildcard behaves
109
+ differently depending on its location in the Key Reference string.
110
+
111
+ If wildcard is the last part of the KR, then it must correspond to an
112
+ array in the hierarchy. The wildcard thus means all the members in the
113
+ array. Then array members must be Hash entries, if KR is used with
114
+ `@over` or `@base`. The value in this case is array of two, where
115
+ first entry is key for the Hash members and second entry is the
116
+ assigned value.
117
+
118
+ If wildcard is used in the middle of the KR, then the next two path
119
+ selectors are used as key and value matching path, and finally the
120
+ third path selector is key to a value. Wildcard must correspond to an
121
+ array of Hash members, and the array member that matches the key and
122
+ value, will be selected. In allows travelling through the array
123
+ without knowing the array index of the desired member.
124
+
125
+ Example:
126
+
127
+ ```
128
+ "base-name": { "@base": [ ":modules:*", [ "start", "starter" ] ] },
129
+ "over-name": { "@over": [ ":modules:*:name:main:stop", "stopper" ] },
130
+ ```
@@ -0,0 +1,50 @@
1
+ {
2
+ "action": "test",
3
+ "regressions": {
4
+ "all": {
5
+ "test": [
6
+ {
7
+ "name": "t_run_1_1",
8
+ "dir": "test_dir"
9
+ },
10
+ {
11
+ "name": "t_run_1_2",
12
+ "dir": "test_dir-1_2"
13
+ },
14
+ {
15
+ "name": "t_run_1_3",
16
+ "dir": "test_dir"
17
+ },
18
+ {
19
+ "foobar1": "test1",
20
+ "dir": "test_dir"
21
+ },
22
+ {
23
+ "foobar2": "test2",
24
+ "dir": "test_dir"
25
+ }
26
+ ]
27
+ }
28
+ },
29
+ "foobar1": "test1",
30
+ "foobar2": "test2",
31
+ "release": "210701",
32
+ "minor": "3",
33
+ "runroot": "prog-",
34
+ "home": "FOOBAR",
35
+ "dut_files": [
36
+ "/release/210701/src/main.c",
37
+ [
38
+ "YES: 3"
39
+ ],
40
+ {
41
+ "myarr": [
42
+ "bar",
43
+ 1,
44
+ 2,
45
+ 3
46
+ ]
47
+ }
48
+ ],
49
+ "eof": true
50
+ }