diagrammatron 0.4.3 → 0.5.0

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.
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2023 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/common'
8
+ require 'optparse'
9
+ require 'yaml'
10
+ require 'json'
11
+
12
+ def convert(result, format)
13
+ case format
14
+ when :json then JSON.generate(result)
15
+ when :yaml then YAML.dump(result)
16
+ when :prettyjson then JSON.pretty_generate(result)
17
+ end
18
+ end
19
+
20
+ def main
21
+ output = nil
22
+ format = :json
23
+ package = :bare
24
+ ENV['POSIXLY_CORRECT'] = '1' # Leaves patterns as they are.
25
+ parser = OptionParser.new do |opts|
26
+ opts.summary_indent = ' '
27
+ opts.summary_width = 20
28
+ opts.banner = 'Usage: diagrammatron-schema [options] schema-name ...'
29
+ opts.separator ''
30
+ opts.separator 'Options:'
31
+ opts.on('-f', '--format FMT', 'Output schema format (json, yaml, pretty).') do |fmt|
32
+ case fmt.upcase
33
+ when 'JSON' then format = :json
34
+ when 'YAML' then format = :yaml
35
+ when 'PRETTY' then format = :prettyjson
36
+ else
37
+ format = fmt
38
+ end
39
+ end
40
+ opts.on('-p', '--package PKG', 'Wrap schema(s) in given way.') do |pkg|
41
+ case pkg.upcase
42
+ when 'OBJECT' then package = :object
43
+ when 'ARRAY' then package = :array
44
+ when 'BARE' then package = :bare
45
+ else
46
+ package = pkg
47
+ end
48
+ end
49
+ opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
50
+ output = filename
51
+ end
52
+ opts.on('-l', '--list', 'List available schemas and exit.') do
53
+ $stdout.puts list_schemas.join("\n")
54
+ exit 0
55
+ end
56
+ opts.on('-h', '--help', 'Print this help and exit.') do
57
+ $stdout.puts %(#{opts}
58
+ Outputs the given schema files.
59
+ Formats are: JSON, YAML, pretty for prettified JSON.
60
+ Packages are:
61
+ object: Output schemas as an object with name to schema mapping.
62
+ array: Output schemas as an array in the order given on the command-line
63
+ bare: Output schemas in the specified order as they are. The default.
64
+ )
65
+ exit 0
66
+ end
67
+ end
68
+ parser.parse! ARGV
69
+
70
+ aargh("Unknown format: #{format}", 1) unless format.is_a? Symbol
71
+ case package
72
+ when :bare, :array then out = []
73
+ when :object then out = {}
74
+ else
75
+ return aargh("Unknown package: #{package}", 1) unless package.is_a? Symbol
76
+ end
77
+
78
+ ARGV.each do |name|
79
+ s = load_schema(name)
80
+ if out.is_a? Array
81
+ out.push(s)
82
+ else
83
+ out[name] = s
84
+ end
85
+ rescue Errno::ENOENT
86
+ aargh "No such schema: #{name}", 2
87
+ rescue StandardError => e
88
+ aargh "#{e}\nInternal error: #{name}", 3
89
+ end
90
+
91
+ if output.nil?
92
+ fp = $stdout
93
+ else
94
+ fp = File.new(output, 'w')
95
+ end
96
+ if package == :bare
97
+ out.each do |item|
98
+ fp.puts(convert(item, format))
99
+ end
100
+ else
101
+ fp.puts(convert(out, format))
102
+ end
103
+ fp.close
104
+ 0
105
+ end
106
+
107
+ exit(main) if (defined? $unit_test).nil?
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2023 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/common'
8
+ require_relative '../lib/subsets'
9
+ require 'optparse'
10
+ require 'set'
11
+
12
+
13
+ class Item
14
+ @@counter = 0
15
+ attr_reader :item, :ids
16
+
17
+ def initialize(item, label2id = {})
18
+ @item = item
19
+ @ids = [ @@counter ]
20
+ @@counter += 1
21
+ # This silently removes edges that have no valid end-point.
22
+ item.fetch('between', []).each { |x| @ids.push(label2id.fetch(x, -1)) }
23
+ end
24
+
25
+ def self.reset
26
+ @@counter = 0
27
+ end
28
+ end
29
+
30
+ def work_copy(src)
31
+ nodes = src.fetch('nodes', []).map { |n| Item.new(n) }
32
+ label2id = {}
33
+ nodes.each { |x| label2id[x.item['label']] = x.ids.first }
34
+ edges = src.fetch('edges', []).map { |e| Item.new(e, label2id) }
35
+ {
36
+ edges: edges,
37
+ nodes: nodes
38
+ }
39
+ end
40
+
41
+ def prepare_output(doc, work, chosen)
42
+ %i[edges nodes].each do |s|
43
+ work[s].select! do |x|
44
+ (x.ids.index { |id| !chosen.member?(id) }).nil?
45
+ end
46
+ doc[s.to_s] = work[s].map(&:item)
47
+ end
48
+ end
49
+
50
+ def gather_set(work, rules, current)
51
+ r = rules['sets'].fetch(current, nil)
52
+ return aargh("Undefined set: #{current}", 9) if r.nil?
53
+ result = Set.new
54
+ %i[nodes edges].each do |category|
55
+ work[category].each do |item|
56
+ if match_item(item.item, r.fetch(category, {}))
57
+ result.add(item.ids.first)
58
+ end
59
+ end
60
+ end
61
+ result
62
+ end
63
+
64
+ def gather(work, rules, current, above, set_cache)
65
+ e = rules['sets'].fetch(current, nil)
66
+ unless e.nil?
67
+ set = set_cache.fetch(current, nil)
68
+ return set unless set.nil?
69
+ # Evaluate needed set, store, and return it.
70
+ set = gather_set(work, rules, current)
71
+ set_cache[current] = set # Error may end up in cache, not a problem.
72
+ return set
73
+ end
74
+ return aargh("Loop in expressions for #{current}: #{above.reverse.join("\n")}", 8) if above.include?(current)
75
+ e = rules['expressions'].fetch(current, nil)
76
+ return aargh("Undefined expression: #{current}", 7) if e.nil?
77
+ above.push(current)
78
+ result = gather(work, rules, e.first, above, set_cache)
79
+ return result unless result.is_a?(Set)
80
+ (1...e.size).step(2) do |k|
81
+ op = e[k]
82
+ id = e[k + 1]
83
+ s = gather(work, rules, id, above, set_cache)
84
+ return s unless s.is_a?(Set)
85
+ case op
86
+ when :plus then result += s
87
+ when :minus then result -= s
88
+ else
89
+ return aargh("Unsupported operator: #{op}", 100)
90
+ end
91
+ end
92
+ above.pop
93
+ result
94
+ end
95
+
96
+ def main
97
+ input_output_schema = 'nodes'
98
+ input = nil
99
+ output = nil
100
+ selected = nil
101
+ ENV['POSIXLY_CORRECT'] = '1'
102
+ parser = OptionParser.new do |opts|
103
+ opts.summary_indent = ' '
104
+ opts.summary_width = 20
105
+ opts.banner = 'Usage: diagrammatron-subset [options] rule-file-names...'
106
+ opts.separator ''
107
+ opts.separator 'Options:'
108
+ opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
109
+ input = filename
110
+ end
111
+ opts.on('-o', '--output FILE', 'Output file name. Written to stdout if not given.') do |filename|
112
+ output = filename
113
+ end
114
+ opts.on('-s', '--select NAME', 'Select expression or subset NAME result.') do |name|
115
+ selected = name
116
+ end
117
+ opts.on('-h', '--help', 'Print this help and exit.') do
118
+ $stdout.puts %(#{opts}
119
+ Rule files can specify named subset selection rules and named expressions on
120
+ how to combine the subsets when determining which nodes and edges will form the
121
+ diagram.
122
+
123
+ Subset selection rules can apply to nodes only, edges only, or both. The rules
124
+ give for a field name an array of strings or patterns. If a node or edge
125
+ matches any of the rules, it belongs to the subset.
126
+
127
+ Expressions can refer to the subset or expression names and use operations to
128
+ form more complex subsets. In the simplest form, they just are a subset name.
129
+ Operations are a + b to add elements of b and a - b to drop elements of b. If
130
+ expression and subset have the same name, identifier refers to subset.
131
+
132
+ The select option gives the name of the expression or subset to use.
133
+
134
+ Edges to removed nodes are removed. Nodes with all edges removed are retained.
135
+ Output is the input file with remaining nodes and edges.
136
+
137
+ Input and output YAML file schema is returned by:
138
+ diagrammatron-schema #{input_output_schema}
139
+
140
+ Rule YAML file schema is returned by:
141
+ diagrammatron-schema subset
142
+
143
+ Note that input schema is minimal and does not restrict running this program
144
+ at a later stage in the pipeline after other tools have added their fields.
145
+ You can refer to fields outside the schema.
146
+ )
147
+ exit 0
148
+ end
149
+ end
150
+ parser.parse! ARGV
151
+
152
+ if selected.nil?
153
+ return aargh('Must select an expression or subset to use for output.', 2)
154
+ end
155
+
156
+ rules = {}
157
+ ARGV.each do |filename|
158
+ r = load_verified(filename, 'subset')
159
+ return 5 if r.nil?
160
+ return 6 unless check_rules(r, filename)
161
+ merge_any(r) # The 'any' field exists to avoid duplicating rules by hand.
162
+ merge_rules(rules, r)
163
+ end
164
+
165
+ if rules.fetch('sets', {}).key? selected
166
+ # Refers directly to subset so other expressions do not matter.
167
+ rules['expressions'] = { selected => selected }
168
+ elsif !rules.fetch('expressions', {}).key? selected
169
+ return aargh("Selected #{selected} is not expression or subset name.", 2)
170
+ end
171
+
172
+ doc = load_verified(input, input_output_schema)
173
+ return 2 if doc.nil?
174
+
175
+ begin
176
+ work = work_copy(doc)
177
+ return 3 if work.nil?
178
+ rescue StandardError
179
+ return aargh('Error processing input.', 3)
180
+ end
181
+
182
+ chosen = gather(work, rules, selected, [], {})
183
+ return chosen unless chosen.is_a?(Set)
184
+ prepare_output(doc, work, chosen)
185
+ save_verified(output, doc, 4, input_output_schema)
186
+ end
187
+
188
+ exit(main) if (defined? $unit_test).nil?
@@ -1,14 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Copyright © 2021, 2022 Ismo Kärkkäinen
4
+ # Copyright © 2021-2023 Ismo Kärkkäinen
5
5
  # Licensed under Universal Permissive License. See LICENSE.txt.
