diver_down 0.0.1.alpha1
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +132 -0
- data/exe/diver_down_web +55 -0
- data/lib/diver_down/definition/dependency.rb +107 -0
- data/lib/diver_down/definition/method_id.rb +83 -0
- data/lib/diver_down/definition/source.rb +90 -0
- data/lib/diver_down/definition.rb +112 -0
- data/lib/diver_down/helper.rb +81 -0
- data/lib/diver_down/trace/call_stack.rb +45 -0
- data/lib/diver_down/trace/ignored_method_ids.rb +136 -0
- data/lib/diver_down/trace/module_set/array_module_set.rb +31 -0
- data/lib/diver_down/trace/module_set/const_source_location_module_set.rb +28 -0
- data/lib/diver_down/trace/module_set.rb +78 -0
- data/lib/diver_down/trace/redefine_ruby_methods.rb +64 -0
- data/lib/diver_down/trace/tracer/session.rb +121 -0
- data/lib/diver_down/trace/tracer.rb +96 -0
- data/lib/diver_down/trace.rb +27 -0
- data/lib/diver_down/version.rb +5 -0
- data/lib/diver_down/web/action.rb +344 -0
- data/lib/diver_down/web/bit_id.rb +41 -0
- data/lib/diver_down/web/definition_enumerator.rb +54 -0
- data/lib/diver_down/web/definition_loader.rb +37 -0
- data/lib/diver_down/web/definition_store.rb +89 -0
- data/lib/diver_down/web/definition_to_dot.rb +399 -0
- data/lib/diver_down/web/dev_server_middleware.rb +72 -0
- data/lib/diver_down/web/indented_string_io.rb +59 -0
- data/lib/diver_down/web/module_store.rb +59 -0
- data/lib/diver_down/web.rb +101 -0
- data/lib/diver_down-trace.rb +4 -0
- data/lib/diver_down-web.rb +4 -0
- data/lib/diver_down.rb +14 -0
- data/web/assets/CjLq7LhZ.css +1 -0
- data/web/assets/bundle.js +978 -0
- data/web/index.html +13 -0
- metadata +122 -0
| @@ -0,0 +1,399 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'json'
         | 
| 4 | 
            +
            require 'cgi'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module DiverDown
         | 
| 7 | 
            +
              class Web
         | 
| 8 | 
            +
                class DefinitionToDot
         | 
| 9 | 
            +
                  ATTRIBUTE_DELIMITER = ' '
         | 
| 10 | 
            +
                  MODULE_DELIMITER = '::'
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  # Between modules is prominently distanced
         | 
| 13 | 
            +
                  MODULE_MINLEN = 3
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  class MetadataStore
         | 
| 16 | 
            +
                    Metadata = Data.define(:id, :type, :data, :module_store) do
         | 
| 17 | 
            +
                      # @return [Hash]
         | 
| 18 | 
            +
                      def to_h
         | 
| 19 | 
            +
                        case type
         | 
| 20 | 
            +
                        when :source
         | 
| 21 | 
            +
                          source_to_h
         | 
| 22 | 
            +
                        when :dependency
         | 
| 23 | 
            +
                          dependency_to_h
         | 
| 24 | 
            +
                        when :module
         | 
| 25 | 
            +
                          module_to_h
         | 
| 26 | 
            +
                        else
         | 
| 27 | 
            +
                          raise NotImplementedError, "not implemented yet #{type}"
         | 
| 28 | 
            +
                        end
         | 
| 29 | 
            +
                      end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                      private
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                      def source_to_h
         | 
| 34 | 
            +
                        modules = module_store.get(data.source_name).map do
         | 
| 35 | 
            +
                          {
         | 
| 36 | 
            +
                            module_name: _1,
         | 
| 37 | 
            +
                          }
         | 
| 38 | 
            +
                        end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                        {
         | 
| 41 | 
            +
                          id:,
         | 
| 42 | 
            +
                          type: 'source',
         | 
| 43 | 
            +
                          source_name: data.source_name,
         | 
| 44 | 
            +
                          modules:,
         | 
| 45 | 
            +
                        }
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                      def dependency_to_h
         | 
| 49 | 
            +
                        {
         | 
| 50 | 
            +
                          id:,
         | 
| 51 | 
            +
                          type: 'dependency',
         | 
| 52 | 
            +
                          dependencies: data.map do |dependency|
         | 
| 53 | 
            +
                            {
         | 
| 54 | 
            +
                              source_name: dependency.source_name,
         | 
| 55 | 
            +
                              method_ids: dependency.method_ids.sort.map do
         | 
| 56 | 
            +
                                {
         | 
| 57 | 
            +
                                  name: _1.name,
         | 
| 58 | 
            +
                                  context: _1.context,
         | 
| 59 | 
            +
                                }
         | 
| 60 | 
            +
                              end,
         | 
| 61 | 
            +
                            }
         | 
| 62 | 
            +
                          end,
         | 
| 63 | 
            +
                        }
         | 
