Module: Scjson

Defined in:
lib/scjson.rb,
lib/scjson/cli.rb,
lib/scjson/version.rb

Overview

Agent Name: ruby-scjson-version

Part of the scjson project. Developed by Softoboros Technology Inc. Licensed under the BSD 1-Clause License.

Constant Summary collapse

XMLNS =
'http://www.w3.org/2005/07/scxml'.freeze
ATTRIBUTE_MAP =
{
  'datamodel' => 'datamodel_attribute',
  'initial' => 'initial_attribute',
  'type' => 'type_value',
  'raise' => 'raise_value'
}.freeze
COLLAPSE_ATTRS =
%w[expr cond event target delay location name src id].freeze
SCXML_ELEMENTS =
%w[
  scxml state parallel final history transition invoke finalize datamodel data
  onentry onexit log send cancel raise assign script foreach param if elseif
  else content donedata initial
].freeze
STRUCTURAL_FIELDS =
%w[
  state parallel final history transition invoke finalize datamodel data
  onentry onexit log send cancel raise assign script foreach param if_value
  elseif else_value raise_value content donedata initial
].freeze
PRESERVE_EMPTY_KEYS =
%w[expr cond event target id name label text].freeze
ALWAYS_KEEP_KEYS =
%w[else_value else final onentry].freeze
VERSION =
'0.1.4'

Class Method Summary collapse

Class Method Details

.convert_scjson_file(src, dest, verify) ⇒ void

This method returns an undefined value.

Convert a single scjson document to SCXML.

Parameters:

  • src (String, Pathname)

    Input scjson file path.

  • dest (Pathname)

    Target SCXML file path.

  • verify (Boolean)

    When true, only validate round-tripping without writing.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/scjson/cli.rb', line 158

def self.convert_scjson_file(src, dest, verify)
  json_str = File.read(src)
  begin
    xml_str = Scjson.json_to_xml(json_str)
    if verify
      Scjson.xml_to_json(xml_str)
      puts "Verified #{src}"
    else
      FileUtils.mkdir_p(dest.dirname)
      File.write(dest, xml_str)
      puts "Wrote #{dest}"
    end
  rescue StandardError => e
    warn "Failed to convert #{src}: #{e}"
  end
end

.convert_scxml_file(src, dest, verify, keep_empty) ⇒ void

This method returns an undefined value.

Convert a single SCXML document to scjson.

Parameters:

  • src (String, Pathname)

    Input SCXML file path.

  • dest (Pathname)

    Target path for scjson output.

  • verify (Boolean)

    When true, only validate round-tripping without writing.

  • keep_empty (Boolean)

    When true, retain empty containers in JSON output.



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/scjson/cli.rb', line 134

def self.convert_scxml_file(src, dest, verify, keep_empty)
  xml_str = File.read(src)
  begin
    json_str = Scjson.xml_to_json(xml_str, !keep_empty)
    if verify
      Scjson.json_to_xml(json_str)
      puts "Verified #{src}"
    else
      FileUtils.mkdir_p(dest.dirname)
      File.write(dest, json_str)
      puts "Wrote #{dest}"
    end
  rescue StandardError => e
    warn "Failed to convert #{src}: #{e}"
  end
end

.handle_json(path, opt) ⇒ void

This method returns an undefined value.

Convert SCXML inputs to scjson outputs.

Handles both file and directory inputs, preserving relative paths when writing to directories.

Parameters:

  • path (Pathname)

    Source file or directory.

  • opt (Hash)

    Options hash controlling output and recursion behaviour.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/scjson/cli.rb', line 78

def self.handle_json(path, opt)
  if path.directory?
    out_dir = opt[:output] ? Pathname.new(opt[:output]) : path
    pattern = opt[:recursive] ? '**/*.scxml' : '*.scxml'
    Dir.glob(path.join(pattern).to_s).each do |src|
      rel = Pathname.new(src).relative_path_from(path)
      dest = out_dir.join(rel).sub_ext('.scjson')
      convert_scxml_file(src, dest, opt[:verify], opt[:keep_empty])
    end
  else
    dest = if opt[:output]
             p = Pathname.new(opt[:output])
             p.directory? ? p.join(path.basename.sub_ext('.scjson')) : p
           else
             path.sub_ext('.scjson')
           end
    convert_scxml_file(path, dest, opt[:verify], opt[:keep_empty])
  end
end

.handle_xml(path, opt) ⇒ void

This method returns an undefined value.

Convert scjson inputs to SCXML outputs.

Handles both file and directory inputs.

Parameters:

  • path (Pathname)

    Source file or directory.

  • opt (Hash)

    Options hash controlling output and recursion behaviour.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/scjson/cli.rb', line 106