6
6
 
7
7
  require_relative '../lib/common'
8
8
  require 'optparse'
9
- require 'yaml'
10
- require 'json'
11
- require 'pathname'
12
9
 
13
10
 
14
11
  def convert(src)
@@ -63,10 +60,10 @@ def convert(src)
63
60
  { 'edges' => edges, 'nodes' => nodes }
64
61
  end
65
62
 
66
- $INPUT = nil
67
- $OUTPUT = nil
68
-
69
63
  def main
64
+ input = nil
65
+ output = nil
66
+ output_schema = 'nodes'
70
67
  parser = OptionParser.new do |opts|
71
68
  opts.summary_indent = ' '
72
69
  opts.summary_width = 20
@@ -74,24 +71,28 @@ def main
74
71
  opts.separator ''
75
72
  opts.separator 'Options:'
76
73
  opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
77
- $INPUT = filename
74
+ input = filename
78
75
  end
79
76
  opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
80
- $OUTPUT = filename
77
+ output = filename
81
78
  end
82
79
  opts.on('-h', '--help', 'Print this help and exit.') do
83
80
  $stdout.puts opts
84
81
  $stdout.puts %(
85
82
  Output is a YAML file with "name" used as "label" and "shape" used as is
86
- for nodes. For edges "tail" and "head" are used as the first and second element
87
- of "between" with the "name" as the identifier. Compound objects are ignored.
83
+ for nodes. For edges "tail" and "head" are used as the first and second
84
+ element of "between" with the "name" as the identifier. Compound objects
85
+ are ignored.
86
+
87
+ Output YAML file schema is returned by:
88
+ diagrammatron-schema #{output_schema}
88
89
  )