| 64 | 
            +
                      end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                      def module_to_h
         | 
| 67 | 
            +
                        {
         | 
| 68 | 
            +
                          id:,
         | 
| 69 | 
            +
                          type: 'module',
         | 
| 70 | 
            +
                          modules: data.map do
         | 
| 71 | 
            +
                            {
         | 
| 72 | 
            +
                              module_name: _1,
         | 
| 73 | 
            +
                            }
         | 
| 74 | 
            +
                          end,
         | 
| 75 | 
            +
                        }
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    def initialize(module_store)
         | 
| 80 | 
            +
                      @prefix = 'graph_'
         | 
| 81 | 
            +
                      @module_store = module_store
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                      # Hash{ id => Metadata }
         | 
| 84 | 
            +
                      @to_h = {}
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    # @param type [Symbol]
         | 
| 88 | 
            +
                    # @param record [DiverDown::Definition::Source]
         | 
| 89 | 
            +
                    # @return [String]
         | 
| 90 | 
            +
                    def issue_source_id(source)
         | 
| 91 | 
            +
                      build_metadata_and_return_id(:source, source)
         | 
| 92 | 
            +
                    end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    # @param dependency [DiverDown::Definition::Dependency]
         | 
| 95 | 
            +
                    # @return [String]
         | 
| 96 | 
            +
                    def issue_dependency_id(dependency)
         | 
| 97 | 
            +
                      build_metadata_and_return_id(:dependency, [dependency])
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    # @param module_names [Array<String>]
         | 
| 101 | 
            +
                    # @return [String]
         | 
| 102 | 
            +
                    def issue_modules_id(module_names)
         | 
| 103 | 
            +
                      issued_metadata = @to_h.values.find { _1.type == :module && _1.data == module_names }
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                      if issued_metadata
         | 
| 106 | 
            +
                        issued_metadata.id
         | 
| 107 | 
            +
                      else
         | 
| 108 | 
            +
                        build_metadata_and_return_id(:module, module_names)
         | 
| 109 | 
            +
                      end
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    # @param id [String]
         | 
| 113 | 
            +
                    # @param dependency [DiverDown::Definition::Dependency]
         | 
| 114 | 
            +
                    def append_dependency(id, dependency)
         | 
| 115 | 
            +
                      metadata = @to_h.fetch(id)
         | 
| 116 | 
            +
                      dependencies = metadata.data
         | 
| 117 | 
            +
                      combined_dependencies = DiverDown::Definition::Dependency.combine(*dependencies, dependency)
         | 
| 118 | 
            +
                      metadata.data.replace(combined_dependencies)
         | 
| 119 | 
            +
                    end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                    # @return [Array<Hash>]
         | 
| 122 | 
            +
                    def to_a
         | 
| 123 | 
            +
                      @to_h.values.map(&:to_h)
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    private
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    def build_metadata_and_return_id(type, data)
         | 
| 129 | 
            +
                      id = "#{@prefix}#{length + 1}"
         | 
| 130 | 
            +
                      metadata = Metadata.new(id:, type:, data:, module_store: @module_store)
         | 
| 131 | 
            +
                      @to_h[id] = metadata
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                      id
         | 
| 134 | 
            +
                    end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                    def length
         | 
| 137 | 
            +
                      @to_h.length
         | 
| 138 | 
            +
                    end
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                  # @param definition [DiverDown::Definition]
         | 
| 142 | 
            +
                  # @param module_store [DiverDown::ModuleStore]
         | 
| 143 | 
            +
                  # @param compound [Boolean]
         | 
| 144 | 
            +
                  # @param concentrate [Boolean] https://graphviz.org/docs/attrs/concentrate/
         | 
| 145 | 
            +
                  def initialize(definition, module_store, compound: false, concentrate: false, only_module: false)
         | 
| 146 | 
            +
                    @definition = definition
         | 
| 147 | 
            +
                    @module_store = module_store
         | 
| 148 | 
            +
                    @io = DiverDown::Web::IndentedStringIo.new
         | 
| 149 | 
            +
                    @indent = 0
         | 
| 150 | 
            +
                    @compound = compound || only_module # When only-module is enabled, dependencies between modules are displayed as compound.
         | 
