lutaml 0.3.2 → 0.4.1.pre.alpha.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.
- checksums.yaml +4 -4
- data/.github/workflows/macos.yml +2 -0
- data/.github/workflows/ubuntu.yml +2 -0
- data/.github/workflows/windows.yml +7 -0
- data/README.adoc +19 -1
- data/exe/lutaml +22 -0
- data/lib/lutaml/command_line.rb +262 -0
- data/lib/lutaml/express/lutaml_path/document_wrapper.rb +3 -45
- data/lib/lutaml/formatter.rb +19 -0
- data/lib/lutaml/formatter/base.rb +66 -0
- data/lib/lutaml/formatter/graphviz.rb +332 -0
- data/lib/lutaml/layout/engine.rb +15 -0
- data/lib/lutaml/layout/graph_viz_engine.rb +18 -0
- data/lib/lutaml/lutaml_path/document_wrapper.rb +10 -4
- data/lib/lutaml/parser.rb +42 -8
- data/lib/lutaml/version.rb +1 -1
- data/lutaml.gemspec +5 -2
- metadata +13 -5
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 0e2accd48e5932fbf111f54429604e7709a98418d6b9c97af40f29c8547a7238
         | 
| 4 | 
            +
              data.tar.gz: 6561a16f4d783ddc50812af99e3da854dc2a8673be5e2cc9e7adcffa1678099f
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 3006890a278d837602f7c3149c46b7810a7f5e367100862eac17ac7e54ef2bf18623b9bb8a98483b12cec3fe52bfbef1e3f3c40b7231b7d669c31b0b5e3548f1
         | 
| 7 | 
            +
              data.tar.gz: c509f969284fc89e5a4d3293426a71ff861aa8283f33193114887d55d2173427a988998db906d18ea94e1a821db07147a213f892052241dd383c6e36556fa2b0
         | 
    
        data/.github/workflows/macos.yml
    CHANGED
    
    
| @@ -30,6 +30,13 @@ jobs: | |
| 30 30 | 
             
                    uses: actions/setup-ruby@v1
         | 
| 31 31 | 
             
                    with:
         | 
| 32 32 | 
             
                      ruby-version: ${{ matrix.ruby }}
         | 
| 33 | 
            +
                  - name: Install graphviz
         | 
| 34 | 
            +
                    uses: nick-invision/retry@v1
         | 
| 35 | 
            +
                    with:
         | 
| 36 | 
            +
                      polling_interval_seconds: 5
         | 
| 37 | 
            +
                      timeout_minutes: 5
         | 
| 38 | 
            +
                      max_attempts: 3
         | 
| 39 | 
            +
                      command: choco install --no-progress graphviz --version 2.38.0.20190211
         | 
| 33 40 | 
             
                  - name: Update gems
         | 
| 34 41 | 
             
                    shell: pwsh
         | 
| 35 42 | 
             
                    run: |
         | 
    
        data/README.adoc
    CHANGED
    
    | @@ -30,7 +30,9 @@ $ gem install lutaml | |
| 30 30 |  | 
| 31 31 | 
             
            === Usage
         | 
| 32 32 |  | 
| 33 | 
            -
             | 
| 33 | 
            +
            == From ruby
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            In order to parse files supported by lutaml extensions, use Lutaml::Parser.parse method.
         | 
| 34 36 |  | 
| 35 37 | 
             
            [source,ruby]
         | 
| 36 38 | 
             
            ----
         | 
| @@ -38,6 +40,22 @@ In order to parse files supported by lutaml extensions, use Lutaml::Parser.parse | |
| 38 40 | 
             
            Lutaml::Parser.parse(File.new("example.exp")) # will produce Lutaml::LutamlPath::DocumentWrapper object with serialized express repository
         | 
| 39 41 | 
             
            ----
         | 
| 40 42 |  | 
| 43 | 
            +
            == With cli tool
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            There is a cli tool available for parsing lutaml/exp files(also yaml datastruct files are supported).
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            [source,bash]
         | 
| 48 | 
            +
            ----
         | 
| 49 | 
            +
            # Will generate `test.dot` file in the current directory
         | 
| 50 | 
            +
            $: lutaml -o . test.lutaml
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            # Will generate `test.png` file in the `assets` directory
         | 
| 53 | 
            +
            $: lutaml -o assets -t png test.lutaml
         | 
| 54 | 
            +
            ----
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            For additional info refer to `lutaml --help output`
         | 
| 57 | 
            +
             | 
| 58 | 
            +
             | 
| 41 59 | 
             
            == Development
         | 
| 42 60 |  | 
| 43 61 | 
             
            After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
         | 
    
        data/exe/lutaml
    ADDED
    
    | @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # encoding: UTF-8
         | 
| 3 | 
            +
            # frozen_string_literal: true
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            # resolve bin path, ignoring symlinks
         | 
| 6 | 
            +
            require "pathname"
         | 
| 7 | 
            +
            bin_file = Pathname.new(__FILE__).realpath
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            # add self to libpath
         | 
| 10 | 
            +
            $:.unshift File.expand_path("../../lib", bin_file)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            # Fixes https://github.com/rubygems/rubygems/issues/1420
         | 
| 13 | 
            +
            require "rubygems/specification"
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            class Gem::Specification
         | 
| 16 | 
            +
              def this; self; end
         | 
| 17 | 
            +
            end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            require "lutaml"
         | 
| 20 | 
            +
            require "lutaml/command_line"
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            Lutaml::CommandLine.run(ARGV.dup, STDOUT)
         | 
| @@ -0,0 +1,262 @@ | |
| 1 | 
            +
            require "optparse"
         | 
| 2 | 
            +
            require "pathname"
         | 
| 3 | 
            +
            require "lutaml/formatter"
         | 
| 4 | 
            +
            require "lutaml/uml/has_attributes"
         | 
| 5 | 
            +
            require "lutaml/uml/parsers/attribute"
         | 
| 6 | 
            +
            require "lutaml/uml/parsers/dsl"
         | 
| 7 | 
            +
            require "lutaml/uml/parsers/yaml"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Lutaml
         | 
| 10 | 
            +
              class CommandLine
         | 
| 11 | 
            +
                class Error < StandardError; end
         | 
| 12 | 
            +
                class FileError < Error; end
         | 
| 13 | 
            +
                class NotSupportedInputFormat < Error; end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                include ::Lutaml::Uml::HasAttributes
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                SUPPORTED_FORMATS = %w[yaml lutaml exp].freeze
         | 