89
90
  exit 0
90
91
  end
91
92
  end
92
93
  parser.parse! ARGV
93
94
 
94
- doc = load_source
95
+ doc = load_source(input)
95
96
  exit(2) if doc.nil?
96
97
 
97
98
  begin
@@ -101,19 +102,7 @@ of "between" with the "name" as the identifier. Compound objects are ignored.
101
102
  end
102
103
  exit(3) if out.nil?
103
104
 
104
- begin
105
- if $OUTPUT.nil?
106
- $stdout.puts YAML.dump(out)
107
- else
108
- fp = Pathname.new $OUTPUT
109
- fp.open('w') do |f|
110
- f.puts YAML.dump(out)
111
- end
112
- end
113
- rescue StandardError => e
114
- aargh("#{e}\nFailed to write output: #{$OUTPUT}")
115
- exit 4
116
- end
105
+ save_verified(output, pout, 4, output_schema)
117
106
  end
118
107
 
119
108
  main if (defined? $unit_test).nil?
data/lib/common.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright © 2021, 2022 Ismo Kärkkäinen
3
+ # Copyright © 2021-2023 Ismo Kärkkäinen
4
4
  # Licensed under Universal Permissive License. See LICENSE.txt.
5
5
 
6
+ require 'json_schemer'
7
+ require 'yaml'
8
+ require 'set'
9
+
10
+
6
11
  def aargh(message, return_value = nil)
