Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Error < StandardError; end
autoload :Model, "ea/model"
autoload :Sources, "ea/sources"
autoload :Spa, "ea/spa"
autoload :Svg, "ea/svg"
autoload :Diagram, "ea/diagram"
autoload :Transformations, "ea/transformations"
autoload :Xmi, "ea/xmi"
Expand Down
14 changes: 14 additions & 0 deletions lib/ea/cli/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class App < Thor
# search, and every output-bearing command honours the same flag.
OUTPUT_OPTION = { type: :string, aliases: :o }.freeze

# Shared kwargs for any command that accepts a YAML config file.
# Matches OUTPUT_OPTION; adding config support to a command is a
# one-line addition.
CONFIG_OPTION = { type: :string, aliases: :c }.freeze

class << self
def exit_on_failure?
true
Expand Down Expand Up @@ -72,12 +77,21 @@ def convert(file)
desc "spa FILE", "Generate single-page app (SPA) from QEA/XMI"
option :output, **OUTPUT_OPTION,
desc: "Output path (default: <basename>.html or <basename>.spa/)"
option :config, **CONFIG_OPTION,
desc: "Path to SPA YAML config (overrides model metadata: title, description, etc.)"
option :mode, type: :string, default: "single_file",
desc: "Output mode: single_file | sharded"
def spa(file)
Command::Spa.new(file: file, **symbolize(options)).call
end

desc "svg NAME FILE", "Render a diagram from QEA/XMI to standalone SVG"
option :output, **OUTPUT_OPTION,
desc: "Output path (default: <basename>.<diagram>.svg)"
def svg(name, file)
Command::Svg.new(name: name, file: file, **symbolize(options)).call
end

private

def symbolize(opts)
Expand Down
1 change: 1 addition & 0 deletions lib/ea/cli/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module Command
autoload :Parse, "ea/cli/command/parse"
autoload :Convert, "ea/cli/command/convert"
autoload :Spa, "ea/cli/command/spa"
autoload :Svg, "ea/cli/command/svg"
autoload :RepositoryBuilder, "ea/cli/command/repository_builder"
end
end
Expand Down
16 changes: 15 additions & 1 deletion lib/ea/cli/command/spa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def call
Ea::Spa::Generator.new(
document,
output: output_path,
mode: mode
mode: mode,
configuration: spa_configuration
).generate

formatter.render([[output_path]], columns: [:written_to])
Expand All @@ -45,6 +46,19 @@ def mode
(options[:mode] || DEFAULT_MODE).to_sym
end

def spa_configuration
path = options[:config]
return nil unless path

expanded = File.expand_path(path)
unless File.exist?(expanded)
raise Ea::Cli::FileNotFound,
"SPA config not found: #{path}"
end

Ea::Spa::Configuration.load(expanded)
end

def resolve_output_path
options[:output] || default_output_path
end
Expand Down
68 changes: 68 additions & 0 deletions lib/ea/cli/command/svg.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

require "fileutils"

module Ea
module Cli
module Command
# `ea svg NAME FILE [--output=PATH]`
#
# Renders a single diagram from a QEA or XMI file into a
# standalone SVG using the umldi content (placed element
# bounds + connector waypoints) captured in Ea::Model::Diagram.
class Svg < Base
def call
diagram = find_diagram
svg = Ea::Svg::Renderer.new(diagram,
model_index: document.index_by_id).render

output_path = resolve_output_path(diagram)
FileUtils.mkdir_p(File.dirname(output_path))
File.write(output_path, svg)

formatter.render([[output_path]], columns: [:written_to])
end

private

def document
@document ||= build_model_document
end

def build_model_document
case File.extname(file_path).downcase
when ".qea"
Ea::Sources::Qea::Adapter.from_path(file_path)
when ".xmi"
Ea::Sources::Xmi::Adapter.from_path(file_path)
else
raise Ea::Cli::UnsupportedFormat,
"Unknown file format: #{File.extname(file_path)}"
end
end

def diagram_name
options[:name] or raise Ea::Cli::Error, "missing required diagram name"
end

def find_diagram
match = document.diagrams.find { |d| d.name == diagram_name }
return match if match

raise Ea::Cli::Error,
"Diagram #{diagram_name.inspect} not found in #{file_path}. " \
"Use `ea diagrams list #{file_path}` to see names."
end

def resolve_output_path(diagram)
options[:output] || begin
base = File.basename(file_path, ".*")
dir = File.dirname(file_path)
safe_name = (diagram.name || "diagram").gsub(/[^\w.\-]+/, "_")
File.join(dir, "#{base}.#{safe_name}.svg")
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/ea/spa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ module Spa
autoload :Projector, "ea/spa/projector"
autoload :Output, "ea/spa/output"
autoload :Generator, "ea/spa/generator"
autoload :Configuration, "ea/spa/configuration"
end
end
96 changes: 96 additions & 0 deletions lib/ea/spa/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# frozen_string_literal: true

require "yaml"

module Ea
module Spa
# SPA-side configuration. A YAML file with the shape:
#
# metadata:
# title: ...
# description: ...
# version: ...
# author: ...
# license: ...
# homepage: ...
# repository: ...
# ui:
# title: ...
# description: ...
#
# Values under `metadata` override fields on the model's
# Ea::Model::Metadata. Values under `ui` (and other sections
# like `appearance`) are surfaced to the SPA view via the
# skeleton's metadata hash so the frontend can use them.
class Configuration
attr_reader :path, :data

def self.load(path)
return nil if path.nil? || path.empty?
unless File.exist?(path)
raise ArgumentError, "SPA config not found: #{path}"
end