| 18 | 
            +
                DEFAULT_INPUT_FORMAT = "lutaml".freeze
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def self.run(args, out_object, attributes = {})
         | 
| 21 | 
            +
                  new(attributes, out_object).run(args)
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def initialize(attributes = {}, out_object = STDOUT)
         | 
| 25 | 
            +
                  @formatter = ::Lutaml::Formatter::Graphviz.new
         | 
| 26 | 
            +
                  @verbose = false
         | 
| 27 | 
            +
                  @option_parser = OptionParser.new
         | 
| 28 | 
            +
                  @out_object = out_object
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  setup_parser_options
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  # rubocop:disable Rails/ActiveRecordAliases
         | 
| 33 | 
            +
                  update_attributes(attributes)
         | 
| 34 | 
            +
                  # rubocop:enable Rails/ActiveRecordAliases
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def output_path=(value)
         | 
| 38 | 
            +
                  @output_path = determine_output_path_value(value)
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def determine_output_path_value(value)
         | 
| 42 | 
            +
                  unless value.nil? || @output_path = value.is_a?(Pathname)
         | 
| 43 | 
            +
                    return Pathname.new(value.to_s)
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  value
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def paths=(values)
         | 
| 50 | 
            +
                  @paths = values.to_a.map { |path| Pathname.new(path) }
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def formatter=(value)
         | 
| 54 | 
            +
                  value = value.to_s.strip.downcase.to_sym
         | 
| 55 | 
            +
                  value = Lutaml::Uml::Formatter.find_by(name: value)
         | 
| 56 | 
            +
                  raise Error, "Formatter not found: #{value}" if value.nil?
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  @formatter = value
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def input_format=(value)
         | 
| 62 | 
            +
                  if value.nil?
         | 
| 63 | 
            +
                    @input_format = DEFAULT_INPUT_FORMAT
         | 
| 64 | 
            +
                    return
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  @input_format = SUPPORTED_FORMATS.detect { |n| n == value }
         | 
| 68 | 
            +
                  raise(NotSupportedInputFormat, value) if @input_format.nil?
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def run(original_args)
         | 
| 72 | 
            +
                  args = original_args.dup
         | 
| 73 | 
            +
                  @option_parser.parse!(args) rescue nil
         | 
| 74 | 
            +
                  @paths = args
         | 
| 75 | 
            +
                  @formatter.type = @type
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  if @output_path&.file? && @paths.length > 1
         | 
| 78 | 
            +
                    raise Error,
         | 
| 79 | 
            +
                          'Output path must be a directory \
         | 
| 80 | 
            +
                          if multiple input files are given'
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  @paths.each do |input_path_string|
         | 
| 84 | 
            +
                    input_path = Pathname.new(input_path_string)
         | 
| 85 | 
            +
                    unless input_path.exist?
         | 
| 86 | 
            +
                      raise FileError, "File does not exist: #{input_path}"
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    document = Lutaml::Parser
         | 
| 90 | 
            +
                      .parse_into_document(File.new(input_path), @input_format)
         | 
| 91 | 
            +
                      .first
         | 
| 92 | 
            +
                    result = @formatter.format(document)
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    if @output_path
         | 
| 95 | 
            +
                      output_path = @output_path
         | 
| 96 | 
            +
                      if output_path.directory?
         | 
| 97 | 
            +
                        output_path = output_path.join(input_path
         | 
| 98 | 
            +
                                                        .basename(".*").to_s +
         | 
| 99 | 
            +
                                                      ".#{@formatter.type}")
         | 
| 100 | 
            +
                      end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                      output_path.open("w+") { |file| file.write(result) }
         | 
| 103 | 
            +
                    else
         | 
| 104 | 
            +
                      @out_object.puts(result)
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                protected
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def text_bold(body = nil)
         | 
| 112 | 
            +
                  text_effect(1, body)
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def text_italic(body = nil)
         | 
| 116 | 
            +
                  text_effect(3, body)
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def text_bold_italic(body = nil)
         | 
| 120 | 
            +
                  text_bold(text_italic(body))
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def text_underline(body = nil)
         | 
| 124 | 
            +
                  text_effect(4, body)
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def text_effect(num, body = nil)
         | 
| 128 | 
            +
                  result = "\e[#{num}m"
         | 
| 129 | 
            +
                  result << "#{body}#{text_reset}" unless body.nil?
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  result
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                def text_reset
         | 
| 135 | 
            +
                  "\e[0m"
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                def setup_parser_options
         | 
| 139 | 
            +
                  @option_parser.banner = ""
         | 
| 140 | 
            +
                  format_desc = "The output formatter (Default: '#{@formatter.name}')"
         | 
| 141 | 
            +
                  @option_parser
         | 
| 142 | 
            +
                    .on("-f",
         | 
| 143 | 
            +
                        "--formatter VALUE",
         | 
| 144 | 
            +
                        format_desc) do |value|
         | 
| 145 | 
            +
                    @formatter = value
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
                  @option_parser
         | 
| 148 | 
            +
                    .on("-t", "--type VALUE", "The output format type") do |value|
         | 
| 149 | 
            +
                      @type = value
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
                  @option_parser
         | 
| 152 | 
            +
                    .on("-o", "--output VALUE", "The output path") do |value|
         | 
| 153 | 
            +
                      @output_path = Pathname.new(value)
         | 
| 154 | 
            +
                    end
         | 
| 155 | 
            +
                  @option_parser
         | 
| 156 | 
            +
                    .on("-i", "--input-format VALUE", "The input format") do |value|
         | 
| 157 | 
            +
                      @input_format = value
         | 
| 158 | 
            +
                    end
         | 
| 159 | 
            +
                  @option_parser
         | 
| 160 | 
            +
                    .on("-h", "--help", "Prints this help") do
         | 
| 161 | 
            +
                    print_help
         | 
| 162 | 
            +
                    exit
         | 
| 163 | 
            +
                  end
         | 
| 164 | 
            +
                  @option_parser.on("-g", "--graph VALUE") do |value|
         | 
| 165 | 
            +
                    Parsers::Attribute.parse(value).each do |key, attr_value|
         | 
| 166 | 
            +
                      @formatter.graph[key] = attr_value
         | 
| 167 | 
            +
                    end
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                  @option_parser.on("-e", "--edge VALUE") do |value|
         | 
