archsight 0.1.2 → 0.1.3
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/README.md +26 -5
- data/lib/archsight/analysis/executor.rb +112 -0
- data/lib/archsight/analysis/result.rb +174 -0
- data/lib/archsight/analysis/sandbox.rb +319 -0
- data/lib/archsight/analysis.rb +11 -0
- data/lib/archsight/annotations/architecture_annotations.rb +2 -2
- data/lib/archsight/cli.rb +163 -0
- data/lib/archsight/database.rb +6 -2
- data/lib/archsight/helpers/analysis_renderer.rb +83 -0
- data/lib/archsight/helpers/formatting.rb +95 -0
- data/lib/archsight/helpers.rb +20 -4
- data/lib/archsight/import/concurrent_progress.rb +341 -0
- data/lib/archsight/import/executor.rb +466 -0
- data/lib/archsight/import/git_analytics.rb +626 -0
- data/lib/archsight/import/handler.rb +263 -0
- data/lib/archsight/import/handlers/github.rb +161 -0
- data/lib/archsight/import/handlers/gitlab.rb +202 -0
- data/lib/archsight/import/handlers/jira_base.rb +189 -0
- data/lib/archsight/import/handlers/jira_discover.rb +161 -0
- data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
- data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
- data/lib/archsight/import/handlers/repository.rb +439 -0
- data/lib/archsight/import/handlers/rest_api.rb +293 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
- data/lib/archsight/import/progress.rb +91 -0
- data/lib/archsight/import/registry.rb +54 -0
- data/lib/archsight/import/shared_file_writer.rb +67 -0
- data/lib/archsight/import/team_matcher.rb +195 -0
- data/lib/archsight/import.rb +14 -0
- data/lib/archsight/resources/analysis.rb +91 -0
- data/lib/archsight/resources/application_component.rb +2 -2
- data/lib/archsight/resources/application_service.rb +12 -12
- data/lib/archsight/resources/business_product.rb +12 -12
- data/lib/archsight/resources/data_object.rb +1 -1
- data/lib/archsight/resources/import.rb +79 -0
- data/lib/archsight/resources/technology_artifact.rb +23 -2
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/docs.rb +17 -0
- data/lib/archsight/web/api/json_helpers.rb +164 -0
- data/lib/archsight/web/api/openapi/spec.yaml +500 -0
- data/lib/archsight/web/api/routes.rb +101 -0
- data/lib/archsight/web/application.rb +66 -43
- data/lib/archsight/web/doc/import.md +458 -0
- data/lib/archsight/web/doc/index.md.erb +1 -0
- data/lib/archsight/web/public/css/artifact.css +10 -0
- data/lib/archsight/web/public/css/graph.css +14 -0
- data/lib/archsight/web/public/css/instance.css +489 -0
- data/lib/archsight/web/views/api_docs.erb +19 -0
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
- data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
- data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
- data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
- data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
- data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
- data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
- metadata +78 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "archsight/import"
|
|
4
|
+
require "dry/inflector"
|
|
5
|
+
|
|
6
|
+
# Parses OpenAPI schemas and extracts DataObject information
|
|
7
|
+
#
|
|
8
|
+
# Features:
|
|
9
|
+
# - Schema name normalization (strips CRUD prefixes/suffixes, singularizes)
|
|
10
|
+
# - Skip utility schemas (Error, Links, Metadata, etc.)
|
|
11
|
+
# - $ref resolution with cycle detection
|
|
12
|
+
# - allOf composition handling
|
|
13
|
+
# - Nested property extraction (up to 3 levels deep)
|
|
14
|
+
# - Field documentation generation (markdown table format)
|
|
15
|
+
class Archsight::Import::Handlers::OpenAPISchemaParser
|
|
16
|
+
# Schemas that should be skipped (utility schemas)
|
|
17
|
+
SKIP_SCHEMAS = %w[
|
|
18
|
+
Error Links Offset Limit Metadata PaginationLinks ErrorMessage Pagination
|
|
19
|
+
ErrorMessages Type State ErrorResponse ValidationError InternalError
|
|
20
|
+
NotFoundError ForbiddenError UnauthorizedError BadRequestError
|
|
21
|
+
ConflictError RateLimitError ServiceUnavailableError
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Suffixes to strip from schema names for normalization
|
|
25
|
+
STRIP_SUFFIXES = %w[
|
|
26
|
+
Create Read Ensure ReadList List Patch Update Put Post Response Request
|
|
27
|
+
Properties Resource Item Items Collection Result Results Output Input
|
|
28
|
+
Dto DTO Entity Model Data Info Details Summary Overview
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# Prefixes to strip from schema names for normalization
|
|
32
|
+
STRIP_PREFIXES = %w[
|
|
33
|
+
Create Get Update Delete Set Add Remove List Fetch Patch
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
# Words that should remain plural (don't singularize)
|
|
37
|
+
KEEP_PLURAL = %w[
|
|
38
|
+
Status Address Class Process Access Alias Analysis Basis
|
|
39
|
+
Canvas Census Chorus Circus Corpus Crisis Diagnosis Ellipsis
|
|
40
|
+
Emphasis Genesis Hypothesis Oasis Paralysis Parenthesis Synopsis
|
|
41
|
+
Thesis Axis Redis Kubernetes
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
# Maximum depth for nested property extraction
|
|
45
|
+
MAX_PROPERTY_DEPTH = 3
|
|
46
|
+
|
|
47
|
+
attr_reader :schemas, :parsed_objects
|
|
48
|
+
|
|
49
|
+
# @param openapi_doc [Hash] Parsed OpenAPI document
|
|
50
|
+
def initialize(openapi_doc)
|
|
51
|
+
@openapi_doc = openapi_doc
|
|
52
|
+
@schemas = openapi_doc.dig("components", "schemas") || {}
|
|
53
|
+
@parsed_objects = {}
|
|
54
|
+
@visited_refs = Set.new
|
|
55
|
+
@inflector = Dry::Inflector.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Parse all schemas and return normalized DataObjects
|
|
59
|
+
# @return [Hash<String, Hash>] Map of normalized name to data object info
|
|
60
|
+
def parse
|
|
61
|
+
# First pass: create entries for each unique normalized name
|
|
62
|
+
@schemas.each_key do |schema_name|
|
|
63
|
+
next if skip_schema?(schema_name)
|
|
64
|
+
|
|
65
|
+
normalized_name = normalize_name(schema_name)
|
|
66
|
+
next if normalized_name.empty?
|
|
67
|
+
next if @parsed_objects.key?(normalized_name)
|
|
68
|
+
|
|
69
|
+
schema = @schemas[schema_name]
|
|
70
|
+
@visited_refs.clear
|
|
71
|
+
|
|
72
|
+
properties = extract_properties(schema, depth: 0)
|
|
73
|
+
next if properties.empty? && !has_schema_content?(schema)
|
|
74
|
+
|
|
75
|
+
@parsed_objects[normalized_name] = {
|
|
76
|
+
"original_names" => [schema_name],
|
|
77
|
+
"properties" => properties,
|
|
78
|
+
"description" => schema["description"]
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Second pass: track all original names that map to each normalized name
|
|
83
|
+
# rubocop:disable Style/CombinableLoops -- loops must be separate: first creates entries, second merges
|
|
84
|
+
@schemas.each_key do |schema_name|
|
|
85
|
+
next if skip_schema?(schema_name)
|
|
86
|
+
|
|
87
|
+
normalized_name = normalize_name(schema_name)
|
|
88
|
+
next if normalized_name.empty?
|
|
89
|
+
next unless @parsed_objects.key?(normalized_name)
|
|
90
|
+
|
|
91
|
+
original_names = @parsed_objects[normalized_name]["original_names"]
|
|
92
|
+
original_names << schema_name unless original_names.include?(schema_name)
|
|
93
|
+
end
|
|
94
|
+
# rubocop:enable Style/CombinableLoops
|
|
95
|
+
|
|
96
|
+
@parsed_objects
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Generate markdown documentation for properties
|
|
100
|
+
# @param properties [Array<Hash>] Property list
|
|
101
|
+
# @return [String] Markdown table
|
|
102
|
+
def self.generate_field_docs(properties)
|
|
103
|
+
return "" if properties.empty?
|
|
104
|
+
|
|
105
|
+
lines = ["## Fields", "", "| Field | Type | Required | Description |", "|-------|------|----------|-------------|"]
|
|
106
|
+
|
|
107
|
+
properties.each do |prop|
|
|
108
|
+
name = "`#{prop["name"]}`"
|
|
109
|
+
type = prop["type"] || "object"
|
|
110
|
+
type = "#{type} (#{prop["format"]})" if prop["format"]
|
|
111
|
+
required = prop["required"] ? "Yes" : "No"
|
|
112
|
+
description = (prop["description"] || "").gsub(/\s+/, " ").strip
|
|
113
|
+
|
|
114
|
+
# Truncate long descriptions
|
|
115
|
+
description = "#{description[0, 80]}..." if description.length > 80
|
|
116
|
+
|
|
117
|
+
lines << "| #{name} | #{type} | #{required} | #{description} |"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
lines.join("\n")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Check if schema should be skipped
|
|
126
|
+
# @param name [String] Schema name
|
|
127
|
+
# @return [Boolean]
|
|
128
|
+
def skip_schema?(name)
|
|
129
|
+
SKIP_SCHEMAS.any? { |skip| name == skip || name.end_with?(skip) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check if schema has meaningful content (properties, refs, composition)
|
|
133
|
+
# @param schema [Hash] Schema definition
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
def has_schema_content?(schema)
|
|
136
|
+
return false if schema.nil?
|
|
137
|
+
|
|
138
|
+
schema["properties"] || schema["$ref"] || schema["allOf"] ||
|
|
139
|
+
schema["oneOf"] || schema["anyOf"] || schema["type"] == "object"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Normalize schema name by removing CRUD prefixes/suffixes and singularizing
|
|
143
|
+
# @param name [String] Original schema name
|
|
144
|
+
# @return [String] Normalized name
|
|
145
|
+
def normalize_name(name)
|
|
146
|
+
result = name.dup
|
|
147
|
+
|
|
148
|
+
# Strip prefixes only if followed by an uppercase letter (word boundary)
|
|
149
|
+
STRIP_PREFIXES.each do |prefix|
|
|
150
|
+
if result.start_with?(prefix) && result.length > prefix.length
|
|
151
|
+
next_char = result[prefix.length]
|
|
152
|
+
result = result.sub(/^#{prefix}/, "") if next_char == next_char.upcase && next_char =~ /[A-Z]/
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Strip suffixes (can apply multiple times for compound suffixes like CreateResponse)
|
|
157
|
+
2.times do
|
|
158
|
+
STRIP_SUFFIXES.each do |suffix|
|
|
159
|
+
result = result[0..-(suffix.length + 1)] if result.end_with?(suffix) && result.length > suffix.length
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Singularize unless in keep_plural list
|
|
164
|
+
result = @inflector.singularize(result) unless KEEP_PLURAL.any? { |word| result.downcase == word.downcase }
|
|
165
|
+
|
|
166
|
+
result
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Extract properties from a schema
|
|
170
|
+
# @param schema [Hash] Schema definition
|
|
171
|
+
# @param depth [Integer] Current depth for nested extraction
|
|
172
|
+
# @param prefix [String] Property name prefix for nested properties
|
|
173
|
+
# @return [Array<Hash>] List of property definitions
|
|
174
|
+
def extract_properties(schema, depth:, prefix: "")
|
|
175
|
+
return [] if schema.nil? || depth > MAX_PROPERTY_DEPTH
|
|
176
|
+
|
|
177
|
+
# Handle special schema types first
|
|
178
|
+
result = extract_special_schema(schema, depth: depth, prefix: prefix)
|
|
179
|
+
return result if result
|
|
180
|
+
|
|
181
|
+
# Handle object properties
|
|
182
|
+
extract_object_properties(schema, depth: depth, prefix: prefix)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Extract from $ref, allOf, oneOf, or anyOf schemas
|
|
186
|
+
def extract_special_schema(schema, depth:, prefix:)
|
|
187
|
+
return resolve_ref(schema["$ref"], depth: depth, prefix: prefix) if schema["$ref"]
|
|
188
|
+
return schema["allOf"].flat_map { |sub| extract_properties(sub, depth: depth, prefix: prefix) } if schema["allOf"]
|
|
189
|
+
|
|
190
|
+
return unless schema["oneOf"] || schema["anyOf"]
|
|
191
|
+
|
|
192
|
+
options = schema["oneOf"] || schema["anyOf"]
|
|
193
|
+
extract_properties(options.first, depth: depth, prefix: prefix) if options.any?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Extract properties from an object schema
|
|
197
|
+
def extract_object_properties(schema, depth:, prefix:)
|
|
198
|
+
properties = schema["properties"] || {}
|
|
199
|
+
required_props = schema["required"] || []
|
|
200
|
+
|
|
201
|
+
properties.flat_map do |prop_name, prop_schema|
|
|
202
|
+
full_name = prefix.empty? ? prop_name : "#{prefix}.#{prop_name}"
|
|
203
|
+
extract_single_property(prop_schema, full_name, required_props.include?(prop_name), depth)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Extract a single property, handling nested objects and arrays
|
|
208
|
+
def extract_single_property(prop_schema, full_name, required, depth)
|
|
209
|
+
if nested_object?(prop_schema)
|
|
210
|
+
extract_nested_property(prop_schema, full_name, required, depth)
|
|
211
|
+
elsif array_of_refs?(prop_schema)
|
|
212
|
+
[build_array_property(prop_schema, full_name, required)]
|
|
213
|
+
else
|
|
214
|
+
[build_simple_property(prop_schema, full_name, required)]
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def nested_object?(prop_schema)
|
|
219
|
+
prop_schema["$ref"] || prop_schema["type"] == "object" || prop_schema["allOf"]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def array_of_refs?(prop_schema)
|
|
223
|
+
prop_schema["type"] == "array" && prop_schema.dig("items", "$ref")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def extract_nested_property(prop_schema, full_name, required, depth)
|
|
227
|
+
nested = extract_properties(prop_schema, depth: depth + 1, prefix: full_name)
|
|
228
|
+
if nested.empty? && prop_schema["type"] == "object"
|
|
229
|
+
[{ "name" => full_name, "type" => "object", "required" => required, "description" => prop_schema["description"] }]
|
|
230
|
+
else
|
|
231
|
+
nested
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def build_array_property(prop_schema, full_name, required)
|
|
236
|
+
{
|
|
237
|
+
"name" => full_name, "type" => "array",
|
|
238
|
+
"format" => extract_ref_name(prop_schema.dig("items", "$ref")),
|
|
239
|
+
"required" => required, "description" => prop_schema["description"]
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def build_simple_property(prop_schema, full_name, required)
|
|
244
|
+
{
|
|
245
|
+
"name" => full_name, "type" => prop_schema["type"] || "string",
|
|
246
|
+
"format" => prop_schema["format"], "required" => required, "description" => prop_schema["description"]
|
|
247
|
+
}
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Resolve a $ref and extract its properties
|
|
251
|
+
# @param ref [String] Reference string (e.g., "#/components/schemas/Server")
|
|
252
|
+
# @param depth [Integer] Current depth
|
|
253
|
+
# @param prefix [String] Property name prefix
|
|
254
|
+
# @return [Array<Hash>] List of property definitions
|
|
255
|
+
def resolve_ref(ref, depth:, prefix: "")
|
|
256
|
+
# Cycle detection
|
|
257
|
+
return [] if @visited_refs.include?(ref)
|
|
258
|
+
|
|
259
|
+
@visited_refs.add(ref)
|
|
260
|
+
|
|
261
|
+
# Parse local reference
|
|
262
|
+
if ref.start_with?("#/components/schemas/")
|
|
263
|
+
schema_name = ref.sub("#/components/schemas/", "")
|
|
264
|
+
schema = @schemas[schema_name]
|
|
265
|
+
return extract_properties(schema, depth: depth, prefix: prefix) if schema
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
[]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Extract schema name from a $ref
|
|
272
|
+
# @param ref [String] Reference string
|
|
273
|
+
# @return [String, nil] Schema name or nil
|
|
274
|
+
def extract_ref_name(ref)
|
|
275
|
+
return nil unless ref
|
|
276
|
+
|
|
277
|
+
ref.sub("#/components/schemas/", "")
|
|
278
|
+
end
|
|
279
|
+
end
|