def self.handle_xml(path, opt)
  if path.directory?
    out_dir = opt[:output] ? Pathname.new(opt[:output]) : path
    pattern = opt[:recursive] ? '**/*.scjson' : '*.scjson'
    Dir.glob(path.join(pattern).to_s).each do |src|
      rel = Pathname.new(src).relative_path_from(path)
      dest = out_dir.join(rel).sub_ext('.scxml')
      convert_scjson_file(src, dest, opt[:verify])
    end
  else
    dest = if opt[:output]
             p = Pathname.new(opt[:output])
             p.directory? ? p.join(path.basename.sub_ext('.scxml')) : p
           else
             path.sub_ext('.scxml')
           end
    convert_scjson_file(path, dest, opt[:verify])
  end
end

.help_textString

Render the help text describing CLI usage.

Returns:

  • (String)

    A one-line summary of the CLI purpose.



65
66
67
# File 'lib/scjson/cli.rb', line 65

def self.help_text
  'scjson - SCXML <-> scjson converter and validator'
end

.json_to_xml(json_str) ⇒ String

Convert a canonical scjson document back to SCXML.

Parameters:

  • json_str (String)

    Canonical scjson input.

Returns:

  • (String)

    XML document encoded as UTF-8.



63
64
65
66
67
68
69
70
71
# File 'lib/scjson.rb', line 63

def json_to_xml(json_str)
  data = JSON.parse(json_str)
  remove_empty(data)
  doc = Nokogiri::XML::Document.new
  doc.encoding = 'utf-8'
  root = build_element(doc, 'scxml', data)
  doc.root = root
  doc.to_xml
end

.main(argv = ARGV) ⇒ void

This method returns an undefined value.

Command line interface for scjson conversions.

Parameters:

  • argv (Array<String>) (defaults to: ARGV)

    Command line arguments provided by the user.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/scjson/cli.rb', line 28

def self.main(argv = ARGV)
  options = { recursive: false, verify: false, keep_empty: false }
  cmd = argv.shift
  if cmd.nil? || %w[-h --help].include?(cmd)
    puts(help_text)
    return
  end
  parser = OptionParser.new do |opts|
    opts.banner = ''
    opts.on('-o', '--output PATH', 'output file or directory') { |v| options[:output] = v }
    opts.on('-r', '--recursive', 'recurse into directories') { options[:recursive] = true }
    opts.on('-v', '--verify', 'verify conversion without writing output') { options[:verify] = true }
    opts.on('--keep-empty', 'keep null or empty items when producing JSON') { options[:keep_empty] = true }
  end
  path = argv.shift
  parser.parse!(argv)
  unless path
    puts(help_text)
    return
  end
  splash
  case cmd
  when 'json'
    handle_json(Pathname.new(path), options)
  when 'xml'
    handle_xml(Pathname.new(path), options)
  when 'validate'
    validate(Pathname.new(path), options[:recursive])
  else
    puts(help_text)
  end
end

.splashvoid

This method returns an undefined value.

Display the CLI program header.



19
20
21
# File 'lib/scjson/cli.rb', line 19

def self.splash
  puts "scjson #{VERSION} - SCXML <-> scjson converter"
end

.validate(path, recursive) ⇒ void

This method returns an undefined value.

Validate a file or directory tree of SCXML and scjson documents.

Parameters:

  • path (Pathname)

    File or directory to validate.

  • recursive (Boolean)

    When true, traverse subdirectories.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/scjson/cli.rb', line 181

def self.validate(path, recursive)
  success = true
  if path.directory?
    pattern = recursive ? '**/*' : '*'
    Dir.glob(path.join(pattern).to_s).each do |f|
      next unless File.file?(f)
      next unless f.end_with?('.scxml', '.scjson')
      success &= validate_file(f)
    end
  else
    success &= validate_file(path.to_s)
  end
  exit(1) unless success
end

.validate_file(src) ⇒ Boolean

Validate a single SCXML or scjson document by round-tripping.

Parameters:

  • src (String)

    Path to the document to validate.

Returns:

  • (Boolean)

    True when the document validates successfully.



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/scjson/cli.rb', line 201

def self.validate_file(src)
  begin
    data = File.read(src)
    if src.end_with?('.scxml')
      json = Scjson.xml_to_json(data)
      Scjson.json_to_xml(json)
    elsif src.end_with?('.scjson')
      xml = Scjson.json_to_xml(data)
      Scjson.xml_to_json(xml)
    else
      return true
    end
    true
  rescue StandardError => e
    warn "Validation failed for #{src}: #{e}"
    false
  end
end

.xml_to_json(xml_str, omit_empty = true) ⇒ String

Convert an SCXML document to its canonical scjson form.

Parameters:

  • xml_str (String)

    SCXML source document.

  • omit_empty (Boolean) (defaults to: true)

    Remove empty containers when true.

Returns:

  • (String)

    Canonical scjson output.

Raises:

  • (ArgumentError)


47
48
49
50
51
52
53
54
55
56
# File 'lib/scjson.rb', line 47

def xml_to_json(xml_str, omit_empty = true)
  doc = Nokogiri::XML(xml_str) { |cfg| cfg.strict.nonet }
  root = locate_root(doc)
  raise ArgumentError, 'Document missing <scxml> root element' unless root

  map = element_to_hash(root)
  collapse_whitespace(map)
  remove_empty(map) if omit_empty
  JSON.pretty_generate(map)
end