| 171 | 
            +
                    Parsers::Attribute.parse(value).each do |key, attr_value|
         | 
| 172 | 
            +
                      @formatter.edge[key] = attr_value
         | 
| 173 | 
            +
                    end
         | 
| 174 | 
            +
                  end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                  @option_parser.on("-n", "--node VALUE") do |value|
         | 
| 177 | 
            +
                    Parsers::Attribute.parse(value).each do |key, attr_value|
         | 
| 178 | 
            +
                      @formatter.node[key] = attr_value
         | 
| 179 | 
            +
                    end
         | 
| 180 | 
            +
                  end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                  @option_parser.on("-a", "--all VALUE") do |value|
         | 
| 183 | 
            +
                    Parsers::Attribute.parse(value).each do |key, attr_value|
         | 
| 184 | 
            +
                      @formatter.graph[key] = attr_value
         | 
| 185 | 
            +
                      @formatter.edge[key] = attr_value
         | 
| 186 | 
            +
                      @formatter.node[key] = attr_value
         | 
| 187 | 
            +
                    end
         | 
| 188 | 
            +
                  end
         | 
| 189 | 
            +
                end
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                def print_help
         | 
| 192 | 
            +
                  @out_object.puts <<~HELP
         | 
| 193 | 
            +
                    #{text_bold('Usage:')} lutaml [options] PATHS
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                    #{text_bold('Overview:')} Generate output from Supplied language files if supported
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                    #{text_bold('Options:')}
         | 
| 198 | 
            +
                    #{@option_parser}
         | 
| 199 | 
            +
                    #{text_bold('Paths:')}
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                        LUTAML can accept multiple paths for parsing for easier batch processing.
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                        The location of the output by default is standard output.
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                        The output can be directed to a path with #{text_bold_italic('--output')}, which can be a file or a directory.
         | 
| 206 | 
            +
                        If the output path is a directory, then the filename will be the same as the input filename,
         | 
| 207 | 
            +
                          with it's file extension substituted with the #{text_bold_italic('--type')}.
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                        #{text_underline('Examples')}
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                            `lutaml project.lutaml`
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                                Produces DOT notation, sent to standard output
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                            `lutaml -o . project.lutaml`
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                                Produces DOT notation, written to #{text_italic('./project.dot')}
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                            `lutaml -o ./diagram.dot project.lutaml`
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                                Produces DOT notation, written to #{text_italic('./diagram.dot')}
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                            `lutaml -o ./diagram.png project.lutaml`
         | 
| 224 | 
            +
             | 
| 225 | 
            +
                                Produces PNG image, written to #{text_italic('./diagram.png')}
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                            `lutaml -t png -o . project.lutaml`
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                                Produces PNG image, written to #{text_italic('./project.png')}
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                            `lutaml -t png -o . project.lutaml core_ext.lutaml`
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                                Produces PNG images, written to #{text_italic('./project.png')} and #{text_italic('./core_ext.png')}
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                    #{text_bold('Inputs:')}
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                        #{text_underline('Lutaml')}
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                          Lutaml dsl syntax files, supports diagram generation(image or dot files) with Graphviz
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                    #{text_bold('Formatters:')}
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                        #{text_underline('Graphviz')}
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                          Generates DOT notation and can use the DOT notation to generate any format Graphviz can produce.
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                          The output format is based on #{text_bold_italic('--type')}, which by default is "dot".
         | 
| 248 | 
            +
                          If #{text_bold_italic('--type')} is not given and #{text_bold_italic('--output')} is, the file extension of the #{text_bold_italic('--output')} path will be used.
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                          Valid types/extensions are: #{Lutaml::Formatter::Graphviz::VALID_TYPES.join(', ')}
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                          #{text_bold('Options:')}
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                              -g, --graph VALUE                The graph attributes
         | 
| 255 | 
            +
                              -e, --edge VALUE                 The edge attributes
         | 
| 256 | 
            +
                              -n, --node VALUE                 The node attributes
         | 
| 257 | 
            +
                              -a, --all VALUE                  Set attributes for graph, edge, and node
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                  HELP
         | 
| 260 | 
            +
                end
         | 
| 261 | 
            +
              end
         | 
| 262 | 
            +
            end
         | 
| @@ -1,57 +1,15 @@ | |
| 1 1 | 
             
            require "lutaml/lutaml_path/document_wrapper"
         | 
| 2 | 
            -
            require "expressir/express_exp/ | 
| 2 | 
            +
            require "expressir/express_exp/hyperlink_formatter"
         | 
| 3 3 |  | 
| 4 4 | 
             
            module Lutaml
         | 
| 5 5 | 
             
              module Express
         | 
| 6 6 | 
             
                module LutamlPath
         | 
| 7 7 | 
             
                  class DocumentWrapper < ::Lutaml::LutamlPath::DocumentWrapper
         | 
| 8 | 
            -
                    SCHEMA_ATTRIBUTES = %w[
         | 
| 9 | 
            -
                      id
         | 
| 10 | 
            -
                      constants
         | 
| 11 | 
            -
                      declarations
         | 
| 12 | 
            -
                      entities
         | 
| 13 | 
            -
                      functions
         | 
| 14 | 
            -
                      interfaces
         | 
| 15 | 
            -
                      procedures
         | 
| 16 | 
            -
                      remarks
         | 
| 17 | 
            -
                      rules
         | 
| 18 | 
            -
                      subtype_constraints
         | 
| 19 | 
            -
                      types
         | 
| 20 | 
            -
                      version
         | 
| 21 | 
            -
                    ].freeze
         | 
| 22 | 
            -
                    SOURCE_CODE_ATTRIBUTE_NAME = "sourcecode".freeze
         | 
| 23 | 
            -
             | 
| 24 8 | 
             
                    protected
         | 
| 25 9 |  | 
| 26 10 | 
             
                    def serialize_document(repository)
         | 
| 27 | 
            -
                      repository | 
| 28 | 
            -
                         | 
| 29 | 
            -
                        serialized_schema = SCHEMA_ATTRIBUTES
         | 
| 30 | 
            -
                          .each_with_object({}) do |name, nested_res|
         | 
| 31 | 
            -
                          attr_value = schema.send(name)
         | 
| 32 | 
            -
                          nested_res[name] = serialize_value(attr_value)
         | 
| 33 | 
            -
                          if name == "entities"
         | 