7
12
  message = (message.map &(:to_s)).join("\n") if message.is_a? Array
8
13
  $stderr.puts message
@@ -20,9 +25,41 @@ end
20
25
  def load_source_hash(input)
21
26
  src = load_source(input)
22
27
  unless src.nil?
23
- return aargh('Input is not a mapping.') unless src.is_a? Hash
28
+ return aargh("#{input} is not a simple mapping.") unless src.is_a? Hash
29
+ end
30
+ src
31
+ end
32
+
33
+ def list_schemas
34
+ (Dir.entries(File.dirname(__FILE__)).select { |name| name.upcase.end_with?('.YAML') }).map { |name| name[0, name.size - 5] }
35
+ end
36
+
37
+ def load_schema(schema_name)
38
+ YAML.safe_load_file(File.join(File.dirname(__FILE__), "#{schema_name}.yaml"))
39
+ end
40
+
41
+ def make_schemer(schema, reading)
42
+ JSONSchemer.schema(JSON.generate(schema),
43
+ meta_schema: 'https://json-schema.org/draft/2020-12/schema',
44
+ insert_property_defaults: reading)
45
+ end
46
+
47
+ def load_verified(input, schema_name)
48
+ src = load_source(input)
49
+ unless src.nil?
50
+ s = load_schema(schema_name)
51
+ schemer = make_schemer(s, true)
52
+ errs = schemer.validate(src).to_a
53
+ unless errs.empty?
54
+ aargh (errs.map { |e| e['error'] }).join("\n")
55
+ src = nil
56
+ end
24
57
  end
25
58
  src
59
+ rescue Errno::ENOENT
60
+ aargh "Could not load #{schema_name}"
61
+ rescue StandardError => e
62
+ aargh "Internal error: #{e}\n#{e.backtrace.join("\n")}\nFailed to read #{schema_name}"
26
63
  end
27
64
 
28
65
  def dump_result(output, doc, error_return)
@@ -38,3 +75,14 @@ def dump_result(output, doc, error_return)
38
75
  rescue StandardError => e
39
76
  aargh([ e, "Failed to write output: #{output || 'stdout'}" ], error_return)
40
77
  end