new(path, YAML.safe_load(File.read(path)) || {})
end

def initialize(path, data)
@path = path
@data = data.is_a?(Hash) ? data : {}
end

def metadata_override
(@data["metadata"] || {}).transform_keys(&:to_sym)
end

def ui
@data["ui"] || {}
end

def appearance
@data["appearance"] || {}
end

# Apply overrides to a model Metadata instance, returning a new
# one. The original is left untouched.
def apply_to_metadata(model_metadata)
return model_metadata unless model_metadata

merged = model_metadata_to_hash(model_metadata)
.merge(stringify_keys(metadata_override))
hash_to_metadata(merged)
end

# Surface the relevant config (ui, appearance) into a hash the
# SPA skeleton embeds in its metadata payload.
def view_extras
{ "ui" => ui, "appearance" => appearance }.reject { |_, v| v.nil? || v.empty? }
end

private

def model_metadata_to_hash(meta)
JSON.parse(meta.to_json)
end

def hash_to_metadata(hash)
Ea::Model::Metadata.new(
id: hash["id"] || "metadata",
title: hash["title"],
version: hash["version"],
author: hash["author"],
company: hash["company"],
created_date: hash["createdDate"],
modified_date: hash["modifiedDate"],
source_format: hash["sourceFormat"],
source_tool: hash["sourceTool"],
source_path: hash["sourcePath"]
)
end

def stringify_keys(hash)
hash.transform_keys(&:to_s)
end
end
end
end
10 changes: 7 additions & 3 deletions lib/ea/spa/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,25 @@ class Generator

DEFAULT_MODE = :single_file

attr_reader :document, :mode, :output_path, :shard_url_for
attr_reader :document, :mode, :output_path, :shard_url_for, :configuration

def initialize(document, output:, mode: nil, shard_url_for: nil)
def initialize(document, output:, mode: nil, shard_url_for: nil,
configuration: nil)
@document = document
@output_path = output
@mode = (mode || DEFAULT_MODE).to_sym
@shard_url_for = shard_url_for
@configuration = configuration
end

def generate
strategy_class.new(output_path).render(projector)
end

def projector
@projector ||= Projector.new(document, shard_url_for: shard_url_for)
@projector ||= Projector.new(document,
shard_url_for: shard_url_for,
configuration: configuration)
end

def strategy_class
Expand Down
13 changes: 8 additions & 5 deletions lib/ea/spa/output/sharded_multi_file_strategy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ def render(projector)
FileUtils.mkdir_p(output_path)
FileUtils.mkdir_p(File.join(output_path, "data"))

write_json(File.join(output_path, "skeleton.json"),
projector.skeleton)
skeleton = projector.skeleton
write_json(File.join(output_path, "skeleton.json"), skeleton)
write_json(File.join(output_path, "search.json"),
projector.search_index)
write_shards(projector)
write_index_html
write_index_html(skeleton)

output_path
end
Expand All @@ -42,14 +42,17 @@ def pluralize(kind)
end
end

def write_index_html
def write_index_html(skeleton)
title = skeleton.view_extras&.dig("ui", "title") ||
skeleton.metadata&.fetch("title", nil) ||
"Ea::Spa"
File.write(File.join(output_path, "index.html"), <<~HTML)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ea::Spa</title>
<title>#{title}</title>
</head>
<body>
<div id="app"></div>
Expand Down
13 changes: 9 additions & 4 deletions lib/ea/spa/output/single_file_strategy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ class SingleFileStrategy < Strategy
def render(projector)
FileUtils.mkdir_p(File.dirname(output_path))

skeleton = projector.skeleton
payload = {
metadata: projector.skeleton.metadata,
packageTree: projector.skeleton.package_tree,
skeletonEntries: projector.skeleton.entries,
metadata: skeleton.metadata,
packageTree: skeleton.package_tree,
skeletonEntries: skeleton.entries,
searchIndex: projector.search_index,
shards: projector.each_shard.to_a
}
payload[:viewExtras] = skeleton.view_extras unless skeleton.view_extras.nil? || skeleton.view_extras.empty?

File.write(output_path, build_html(payload))
output_path
Expand All @@ -28,13 +30,16 @@ def render(projector)
private

def build_html(payload)
title = payload.dig(:viewExtras, "ui", "title") ||
payload[:metadata]&.fetch("title", nil) ||
"Ea::Spa"
<<~HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>#{payload[:metadata]&.fetch("title", nil) || "Ea::Spa"}</title>
<title>#{title}</title>
</head>
<body>
<div id="app"></div>
Expand Down
16 changes: 12 additions & 4 deletions lib/ea/spa/projector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ module Spa
# proc — by default it produces "data/<kind>s/<id>.json" paths
# matching the sharded output strategy.
class Projector
attr_reader :document, :shard_url_for
attr_reader :document, :shard_url_for, :configuration

def initialize(document, shard_url_for: nil)
def initialize(document, shard_url_for: nil, configuration: nil)
@document = document
@shard_url_for = shard_url_for || default_shard_url
@configuration = configuration
end

def skeleton
Skeleton.new(
metadata: metadata_hash,
package_tree: build_package_tree,
entries: build_entries
entries: build_entries,
view_extras: view_extras
)
end

Expand Down Expand Up @@ -67,7 +69,13 @@ def pluralize(kind)
end

def metadata_hash
JSON.parse(document.metadata.to_json)
base = document.metadata
merged = configuration ? configuration.apply_to_metadata(base) : base
JSON.parse(merged.to_json)
end

def view_extras
configuration&.view_extras || {}
end

def build_package_tree
Expand Down
Loading
Loading