| 34 | 
            -
                            nested_res[name] = merge_source_code_attr(nested_res[name],
         | 
| 35 | 
            -
                                                                      attr_value)
         | 
| 36 | 
            -
                          end
         | 
| 37 | 
            -
                        end
         | 
| 38 | 
            -
                        res[schema.id] = serialized_schema
         | 
| 39 | 
            -
                        serialized_schema = serialized_schema
         | 
| 40 | 
            -
                          .merge(SOURCE_CODE_ATTRIBUTE_NAME =>
         | 
| 41 | 
            -
                                                      entity_source_code(schema))
         | 
| 42 | 
            -
                        res["schemas"].push(serialized_schema)
         | 
| 43 | 
            -
                      end
         | 
| 44 | 
            -
                    end
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                    def merge_source_code_attr(serialized_entries, entities)
         | 
| 47 | 
            -
                      serialized_entries.map do |serialized|
         | 
| 48 | 
            -
                        entity = entities.detect { |n| n.id == serialized["id"] }
         | 
| 49 | 
            -
                        serialized.merge(SOURCE_CODE_ATTRIBUTE_NAME => entity_source_code(entity))
         | 
| 50 | 
            -
                      end
         | 
| 51 | 
            -
                    end
         | 
| 52 | 
            -
             | 
| 53 | 
            -
                    def entity_source_code(entity)
         | 
| 54 | 
            -
                      Expressir::ExpressExp::Formatter.format(entity)
         | 
| 11 | 
            +
                      repository
         | 
| 12 | 
            +
                        .to_hash(formatter: Expressir::ExpressExp::HyperlinkFormatter)['schemas']
         | 
| 55 13 | 
             
                    end
         | 
| 56 14 | 
             
                  end
         | 
| 57 15 | 
             
                end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Lutaml
         | 
| 4 | 
            +
              module Formatter
         | 
| 5 | 
            +
                class << self
         | 
| 6 | 
            +
                  def all
         | 
| 7 | 
            +
                    @all ||= []
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def find_by_name(name)
         | 
| 11 | 
            +
                    name = name.to_sym
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    all.detect { |formatter_class| formatter_class.name == name }
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            require "lutaml/formatter/graphviz"
         | 
| @@ -0,0 +1,66 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "lutaml/formatter"
         | 
| 4 | 
            +
            require "lutaml/uml/has_attributes"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Lutaml
         | 
| 7 | 
            +
              module Formatter
         | 
| 8 | 
            +
                class Base
         | 
| 9 | 
            +
                  class << self
         | 
| 10 | 
            +
                    def inherited(subclass)
         | 
| 11 | 
            +
                      Formatter.all << subclass
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    def format(node, attributes = {})
         | 
| 15 | 
            +
                      new(attributes).format(node)
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    def name
         | 
| 19 | 
            +
                      to_s.split("::").last.downcase.to_sym
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  include ::Lutaml::Uml::HasAttributes
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  # rubocop:disable Rails/ActiveRecordAliases
         | 
| 26 | 
            +
                  def initialize(attributes = {})
         | 
| 27 | 
            +
                    update_attributes(attributes)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                  # rubocop:enable Rails/ActiveRecordAliases
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def name
         | 
| 32 | 
            +
                    self.class.name
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  attr_reader :type
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def type=(value)
         | 
| 38 | 
            +
                    @type = value.to_s.strip.downcase.to_sym
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  def format(node)
         | 
| 42 | 
            +
                    case node
         | 
| 43 | 
            +
                    when ::Lutaml::Uml::Node::Field  then format_field(node)
         | 
| 44 | 
            +
                    when ::Lutaml::Uml::Node::Method then format_method(node)
         | 
| 45 | 
            +
                    when ::Lutaml::Uml::Node::Relationship then format_relationship(node)
         | 
| 46 | 
            +
                    when ::Lutaml::Uml::Node::ClassRelationship
         | 
| 47 | 
            +
                      then format_class_relationship(node)
         | 
| 48 | 
            +
                    when ::Lutaml::Uml::Node::ClassNode then format_class(node)
         | 
| 49 | 
            +
                    when Lutaml::Uml::Document then format_document(node)
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def format_field(_node); raise NotImplementedError; end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def format_method(_node); raise NotImplementedError; end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def format_relationship(_node); raise NotImplementedError; end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def format_class_relationship(_node); raise NotImplementedError; end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def format_class(_node); raise NotImplementedError; end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def format_document(_node); raise NotImplementedError; end
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
            end
         | 
| @@ -0,0 +1,332 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "open3"
         | 
| 4 | 
            +
            require "lutaml/formatter/base"
         | 
| 5 | 
            +
            require "lutaml/layout/graph_viz_engine"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Lutaml
         | 
| 8 | 
            +
              module Formatter
         | 
| 9 | 
            +
                class Graphviz < Base
         | 
| 10 | 
            +
                  class Attributes < Hash
         | 
| 11 | 
            +
                    def to_s
         | 
| 12 | 
            +
                      to_a
         | 
| 13 | 
            +
                        .reject { |(_k, val)| val.nil? }
         | 
| 14 | 
            +
                        .map { |(a, b)| "#{a}=#{b.inspect}" }
         | 
| 15 | 
            +
                        .join(" ")
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  ACCESS_SYMBOLS = {
         | 
| 20 | 
            +
                    "public"    => "+",
         | 
| 21 | 
            +
                    "protected" => "#",
         | 
| 22 | 
            +
                    "private"   => "-",
         | 
| 23 | 
            +
                  }.freeze
         | 
| 24 | 
            +
                  DEFAULT_CLASS_FONT = "Helvetica".freeze
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  VALID_TYPES = %i[
         | 
| 27 | 
            +
                    dot
         | 
| 28 | 
            +
                    xdot
         | 
| 29 | 
            +
                    ps
         | 
| 30 | 
            +
                    pdf
         | 
| 31 | 
            +
                    svg
         | 
| 32 | 
            +
                    svgz
         | 
| 33 | 
            +
                    fig
         | 
| 34 | 
            +
                    png
         | 
| 35 | 
            +
                    gif
         | 
| 36 | 
            +
                    jpg
         | 
| 37 | 
            +
                    jpeg
         | 
| 38 | 
            +
                    json
         | 
| 39 | 
            +
                    imap
         | 
| 40 | 
            +
                    cmapx
         | 
| 41 | 
            +
                  ].freeze
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def initialize(attributes = {})
         | 