78
+
79
+ def save_verified(output, doc, error_return, schema_name)
80
+ s = load_schema(schema_name)
81
+ schemer = make_schemer(s, false)
82
+ errs = schemer.validate(doc).to_a
83
+ unless errs.empty?
84
+ aargh (errs.map { |e| e['error'] }).join("\n")
85
+ return error_return
86
+ end
87
+ dump_result(output, YAML.dump(doc, line_width: 1_000_000), error_return)
88
+ end
data/lib/edges.yaml ADDED
@@ -0,0 +1,45 @@
1
+ type: object
2
+ properties:
3
+ edges:
4
+ type: array
5
+ minItems: 0
6
+ default: []
7
+ items:
8
+ $ref: "#/$defs/Edge"
9
+ nodes:
10
+ type: array
11
+ minItems: 0
12
+ default: []
13
+ items:
14
+ $ref: "#/$defs/Node"
15
+ additionalProperties: true
16
+ $defs:
17
+ Node:
18
+ type: object
19
+ required:
20
+ - label
21
+ - sid
22
+ - xo
23
+ - yo
24
+ properties:
25
+ label:
26
+ type: string
27
+ sid:
28
+ type: integer
29
+ xo:
30
+ type: integer
31
+ yo:
32
+ type: integer
33
+ additionalProperties: true
34
+ Edge:
35
+ type: object
36
+ required:
37
+ - between
38
+ properties:
39
+ between:
40
+ type: array
41
+ items:
42
+ type: string
43
+ minItems: 2
44
+ maxItems: 2
45
+ additionalProperties: true
data/lib/nodes.yaml ADDED
@@ -0,0 +1,36 @@
1
+ type: object
2
+ properties:
3
+ edges:
4
+ type: array
5
+ minItems: 0
6
+ default: []
7
+ items:
8
+ $ref: "#/$defs/Edge"
9
+ nodes:
10
+ type: array
11
+ minItems: 0
12
+ default: []
13
+ items:
14
+ $ref: "#/$defs/Node"
15
+ additionalProperties: true
16
+ $defs:
17
+ Node:
18
+ type: object
19
+ required:
20
+ - label
21
+ properties:
22
+ label:
23
+ type: string
24
+ additionalProperties: true
25
+ Edge:
26
+ type: object
27
+ required:
28
+ - between
29
+ properties:
30
+ between:
31
+ type: array
32
+ items:
33
+ type: string
34
+ minItems: 2
35
+ maxItems: 2
36
+ additionalProperties: true
data/lib/place.yaml ADDED
@@ -0,0 +1,65 @@
1
+ type: object
2
+ properties:
3
+ edges:
4
+ type: array
5
+ minItems: 0
6
+ default: []
7
+ items:
8
+ $ref: "#/$defs/Edge"
9
+ nodes:
10
+ type: array
11
+ minItems: 0
12
+ default: []
13
+ items:
14
+ $ref: "#/$defs/Node"
15
+ additionalProperties: true
16
+ $defs:
17
+ Node:
18
+ type: object
19
+ required:
20
+ - label
21
+ - sid
22
+ - xo
23
+ - yo
24
+ properties:
25
+ label:
26
+ type: string
27
+ sid:
28
+ type: integer
29
+ xo:
30
+ type: integer
31
+ yo:
32
+ type: integer
33
+ additionalProperties: true
34
+ Edge:
35
+ type: object
36
+ required:
37
+ - between
38
+ - path
39
+ - sid
40
+ properties:
41
+ between:
42
+ type: array
43
+ items:
44
+ type: string
45
+ minItems: 2
46
+ maxItems: 2
47
+ path:
48
+ type: array
49
+ items:
50
+ $ref: "#/$defs/Position"
51
+ minItems: 2
52
+ sid:
53
+ type: integer
54
+ additionalProperties: true
55
+ Position:
56
+ type: object
57
+ required:
58
+ - xo
59
+ - yo
60
+ properties:
61
+ xo:
62
+ type: number
63
+ yo:
64
+ type: number
65
+
data/lib/render.yaml ADDED
@@ -0,0 +1,111 @@
1
+ type: object
2
+ properties:
3
+ diagram:
4
+ type: object
5
+ default: {}
6
+ properties:
7
+ style:
8
+ $ref: "#/$defs/Style"
9
+ default: 'default'
10
+ edges:
11
+ type: array
12
+ minItems: 0
13
+ default: []
14
+ items:
15
+ $ref: "#/$defs/Edge"
16
+ nodes:
17
+ type: array
18
+ minItems: 0
19
+ default: []
20
+ items:
21
+ $ref: "#/$defs/Node"
22
+ styles:
23
+ type: object
24
+ properties:
25
+ node:
26
+ type: object
27
+ properties:
28
+ default:
29
+ type: object
30
+ default: {}
31
+ additionalProperties: true
32
+ edge:
33
+ type: object
34
+ default: {}
35
+ properties:
36
+ default:
37
+ type: object
38
+ default: {}
39
+ additionalProperties: true
40
+ diagram:
41
+ type: object
42
+ default: {}
43
+ properties:
44
+ default:
45
+ type: object
46
+ default: {}
47
+ additionalProperties: true
48
+ additionalProperties: true
49
+ additionalProperties: true
50
+ $defs:
51
+ Node:
52
+ type: object
53
+ required:
54
+ - label
55
+ - sid
56
+ - xo
57
+ - yo
58
+ properties:
59
+ label:
60
+ type: string
61
+ sid:
62
+ type: integer
63
+ xo:
64
+ type: integer
65
+ yo:
66
+ type: integer
67
+ style:
68
+ $ref: "#/$defs/Style"
69
+ default: 'default'
70
+ additionalProperties: true
71
+ Edge:
72
+ type: object
73
+ required:
74
+ - between
75
+ - path
76
+ - sid
77
+ properties:
78
+ between:
79
+ type: array
80
+ items:
81
+ type: string
82
+ minItems: 2
83
+ maxItems: 2
84
+ path:
85
+ type: array
86
+ items:
87
+ $ref: "#/$defs/Position"
88
+ minItems: 2
89
+ sid:
90
+ type: integer
91
+ style:
92
+ $ref: "#/$defs/Style"
93
+ default: 'default'
94
+ additionalProperties: true
95
+ Position:
96
+ type: object
97
+ required:
98
+ - xo
99
+ - yo
100
+ properties:
101
+ xo:
102
+ type: number
103
+ yo:
104
+ type: number
105
+ Style:
106
+ oneOf:
107
+ - type: string
108
+ - type: array
109
+ items:
110
+ type: string
111
+