| 151 | 
            +
                    @compound_map = Hash.new { |h, k| h[k] = {} } # Hash{ ltail => Hash{ lhead => issued id } }
         | 
| 152 | 
            +
                    @concentrate = concentrate
         | 
| 153 | 
            +
                    @only_module = only_module
         | 
| 154 | 
            +
                    @metadata_store = MetadataStore.new(module_store)
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                  # @return [Array<Hash>]
         | 
| 158 | 
            +
                  def metadata
         | 
| 159 | 
            +
                    @metadata_store.to_a
         | 
| 160 | 
            +
                  end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                  # @return [String]
         | 
| 163 | 
            +
                  def to_s
         | 
| 164 | 
            +
                    io.puts %(strict digraph "#{definition.title}" {)
         | 
| 165 | 
            +
                    io.indented do
         | 
| 166 | 
            +
                      io.puts('compound=true') if @compound
         | 
| 167 | 
            +
                      io.puts('concentrate=true') if @concentrate
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                      if @only_module
         | 
| 170 | 
            +
                        render_only_modules
         | 
| 171 | 
            +
                      else
         | 
| 172 | 
            +
                        definition.sources.sort_by(&:source_name).each do
         | 
| 173 | 
            +
                          insert_source(_1)
         | 
| 174 | 
            +
                        end
         | 
| 175 | 
            +
                      end
         | 
| 176 | 
            +
                    end
         | 
| 177 | 
            +
                    io.puts '}'
         | 
| 178 | 
            +
                    io.string
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                  private
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  attr_reader :definition, :module_store, :io
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                  def render_only_modules
         | 
| 186 | 
            +
                    # Hash{ from_module => { to_module => Array<DiverDown::Definition::Dependency> } }
         | 
| 187 | 
            +
                    dependency_map = Hash.new { |h, k| h[k] = Hash.new { |hi, ki| hi[ki] = [] } }
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                    definition.sources.sort_by(&:source_name).each do |source|
         | 
| 190 | 
            +
                      source_modules = module_store.get(source.source_name)
         | 
| 191 | 
            +
                      next if source_modules.empty?
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                      source.dependencies.each do |dependency|
         | 
| 194 | 
            +
                        dependency_modules = module_store.get(dependency.source_name)
         | 
| 195 | 
            +
                        next if dependency_modules.empty?
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                        dependency_map[source_modules][dependency_modules].push(dependency)
         | 
| 198 | 
            +
                      end
         | 
| 199 | 
            +
                    end
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                    # Remove duplicated prefix modules
         | 
| 202 | 
            +
                    # from [["A"], ["A", "B"]] to [["A", "B"]]
         | 
| 203 | 
            +
                    uniq_modules = [*dependency_map.keys, *dependency_map.values.map(&:keys).flatten(1)].uniq
         | 
| 204 | 
            +
                    uniq_modules.reject! do |modules|
         | 
| 205 | 
            +
                      modules.empty? ||
         | 
| 206 | 
            +
                        uniq_modules.any? { _1[0..modules.size - 1] == modules && _1.length > modules.size }
         | 
| 207 | 
            +
                    end
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                    uniq_modules.each do |specific_module_names|
         | 
| 210 | 
            +
                      buf = swap_io do
         | 
| 211 | 
            +
                        indexes = (0..(specific_module_names.length - 1)).to_a
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                        chain_yield(indexes) do |index, next_proc|
         | 
| 214 | 
            +
                          module_names = specific_module_names[0..index]
         | 
| 215 | 
            +
                          module_name = specific_module_names[index]
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                          io.puts %(subgraph "#{module_label(module_names)}" {)
         | 
| 218 | 
            +
                          io.indented do
         | 
| 219 | 
            +
                            io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
         | 
| 220 | 
            +
                            io.puts %(label="#{module_name}")
         | 
| 221 | 
            +
                            io.puts %("#{module_name}" #{build_attributes(label: module_name, id: @metadata_store.issue_modules_id(module_names))})
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                            next_proc&.call
         | 
| 224 | 
            +
                          end
         | 
| 225 | 
            +
                          io.puts '}'
         | 
| 226 | 
            +
                        end
         | 
| 227 | 
            +
                      end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                      io.write buf.string
         | 
| 230 | 
            +
                    end
         | 
| 231 | 
            +
             | 
| 232 | 
            +
                    dependency_map.each do |from_modules, h|
         | 
| 233 | 
            +
                      h.each do |to_modules, all_dependencies|
         | 
| 234 | 
            +
                        # Do not render standalone source
         | 
| 235 | 
            +
                        # Do not render self-dependency
         | 
| 236 | 
            +
                        next if from_modules.empty? || to_modules.empty? || from_modules == to_modules
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                        dependencies = DiverDown::Definition::Dependency.combine(*all_dependencies)
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                        dependencies.each do
         | 
| 241 | 
            +
                          attributes = {}
         | 
| 242 | 
            +
                          ltail = module_label(*from_modules)
         | 
| 243 | 
            +
                          lhead = module_label(*to_modules)
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                          # Already rendered dependencies between modules
         | 
| 246 | 
            +
                          # Add the dependency to the edge of the compound
         | 
| 247 | 
            +
                          if @compound_map[ltail].include?(lhead)
         | 
| 248 | 
            +
                            compound_id = @compound_map[ltail][lhead]
         | 
| 249 | 
            +
                            @metadata_store.append_dependency(compound_id, _1)
         | 
| 250 | 
            +
                            next
         | 
| 251 | 
            +
                          end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                          compound_id = @metadata_store.issue_dependency_id(_1)
         | 
| 254 | 
            +
                          @compound_map[ltail][lhead] = compound_id
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                          attributes.merge!(
         | 
| 257 | 
            +
                            id: compound_id,
         | 
| 258 | 
            +
                            ltail:,
         | 
| 259 | 
            +
                            lhead:,
         | 
| 260 | 
            +
                            minlen: MODULE_MINLEN
         | 
| 261 | 
            +
                          )
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                          io.write(%("#{from_modules[-1]}" -> "#{to_modules[-1]}"))
         | 
| 264 | 
            +
                          io.write(%( #{build_attributes(**attributes)}), indent: false) unless attributes.empty?
         | 
| 265 | 
            +
                          io.write("\n")
         | 
| 266 | 
            +
                        end
         | 
| 267 | 
            +
                      end
         | 
| 268 | 
            +
                    end
         | 
| 269 | 
            +
                  end
         | 
| 270 | 
            +
             | 
| 271 | 
            +
                  def insert_source(source)
         | 
| 272 | 
            +
                    if module_store.get(source.source_name).empty?
         | 
| 273 | 
            +
                      io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})
         | 
| 274 | 
            +
                    else
         | 
| 275 | 
            +
                      insert_modules(source)
         | 
| 276 | 
            +
                    end
         | 
| 277 | 
            +
             | 
| 278 | 
            +
                    source.dependencies.each do
         | 
| 279 | 
            +
                      attributes = {}
         | 
| 280 | 
            +
                      ltail = module_label(*module_store.get(source.source_name))
         | 
| 281 | 
            +
                      lhead = module_label(*module_store.get(_1.source_name))
         | 
| 282 | 
            +
             | 
| 283 | 
            +
                      if @compound && (ltail || lhead)
         | 
| 284 | 
            +
                        # Rendering of dependencies between modules is done only once
         | 
| 285 | 
            +
                        between_modules = ltail != lhead
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                        # Already rendered dependencies between modules
         | 
| 288 | 
            +
                        # Add the dependency to the edge of the compound
         | 
| 289 | 
            +
                        if between_modules && @compound_map[ltail].include?(lhead)
         | 
| 290 | 
            +
                          compound_id = @compound_map[ltail][lhead]
         | 
| 291 | 
            +
                          @metadata_store.append_dependency(compound_id, _1)
         | 
| 292 | 
            +
                          next
         | 
| 293 | 
            +
                        end
         | 
| 294 | 
            +
             | 
| 295 | 
            +
                        compound_id = @metadata_store.issue_dependency_id(_1)
         | 
| 296 | 
            +
                        @compound_map[ltail][lhead] = compound_id
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                        attributes.merge!(
         | 
| 299 | 
            +
                          id: compound_id,
         | 
| 300 | 
            +
                          ltail:,
         | 
| 301 | 
            +
                          lhead:,
         | 
| 302 | 
            +
                          minlen: MODULE_MINLEN
         | 
| 303 | 
            +
                        )
         | 
| 304 | 
            +
                      else
         | 
| 305 | 
            +
                        attributes.merge!(
         | 
| 306 | 
            +
                          id: @metadata_store.issue_dependency_id(_1)
         | 
| 307 | 
            +
                        )
         | 
| 308 | 
            +
                      end
         | 
| 309 | 
            +
             | 
| 310 | 
            +
                      io.write(%("#{source.source_name}" -> "#{_1.source_name}"))
         | 
| 311 | 
            +
                      io.write(%( #{build_attributes(**attributes)}), indent: false) unless attributes.empty?
         | 
| 312 | 
            +
                      io.write("\n")
         | 
| 313 | 
            +
                    end
         | 
| 314 | 
            +
                  end
         | 
| 315 | 
            +
             | 
| 316 | 
            +
                  def insert_modules(source)
         | 
| 317 | 
            +
                    buf = swap_io do
         | 
| 318 | 
            +
                      all_module_names = module_store.get(source.source_name)
         | 
| 319 | 
            +
                      indexes = (0..(all_module_names.length - 1)).to_a
         | 
| 320 | 
            +
             | 
| 321 | 
            +
                      chain_yield(indexes) do |index, next_proc|
         | 
| 322 | 
            +
                        module_names = all_module_names[0..index]
         | 
| 323 | 
            +
                        module_name = module_names[-1]
         | 
| 324 | 
            +
             | 
| 325 | 
            +
                        io.puts %(subgraph "#{module_label(module_names)}" {)
         | 
| 326 | 
            +
                        io.indented do
         | 
| 327 | 
            +
                          io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
         | 
| 328 | 
            +
                          io.puts %(label="#{module_name}")
         | 
| 329 | 
            +
             | 
| 330 | 
            +
                          if next_proc
         | 
| 331 | 
            +
                            next_proc.call
         | 
| 332 | 
            +
                          else
         | 
| 333 | 
            +
                            # last. equals indexes[-1] == index
         | 
| 334 | 
            +
                            io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})
         | 
| 335 | 
            +
                          end
         | 
| 336 | 
            +
                        end
         | 
| 337 | 
            +
                        io.puts '}'
         | 
| 338 | 
            +
                      end
         | 
| 339 | 
            +
                    end
         | 
| 340 | 
            +
             | 
| 341 | 
            +
                    io.write buf.string
         | 
| 342 | 
            +
                  end
         | 
| 343 | 
            +
             | 
| 344 | 
            +
                  def chain_yield(values, &block)
         | 
| 345 | 
            +
                    *head, tail = values
         | 
| 346 | 
            +
             | 
| 347 | 
            +
                    last_proc = proc do
         | 
| 348 | 
            +
                      block.call(tail, nil)
         | 
| 349 | 
            +
                    end
         | 
| 350 | 
            +
             | 
| 351 | 
            +
                    chain_proc = head.inject(last_proc) do |next_proc, value|
         | 
| 352 | 
            +
                      proc do
         | 
| 353 | 
            +
                        block.call(value, next_proc)
         | 
| 354 | 
            +
                      end
         | 
| 355 | 
            +
                    end
         | 
| 356 | 
            +
             | 
| 357 | 
            +
                    chain_proc.call
         | 
| 358 | 
            +
                  end
         | 
| 359 | 
            +
             | 
| 360 | 
            +
                  # rubocop:disable Lint/UnderscorePrefixedVariableName
         | 
| 361 | 
            +
                  # attrsの参考 https://qiita.com/rubytomato@github/items/51779135bc4b77c8c20d
         | 
| 362 | 
            +
                  def build_attributes(_wrap: '[]', **attrs)
         | 
| 363 | 
            +
                    attrs = attrs.reject { _2.nil? || _2 == '' }
         | 
| 364 | 
            +
                    return if attrs.empty?
         | 
| 365 | 
            +
             | 
| 366 | 
            +
                    attrs_str = attrs.map { %(#{_1}="#{_2}") }.join(ATTRIBUTE_DELIMITER)
         | 
| 367 | 
            +
             | 
| 368 | 
            +
                    if _wrap
         | 
| 369 | 
            +
                      "#{_wrap[0]}#{attrs_str}#{_wrap[1]}"
         | 
| 370 | 
            +
                    else
         | 
| 371 | 
            +
                      attrs_str
         | 
| 372 | 
            +
                    end
         | 
| 373 | 
            +
                  end
         | 
| 374 | 
            +
                  # rubocop:enable Lint/UnderscorePrefixedVariableName
         | 
| 375 | 
            +
             | 
| 376 | 
            +
                  def increase_indent
         | 
| 377 | 
            +
                    @indent += 1
         | 
| 378 | 
            +
                    yield
         | 
| 379 | 
            +
                  ensure
         | 
| 380 | 
            +
                    @indent -= 1
         | 
| 381 | 
            +
                  end
         | 
| 382 | 
            +
             | 
| 383 | 
            +
                  def swap_io
         | 
| 384 | 
            +
                    old_io = @io
         | 
| 385 | 
            +
                    @io = IndentedStringIo.new
         | 
| 386 | 
            +
                    yield
         | 
| 387 | 
            +
                    @io
         | 
| 388 | 
            +
                  ensure
         | 
| 389 | 
            +
                    @io = old_io
         | 
| 390 | 
            +
                  end
         | 
| 391 | 
            +
             | 
| 392 | 
            +
                  def module_label(*modules)
         | 
| 393 | 
            +
                    return if modules.empty?
         | 
| 394 | 
            +
             | 
| 395 | 
            +
                    "cluster_#{modules.join(MODULE_DELIMITER)}"
         | 
| 396 | 
            +
                  end
         | 
| 397 | 
            +
                end
         | 
| 398 | 
            +
              end
         | 
| 399 | 
            +
            end
         | 
| @@ -0,0 +1,72 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'rack/proxy'
         | 
| 4 | 
            +
            require 'websocket/driver'
         | 
| 5 | 
            +
            require 'eventmachine'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module DiverDown
         | 
| 8 | 
            +
              class Web
         | 
| 9 | 
            +
                # For vite
         | 
| 10 | 
            +
                class DevServerMiddleware
         | 
| 11 | 
            +
                  class HttpProxy < ::Rack::Proxy
         | 
| 12 | 
            +
                    def initialize(_app = nil, host:, port:)
         | 
| 13 | 
            +
                      @host = host
         | 
| 14 | 
            +
                      @port = port
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      super(nil, backend: "http://#{@host}:#{@port}", proxy_host: @host, proxy_port: @port, proxy_scheme: 'http')
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  class WebSocketProxy
         | 
| 21 | 
            +
                    attr_reader :env, :url
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    def initialize(env, host:, port:)
         | 
| 24 | 
            +
                      @env = env
         | 
| 25 | 
            +
                      @url = "ws://#{host}:#{port}#{env['REQUEST_URI']}"
         | 
| 26 | 
            +
                      @driver = WebSocket::Driver.rack(self)
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      env['rack.hijack'].call
         | 
| 29 | 
            +
                      @io = env['rack.hijack_io']
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                      EM.attach(@io, Reader) { |conn| conn.driver = @driver }
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                      @driver.start
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    # @param string [String]
         | 
| 37 | 
            +
                    def write(string)
         | 
| 38 | 
            +
                      @io.write(string)
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    module Reader
         | 
| 42 | 
            +
                      attr_writer :driver
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                      # @param string [String]
         | 
| 45 | 
            +
                      def receive_data(string)
         | 
| 46 | 
            +
                        @driver.parse(string)
         | 
| 47 | 
            +
                      end
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def initialize(app, host:, port:)
         | 
| 52 | 
            +
                    @app = app
         | 
| 53 | 
            +
                    @host = host
         | 
| 54 | 
            +
                    @port = port
         | 
| 55 | 
            +
                    @http_proxy = HttpProxy.new(@app, host: @host, port: @port)
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  # @param env [Hash]
         | 
| 59 | 
            +
                  def call(env)
         | 
| 60 | 
            +
                    request = Rack::Request.new(env)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    if WebSocket::Driver.websocket?(env)
         | 
| 63 | 
            +
                      WebSocketProxy.new(env, host: @host, port: @port)
         | 
| 64 | 
            +
                    elsif request.path.start_with?('/api')
         | 
| 65 | 
            +
                      @app.call(env)
         | 
| 66 | 
            +
                    else
         | 
| 67 | 
            +
                      @http_proxy.call(env)
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
            end
         | 
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'stringio'
         | 
| 4 | 
            +
            require 'forwardable'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module DiverDown
         | 
| 7 | 
            +
              class Web
         | 
| 8 | 
            +
                class IndentedStringIo
         | 
| 9 | 
            +
                  extend ::Forwardable
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def_delegators :@io, :rewind, :string
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  attr_accessor :indent
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  # @param tab [String]
         | 
| 16 | 
            +
                  def initialize(tab: '  ')
         | 
| 17 | 
            +
                    @io = StringIO.new
         | 
| 18 | 
            +
                    @indent = 0
         | 
| 19 | 
            +
                    @tab = tab
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  # @param contents [Array<String>]
         | 
| 23 | 
            +
                  # @param indent [Boolean] Enable or disable indentation
         | 
| 24 | 
            +
                  # @return [void]
         | 
| 25 | 
            +
                  def write(*contents, indent: true)
         | 
| 26 | 
            +
                    indent_string = if indent
         | 
| 27 | 
            +
                                      @tab * @indent
         | 
| 28 | 
            +
                                    else
         | 
| 29 | 
            +
                                      ''
         | 
| 30 | 
            +
                                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    string = contents.join
         | 
| 33 | 
            +
                    lines = string.lines
         | 
| 34 | 
            +
                    lines.each do |line|
         | 
| 35 | 
            +
                      if line == "\n"
         | 
| 36 | 
            +
                        @io.write "\n"
         | 
| 37 | 
            +
                      else
         | 
| 38 | 
            +
                        @io.write "#{indent_string}#{line}"
         | 
| 39 | 
            +
                      end
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  # @param content [String]
         | 
| 44 | 
            +
                  # @return [void]
         | 
| 45 | 
            +
                  def puts(*contents, indent: true)
         | 
| 46 | 
            +
                    write("#{contents.join("\n")}\n", indent:)
         | 
| 47 | 
            +
                    nil
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  # increase the indent level for the block
         | 
| 51 | 
            +
                  def indented
         | 
| 52 | 
            +
                    @indent += 1
         | 
| 53 | 
            +
                    yield
         | 
| 54 | 
            +
                  ensure
         | 
| 55 | 
            +
                    @indent -= 1
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'yaml'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module DiverDown
         | 
| 6 | 
            +
              class Web
         | 
| 7 | 
            +
                class ModuleStore
         | 
| 8 | 
            +
                  BLANK_ARRAY = [].freeze
         | 
| 9 | 
            +
                  BLANK_RE = /\A\s*\z/
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  private_constant(:BLANK_RE)
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def initialize(path)
         | 
| 14 | 
            +
                    @path = path
         | 
| 15 | 
            +
                    @store = load
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  # @param source_name [String]
         | 
| 19 | 
            +
                  # @param module_names [Array<String>]
         | 
| 20 | 
            +
                  def set(source_name, module_names)
         | 
| 21 | 
            +
                    @store[source_name] = module_names.dup.reject do
         | 
| 22 | 
            +
                      BLANK_RE.match?(_1)
         | 
| 23 | 
            +
                    end.freeze
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # @param source_name [String]
         | 
| 27 | 
            +
                  # @return [Array<Module>]
         | 
| 28 | 
            +
                  def get(source_name)
         | 
| 29 | 
            +
                    @store[source_name] || BLANK_ARRAY
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  # @return [Hash]
         | 
| 33 | 
            +
                  def to_h
         | 
| 34 | 
            +
                    @store.dup
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # Write store to file
         | 
| 38 | 
            +
                  # @return [void]
         | 
| 39 | 
            +
                  def flush
         | 
| 40 | 
            +
                    File.write(@path, to_h.to_yaml)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  private
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def load
         | 
| 46 | 
            +
                    store = {}
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    begin
         | 
| 49 | 
            +
                      loaded = YAML.load_file(@path)
         | 
| 50 | 
            +
                      store.merge!(loaded) if loaded
         | 
| 51 | 
            +
                    rescue StandardError
         | 
| 52 | 
            +
                      # Ignore error
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    store
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
| @@ -0,0 +1,101 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'rack'
         | 
| 4 | 
            +
            require 'yaml'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module DiverDown
         | 
| 7 | 
            +
              class Web
         | 
| 8 | 
            +
                WEB_DIR = File.expand_path('../../web', __dir__)
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                require 'diver_down/web/action'
         | 
| 11 | 
            +
                require 'diver_down/web/definition_to_dot'
         | 
| 12 | 
            +
                require 'diver_down/web/definition_enumerator'
         | 
| 13 | 
            +
                require 'diver_down/web/bit_id'
         | 
| 14 | 
            +
                require 'diver_down/web/module_store'
         | 
| 15 | 
            +
                require 'diver_down/web/indented_string_io'
         | 
| 16 | 
            +
                require 'diver_down/web/definition_store'
         | 
| 17 | 
            +
                require 'diver_down/web/definition_loader'
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                # For development
         | 
| 20 | 
            +
                autoload :DevServerMiddleware, 'diver_down/web/dev_server_middleware'
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                # @param definition_dir [String]
         | 
| 23 | 
            +
                # @param module_store [DiverDown::ModuleStore]
         | 
| 24 | 
            +
                # @param store [DiverDown::Web::DefinitionStore]
         | 
| 25 | 
            +
                def initialize(definition_dir:, module_store:, store: DiverDown::Web::DefinitionStore.new)
         | 
| 26 | 
            +
                  @store = store
         | 
| 27 | 
            +
                  @module_store = module_store
         | 
| 28 | 
            +
                  @files_server = Rack::Files.new(File.join(WEB_DIR))
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  definition_files = ::Dir["#{definition_dir}/**/*.{yml,yaml,msgpack,json}"].sort
         | 
| 31 | 
            +
                  @total_definition_files_size = definition_files.size
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  load_definition_files_on_thread(definition_files)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                # @param env [Hash]
         | 
| 37 | 
            +
                # @return [Array[Integer, Hash, Array]]
         | 
| 38 | 
            +
                def call(env)
         | 
| 39 | 
            +
                  request = Rack::Request.new(env)
         | 
| 40 | 
            +
                  action = DiverDown::Web::Action.new(store: @store, module_store: @module_store, request:)
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  case [request.request_method, request.path]
         | 
| 43 | 
            +
                  in ['GET', %r{\A/api/definitions\.json\z}]
         | 
| 44 | 
            +
                    action.definitions(
         | 
| 45 | 
            +
                      page: request.params['page']&.to_i || 1,
         | 
| 46 | 
            +
                      per: request.params['per']&.to_i || 100,
         | 
| 47 | 
            +
                      title: request.params['title'] || '',
         | 
| 48 | 
            +
                      source: request.params['source'] || ''
         | 
| 49 | 
            +
                    )
         | 
| 50 | 
            +
                  in ['GET', %r{\A/api/sources\.json\z}]
         | 
| 51 | 
            +
                    action.sources
         | 
| 52 | 
            +
                  in ['GET', %r{\A/api/modules\.json\z}]
         | 
| 53 | 
            +
                    action.modules
         | 
| 54 | 
            +
                  in ['GET', %r{\A/api/modules/(?<module_names>.+)\.json\z}]
         | 
| 55 | 
            +
                    module_names = Regexp.last_match[:module_names].split('/')
         | 
| 56 | 
            +
                    action.module(module_names)
         | 
| 57 | 
            +
                  in ['GET', %r{\A/api/definitions/(?<bit_id>\d+)\.json\z}]
         | 
| 58 | 
            +
                    bit_id = Regexp.last_match[:bit_id].to_i
         | 
| 59 | 
            +
                    compound = request.params['compound'] == '1'
         | 
| 60 | 
            +
                    concentrate = request.params['concentrate'] == '1'
         | 
| 61 | 
            +
                    only_module = request.params['only_module'] == '1'
         | 
| 62 | 
            +
                    action.combine_definitions(bit_id, compound, concentrate, only_module)
         | 
| 63 | 
            +
                  in ['GET', %r{\A/api/sources/(?<source>[^/]+)\.json\z}]
         | 
| 64 | 
            +
                    source = Regexp.last_match[:source]
         | 
| 65 | 
            +
                    action.source(source)
         | 
| 66 | 
            +
                  in ['POST', %r{\A/api/sources/(?<source>[^/]+)/modules.json\z}]
         | 
| 67 | 
            +
                    source = Regexp.last_match[:source]
         | 
| 68 | 
            +
                    modules = request.params['modules'] || []
         | 
| 69 | 
            +
                    action.set_modules(source, modules)
         | 
| 70 | 
            +
                  in ['GET', %r{\A/api/pid\.json\z}]
         | 
| 71 | 
            +
                    action.pid
         | 
| 72 | 
            +
                  in ['GET', %r{\A/api/initialization_status\.json\z}]
         | 
| 73 | 
            +
                    action.initialization_status(@total_definition_files_size)
         | 
| 74 | 
            +
                  in ['GET', %r{\A/assets/}]
         | 
| 75 | 
            +
                    @files_server.call(env)
         | 
| 76 | 
            +
                  in ['GET', /\.json\z/], ['POST', /\.json\z/]
         | 
| 77 | 
            +
                    action.not_found
         | 
| 78 | 
            +
                  else
         | 
| 79 | 
            +
                    @files_server.call(env.merge('PATH_INFO' => '/index.html'))
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                private
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def load_definition_files_on_thread(definition_files)
         | 
| 86 | 
            +
                  definition_loader = DiverDown::Web::DefinitionLoader.new
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  Thread.new do
         | 
| 89 | 
            +
                    loop do
         | 
| 90 | 
            +
                      break if definition_files.empty?
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                      definition_file = definition_files.shift
         | 
| 93 | 
            +
                      definition = definition_loader.load_file(definition_file)
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                      # No needed to synchronize because this is executed on a single thread.
         | 
| 96 | 
            +
                      @store.set(definition)
         | 
| 97 | 
            +
                    end
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
              end
         | 
| 101 | 
            +
            end
         |