| 44 | 
            +
                    super
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    @graph = Attributes.new
         | 
| 47 | 
            +
                    # Associations lines style, `true` gives curved lines
         | 
| 48 | 
            +
                    # https://graphviz.org/doc/info/attrs.html#d:splines
         | 
| 49 | 
            +
                    @graph["splines"] = "ortho"
         | 
| 50 | 
            +
                    # Padding between outside of picture and nodes
         | 
| 51 | 
            +
                    @graph["pad"] = 0.5
         | 
| 52 | 
            +
                    # Padding between levels
         | 
| 53 | 
            +
                    @graph["ranksep"] = "1.2.equally"
         | 
| 54 | 
            +
                    # Padding between nodes
         | 
| 55 | 
            +
                    @graph["nodesep"] = "1.2.equally"
         | 
| 56 | 
            +
                    # TODO: set rankdir
         | 
| 57 | 
            +
                    # @graph['rankdir'] = 'BT'
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    @edge = Attributes.new
         | 
| 60 | 
            +
                    @edge["color"] = "gray50"
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    @node = Attributes.new
         | 
| 63 | 
            +
                    @node["shape"] = "box"
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    @type = :dot
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  attr_reader :graph
         | 
| 69 | 
            +
                  attr_reader :edge
         | 
| 70 | 
            +
                  attr_reader :node
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def type=(value)
         | 
| 73 | 
            +
                    super
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    @type = :dot unless VALID_TYPES.include?(@type)
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def format(node)
         | 
| 79 | 
            +
                    dot = super.lines.map(&:rstrip).join("\n")
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                    generate_from_dot(dot)
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  def escape_html_chars(text)
         | 
| 85 | 
            +
                    text
         | 
| 86 | 
            +
                      .gsub(/</, "<")
         | 
| 87 | 
            +
                      .gsub(/>/, ">")
         | 
| 88 | 
            +
                      .gsub(/\[/, "[")
         | 
| 89 | 
            +
                      .gsub(/\]/, "]")
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  def format_field(node)
         | 
| 93 | 
            +
                    symbol = ACCESS_SYMBOLS[node.visibility]
         | 
| 94 | 
            +
                    result = "#{symbol}#{node.name}"
         | 
| 95 | 
            +
                    if node.type
         | 
| 96 | 
            +
                      keyword = node.keyword ? "«#{node.keyword}»" : ""
         | 
| 97 | 
            +
                      result += " : #{keyword}#{node.type}"
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
                    if node.cardinality
         | 
| 100 | 
            +
                      result += "[#{node.cardinality[:min]}..#{node.cardinality[:max]}]"
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
                    result = escape_html_chars(result)
         | 
| 103 | 
            +
                    result = "<U>#{result}</U>" if node.static
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    result
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  def format_method(node)
         | 
| 109 | 
            +
                    symbol = ACCESS_SYMBOLS[node.access]
         | 
| 110 | 
            +
                    result = "#{symbol} #{node.name}"
         | 
| 111 | 
            +
                    if node.arguments
         | 
| 112 | 
            +
                      arguments = node.arguments.map do |argument|
         | 
| 113 | 
            +
                        "#{argument.name}#{" : #{argument.type}" if argument.type}"
         | 
| 114 | 
            +
                      end.join(", ")
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    result << "(#{arguments})"
         | 
| 118 | 
            +
                    result << " : #{node.type}" if node.type
         | 
| 119 | 
            +
                    result = "<U>#{result}</U>" if node.static
         | 
| 120 | 
            +
                    result = "<I>#{result}</I>" if node.abstract
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                    result
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  def format_relationship(node)
         | 
| 126 | 
            +
                    graph_parent_name = generate_graph_name(node.owner_end)
         | 
| 127 | 
            +
                    graph_node_name = generate_graph_name(node.member_end)
         | 
| 128 | 
            +
                    attributes = generate_graph_relationship_attributes(node)
         | 
| 129 | 
            +
                    graph_attributes = " [#{attributes}]" unless attributes.empty?
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    %{#{graph_parent_name} -> #{graph_node_name}#{graph_attributes}}
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  def generate_graph_relationship_attributes(node)
         | 
| 135 | 
            +
                    attributes = Attributes.new
         | 
| 136 | 
            +
                    if %w[dependency realizes].include?(node.member_end_type)
         | 
| 137 | 
            +
                      attributes["style"] = "dashed"
         | 
| 138 | 
            +
                    end
         | 
| 139 | 
            +
                    attributes["dir"] = if node.owner_end_type && node.member_end_type
         | 
| 140 | 
            +
                                          "both"
         | 
| 141 | 
            +
                                        elsif node.owner_end_type
         | 
| 142 | 
            +
                                          "back"
         | 
| 143 | 
            +
                                        else
         | 
| 144 | 
            +
                                          "direct"
         | 
| 145 | 
            +
                                        end
         | 
| 146 | 
            +
                    attributes["label"] = node.action if node.action
         | 
| 147 | 
            +
                    if node.owner_end_attribute_name
         | 
| 148 | 
            +
                      attributes["headlabel"] = format_label(
         | 
| 149 | 
            +
                        node.owner_end_attribute_name,
         | 
| 150 | 
            +
                        node.owner_end_cardinality
         | 
| 151 | 
            +
                      )
         | 
| 152 | 
            +
                    end
         | 
| 153 | 
            +
                    if node.member_end_attribute_name
         | 
| 154 | 
            +
                      attributes["taillabel"] = format_label(
         | 
| 155 | 
            +
                        node.member_end_attribute_name,
         | 
| 156 | 
            +
                        node.member_end_cardinality
         | 
| 157 | 
            +
                      )
         | 
| 158 | 
            +
                    end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                    attributes["arrowtail"] = case node.owner_end_type
         | 
| 161 | 
            +
                                              when "composition"
         | 
| 162 | 
            +
                                                "diamond"
         | 
| 163 | 
            +
                                              when "aggregation"
         | 
| 164 | 
            +
                                                "odiamond"
         | 
| 165 | 
            +
                                              when "direct"
         | 
| 166 | 
            +
                                                "vee"
         | 
| 167 | 
            +
                                              else
         | 
| 168 | 
            +
                                                "onormal"
         | 
| 169 | 
            +
                                              end
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                    attributes["arrowhead"] = case node.member_end_type
         | 
| 172 | 
            +
                                              when "composition"
         | 
| 173 | 
            +
                                                "diamond"
         | 
| 174 | 
            +
                                              when "aggregation"
         | 
| 175 | 
            +
                                                "odiamond"
         | 
| 176 | 
            +
                                              when "direct"
         | 
| 177 | 
            +
                                                "vee"
         | 
| 178 | 
            +
                                              else
         | 
| 179 | 
            +
                                                "onormal"
         | 
| 180 | 
            +
                                              end
         | 
| 181 | 
            +
                    # swap labels and arrows if `dir` eq to `back`
         | 
| 182 | 
            +
                    if attributes["dir"] == "back"
         | 
| 183 | 
            +
                      attributes["arrowhead"], attributes["arrowtail"] =
         | 
| 184 | 
            +
                        [attributes["arrowtail"], attributes["arrowhead"]]
         | 
| 185 | 
            +
                      attributes["headlabel"], attributes["taillabel"] =
         | 
| 186 | 
            +
                        [attributes["taillabel"], attributes["headlabel"]]
         | 
| 187 | 
            +
                    end
         | 
| 188 | 
            +
                    attributes
         | 
| 189 | 
            +
                  end
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                  def format_label(name, cardinality = {})
         | 
| 192 | 
            +
                    res = "+#{name}"
         | 
| 193 | 
            +
                    if cardinality.nil? ||
         | 
| 194 | 
            +
                        (cardinality["min"].nil? || cardinality["max"].nil?)
         | 
| 195 | 
            +
                      return res
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                    "#{res} #{cardinality['min']}..#{cardinality['max']}"
         | 
| 199 | 
            +
                  end
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                  def format_member_rows(members, hide_members)
         | 
| 202 | 
            +
                    unless !hide_members && members && members.length.positive?
         | 
| 203 | 
            +
                      return <<~HEREDOC.chomp
         | 
| 204 | 
            +
                        <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
         | 
| 205 | 
            +
                          <TR><TD ALIGN="LEFT"></TD></TR>
         | 
| 206 | 
            +
                        </TABLE>
         | 
| 207 | 
            +
                      HEREDOC
         | 
| 208 | 
            +
                    end
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                    field_rows = members.map do |field|
         | 
| 211 | 
            +
                      %{<TR><TD ALIGN="LEFT">#{format_field(field)}</TD></TR>}
         | 
| 212 | 
            +
                    end
         | 
| 213 | 
            +
                    field_table = <<~HEREDOC.chomp
         | 
| 214 | 
            +
                      <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
         | 
| 215 | 
            +
                        #{field_rows.map { |row| ' ' * 10 + row }.join("\n")}
         | 
| 216 | 
            +
                      </TABLE>
         | 
| 217 | 
            +
                    HEREDOC
         | 
| 218 | 
            +
                    field_table << "\n" << " " * 6
         | 
| 219 | 
            +
                    field_table
         | 
| 220 | 
            +
                  end
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                  def format_class(node, hide_members)
         | 
| 223 | 
            +
                    name = ["<B>#{node.name}</B>"]
         | 
| 224 | 
            +
                    name.unshift("«#{node.keyword}»") if node.keyword
         | 
| 225 | 
            +
                    name_html = <<~HEREDOC
         | 
| 226 | 
            +
                      <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
         | 
| 227 | 
            +
                        #{name.map { |n| %(<TR><TD ALIGN="CENTER">#{n}</TD></TR>) }.join('\n')}
         | 
| 228 | 
            +
                      </TABLE>
         | 
| 229 | 
            +
                    HEREDOC
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                    field_table = format_member_rows(node.attributes, hide_members)
         | 
| 232 | 
            +
                    method_table = format_member_rows(node.methods, hide_members)
         | 
| 233 | 
            +
                    table_body = [name_html, field_table, method_table].map do |type|
         | 
| 234 | 
            +
                      next if type.nil?
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                      <<~TEXT
         | 
| 237 | 
            +
                        <TR>
         | 
| 238 | 
            +
                          <TD>#{type}</TD>
         | 
| 239 | 
            +
                        </TR>
         | 
| 240 | 
            +
                      TEXT
         | 
| 241 | 
            +
                    end
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                    <<~HEREDOC.chomp
         | 
| 244 | 
            +
                      <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="10">
         | 
| 245 | 
            +
                        #{table_body.compact.join("\n")}
         | 
| 246 | 
            +
                      </TABLE>
         | 
| 247 | 
            +
                    HEREDOC
         | 
| 248 | 
            +
                  end
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                  def format_document(node)
         | 
| 251 | 
            +
                    @fontname = node.fontname || DEFAULT_CLASS_FONT
         | 
| 252 | 
            +
                    @node["fontname"] = "#{@fontname}-bold"
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                    if node.fidelity
         | 
| 255 | 
            +
                      hide_members = node.fidelity["hideMembers"]
         | 
| 256 | 
            +
                      hide_other_classes = node.fidelity["hideOtherClasses"]
         | 
| 257 | 
            +
                    end
         | 
| 258 | 
            +
                    classes = (node.classes +
         | 
| 259 | 
            +
                                node.enums +
         | 
| 260 | 
            +
                                node.data_types +
         | 
| 261 | 
            +
                                node.primitives).map do |class_node|
         | 
| 262 | 
            +
                      graph_node_name = generate_graph_name(class_node.name)
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                      <<~HEREDOC
         | 
| 265 | 
            +
                        #{graph_node_name} [
         | 
| 266 | 
            +
                          shape="plain"
         | 
| 267 | 
            +
                          fontname="#{@fontname || DEFAULT_CLASS_FONT}"
         | 
| 268 | 
            +
                          label=<#{format_class(class_node, hide_members)}>]
         | 
| 269 | 
            +
                      HEREDOC
         | 
| 270 | 
            +
                    end.join("\n")
         | 
| 271 | 
            +
                    associations = node.classes.map(&:associations).compact.flatten +
         | 
| 272 | 
            +
                      node.associations
         | 
| 273 | 
            +
                    if node.groups
         | 
| 274 | 
            +
                      associations = sort_by_document_groupping(node.groups,
         | 
| 275 | 
            +
                                                                associations)
         | 
| 276 | 
            +
                    end
         | 
| 277 | 
            +
                    classes_names = node.classes.map(&:name)
         | 
| 278 | 
            +
                    associations = associations.map do |assoc_node|
         | 
| 279 | 
            +
                      if hide_other_classes &&
         | 
| 280 | 
            +
                          !classes_names.include?(assoc_node.member_end)
         | 
| 281 | 
            +
                        next
         | 
| 282 | 
            +
                      end
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                      format_relationship(assoc_node)
         | 
| 285 | 
            +
                    end.join("\n")
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                    classes = classes.lines.map { |line| "  #{line}" }.join.chomp
         | 
| 288 | 
            +
                    associations = associations
         | 
| 289 | 
            +
                      .lines.map { |line| "  #{line}" }.join.chomp
         | 
| 290 | 
            +
             | 
| 291 | 
            +
                    <<~HEREDOC
         | 
| 292 | 
            +
                      digraph G {
         | 
| 293 | 
            +
                        graph [#{@graph}]
         | 
| 294 | 
            +
                        edge [#{@edge}]
         | 
| 295 | 
            +
                        node [#{@node}]
         | 
| 296 | 
            +
             | 
| 297 | 
            +
                      #{classes}
         | 
| 298 | 
            +
             | 
| 299 | 
            +
                      #{associations}
         | 
| 300 | 
            +
                      }
         | 
| 301 | 
            +
                    HEREDOC
         | 
| 302 | 
            +
                  end
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                  protected
         | 
| 305 | 
            +
             | 
| 306 | 
            +
                  def sort_by_document_groupping(groups, associations)
         | 
| 307 | 
            +
                    result = []
         | 
| 308 | 
            +
                    groups.each do |batch|
         | 
| 309 | 
            +
                      batch.each do |group_name|
         | 
| 310 | 
            +
                        associations
         | 
| 311 | 
            +
                          .select { |assc| assc.owner_end == group_name }
         | 
| 312 | 
            +
                          .each do |association|
         | 
| 313 | 
            +
                            result.push(association) unless result.include?(association)
         | 
| 314 | 
            +
                          end
         | 
| 315 | 
            +
                      end
         | 
| 316 | 
            +
                    end
         | 
| 317 | 
            +
                    associations.each do |association|
         | 
| 318 | 
            +
                      result.push(association) unless result.include?(association)
         | 
| 319 | 
            +
                    end
         | 
| 320 | 
            +
                    result
         | 
| 321 | 
            +
                  end
         | 
| 322 | 
            +
             | 
| 323 | 
            +
                  def generate_from_dot(input)
         | 
| 324 | 
            +
                    Lutaml::Layout::GraphVizEngine.new(input: input).render(@type)
         | 
| 325 | 
            +
                  end
         | 
| 326 | 
            +
             | 
| 327 | 
            +
                  def generate_graph_name(name)
         | 
| 328 | 
            +
                    name.gsub(/[^0-9a-zA-Z]/i, "")
         | 
| 329 | 
            +
                  end
         | 
| 330 | 
            +
                end
         | 
| 331 | 
            +
              end
         | 
| 332 | 
            +
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "ruby-graphviz"
         | 
| 4 | 
            +
            require "lutaml/layout/engine"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Lutaml
         | 
| 7 | 
            +
              module Layout
         | 
| 8 | 
            +
                class GraphVizEngine < Engine
         | 
| 9 | 
            +
                  def render(type)
         | 
| 10 | 
            +
                    Open3.popen3("dot -T#{type}") do |stdin, stdout, _stderr, _wait|
         | 
| 11 | 
            +
                      stdin.puts(input)
         | 
| 12 | 
            +
                      stdin.close
         | 
| 13 | 
            +
                      stdout.read
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| @@ -22,18 +22,24 @@ module Lutaml | |
| 22 22 | 
             
                      return attr_value.map(&method(:serialize_to_hash))
         | 
| 23 23 | 
             
                    end
         | 
| 24 24 |  | 
| 25 | 
            -
                    attr_value
         | 
| 25 | 
            +
                    serialize_to_hash(attr_value)
         | 
| 26 26 | 
             
                  end
         | 
| 27 27 |  | 
| 28 28 | 
             
                  def serialize_to_hash(object)
         | 
| 29 | 
            -
                    return object if [String, Integer, Float].include?(object.class)
         | 
| 29 | 
            +
                    return object if [String, Integer, Float, FalseClass, TrueClass, Symbol, NilClass].include?(object.class)
         | 
| 30 30 |  | 
| 31 31 | 
             
                    object.instance_variables.each_with_object({}) do |var, res|
         | 
| 32 32 | 
             
                      variable = object.instance_variable_get(var)
         | 
| 33 33 | 
             
                      res[var.to_s.gsub("@", "")] = if variable.is_a?(Array)
         | 
| 34 | 
            -
                                                      variable.map  | 
| 34 | 
            +
                                                      variable.map do |n|
         | 
| 35 | 
            +
                                                        serialize_to_hash(n)
         | 
| 36 | 
            +
                                                      end
         | 
| 35 37 | 
             
                                                    else
         | 
| 36 | 
            -
                                                      variable
         | 
| 38 | 
            +
                                                      if [String, Integer, Float, FalseClass, TrueClass, Symbol, NilClass].include?(variable.class) || var == :@parent
         | 
| 39 | 
            +
                                                        variable
         | 
| 40 | 
            +
                                                      else
         | 
| 41 | 
            +
                                                        serialize_to_hash(variable)
         | 
| 42 | 
            +
                                                      end
         | 
| 37 43 | 
             
                                                    end
         | 
| 38 44 | 
             
                    end
         | 
| 39 45 | 
             
                  end
         | 
    
        data/lib/lutaml/parser.rb
    CHANGED
    
    | @@ -4,20 +4,54 @@ require "lutaml/uml/lutaml_path/document_wrapper" | |
| 4 4 | 
             
            require "lutaml/express/lutaml_path/document_wrapper"
         | 
| 5 5 |  | 
| 6 6 | 
             
            module Lutaml
         | 
| 7 | 
            -
               | 
| 8 | 
            -
                 | 
| 7 | 
            +
              class Parser
         | 
| 8 | 
            +
                attr_reader :parse_type, :file_list
         | 
| 9 9 |  | 
| 10 | 
            -
                 | 
| 11 | 
            -
                   | 
| 10 | 
            +
                class << self
         | 
| 11 | 
            +
                  def parse(file_list, input_type = nil)
         | 
| 12 | 
            +
                    file_list = file_list.is_a?(Array) ? file_list : [file_list]
         | 
| 13 | 
            +
                    new(Array(file_list), input_type).parse
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def parse_into_document(file_list, input_type = nil)
         | 
| 17 | 
            +
                    file_list = file_list.is_a?(Array) ? file_list : [file_list]
         | 
| 18 | 
            +
                    new(Array(file_list), input_type).parse_into_document
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def initialize(file_list, input_type)
         | 
| 23 | 
            +
                  @parse_type = input_type ? input_type : File.extname(file_list.first.path)[1..-1]
         | 
| 24 | 
            +
                  @file_list = file_list
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def parse
         | 
| 28 | 
            +
                  documents = parse_into_document
         | 
| 29 | 
            +
                  return [document_wrapper(documents)] if parse_type == "exp"
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  documents.map { |doc| document_wrapper(doc) }
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def parse_into_document
         | 
| 35 | 
            +
                  case parse_type
         | 
| 12 36 | 
             
                  when "exp"
         | 
| 13 | 
            -
                     | 
| 14 | 
            -
                      .new(Lutaml::Express::Parsers::Exp.parse(file))
         | 
| 37 | 
            +
                    Expressir::ExpressExp::Parser.from_files(file_list.map(&:path))
         | 
| 15 38 | 
             
                  when "lutaml"
         | 
| 16 | 
            -
                    Lutaml::Uml:: | 
| 17 | 
            -
             | 
| 39 | 
            +
                    file_list.map { |file| Lutaml::Uml::Parsers::Dsl.parse(file) }
         | 
| 40 | 
            +
                  when "yml"
         | 
| 41 | 
            +
                    file_list.map { |file| Lutaml::Uml::Parsers::Yaml.parse(file.path) }
         | 
| 18 42 | 
             
                  else
         | 
| 19 43 | 
             
                    raise ArgumentError, "Unsupported file format"
         | 
| 20 44 | 
             
                  end
         | 
| 21 45 | 
             
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                private
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def document_wrapper(document)
         | 
| 50 | 
            +
                  if parse_type == "exp"
         | 
| 51 | 
            +
                    return Lutaml::Express::LutamlPath::DocumentWrapper.new(document)
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  Lutaml::Uml::LutamlPath::DocumentWrapper.new(document)
         | 
| 55 | 
            +
                end
         | 
| 22 56 | 
             
              end
         | 
| 23 57 | 
             
            end
         | 
    
        data/lib/lutaml/version.rb
    CHANGED
    
    
    
        data/lutaml.gemspec
    CHANGED
    
    | @@ -16,9 +16,12 @@ Gem::Specification.new do |spec| | |
| 16 16 | 
             
              spec.metadata["changelog_uri"] = "https://github.com/lutaml/lutaml/releases"
         | 
| 17 17 |  | 
| 18 18 | 
             
              # Specify which files should be added to the gem when it is released.
         | 
| 19 | 
            -
              # The `git ls-files -z` loads the files in the RubyGem | 
| 19 | 
            +
              # The `git ls-files -z` loads the files in the RubyGem
         | 
| 20 | 
            +
              # that have been added into git.
         | 
| 20 21 | 
             
              spec.files = Dir.chdir(File.expand_path(__dir__)) do
         | 
| 21 | 
            -
                `git ls-files -z | 
| 22 | 
            +
                `git ls-files -z`
         | 
| 23 | 
            +
                  .split("\x0")
         | 
| 24 | 
            +
                  .reject { |f| f.match(%r{^(test|spec|features)/}) }
         | 
| 22 25 | 
             
              end
         | 
| 23 26 | 
             
              spec.bindir        = "exe"
         | 
| 24 27 | 
             
              spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: lutaml
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.4.1.pre.alpha.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Ribose Inc.
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2021- | 
| 11 | 
            +
            date: 2021-02-26 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: lutaml-express
         | 
| @@ -153,7 +153,8 @@ dependencies: | |
| 153 153 | 
             
            description: 'LutaML: data models in textual form'
         | 
| 154 154 | 
             
            email:
         | 
| 155 155 | 
             
            - open.source@ribose.com'
         | 
| 156 | 
            -
            executables: | 
| 156 | 
            +
            executables:
         | 
| 157 | 
            +
            - lutaml
         | 
| 157 158 | 
             
            extensions: []
         | 
| 158 159 | 
             
            extra_rdoc_files: []
         | 
| 159 160 | 
             
            files:
         | 
| @@ -169,8 +170,15 @@ files: | |
| 169 170 | 
             
            - Rakefile
         | 
| 170 171 | 
             
            - bin/console
         | 
| 171 172 | 
             
            - bin/setup
         | 
| 173 | 
            +
            - exe/lutaml
         | 
| 172 174 | 
             
            - lib/lutaml.rb
         | 
| 175 | 
            +
            - lib/lutaml/command_line.rb
         | 
| 173 176 | 
             
            - lib/lutaml/express/lutaml_path/document_wrapper.rb
         | 
| 177 | 
            +
            - lib/lutaml/formatter.rb
         | 
| 178 | 
            +
            - lib/lutaml/formatter/base.rb
         | 
| 179 | 
            +
            - lib/lutaml/formatter/graphviz.rb
         | 
| 180 | 
            +
            - lib/lutaml/layout/engine.rb
         | 
| 181 | 
            +
            - lib/lutaml/layout/graph_viz_engine.rb
         | 
| 174 182 | 
             
            - lib/lutaml/lutaml_path/document_wrapper.rb
         | 
| 175 183 | 
             
            - lib/lutaml/parser.rb
         | 
| 176 184 | 
             
            - lib/lutaml/uml/lutaml_path/document_wrapper.rb
         | 
| @@ -194,9 +202,9 @@ required_ruby_version: !ruby/object:Gem::Requirement | |
| 194 202 | 
             
                  version: '0'
         | 
| 195 203 | 
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 196 204 | 
             
              requirements:
         | 
| 197 | 
            -
              - - " | 
| 205 | 
            +
              - - ">"
         | 
| 198 206 | 
             
                - !ruby/object:Gem::Version
         | 
| 199 | 
            -
                  version:  | 
| 207 | 
            +
                  version: 1.3.1
         | 
| 200 208 | 
             
            requirements: []
         | 
| 201 209 | 
             
            rubygems_version: 3.0.3
         | 
| 202 210 | 
             
            signing_key:
         |