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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -5
  3. data/lib/archsight/analysis/executor.rb +112 -0
  4. data/lib/archsight/analysis/result.rb +174 -0
  5. data/lib/archsight/analysis/sandbox.rb +319 -0
  6. data/lib/archsight/analysis.rb +11 -0
  7. data/lib/archsight/annotations/architecture_annotations.rb +2 -2
  8. data/lib/archsight/cli.rb +163 -0
  9. data/lib/archsight/database.rb +6 -2
  10. data/lib/archsight/helpers/analysis_renderer.rb +83 -0
  11. data/lib/archsight/helpers/formatting.rb +95 -0
  12. data/lib/archsight/helpers.rb +20 -4
  13. data/lib/archsight/import/concurrent_progress.rb +341 -0
  14. data/lib/archsight/import/executor.rb +466 -0
  15. data/lib/archsight/import/git_analytics.rb +626 -0
  16. data/lib/archsight/import/handler.rb +263 -0
  17. data/lib/archsight/import/handlers/github.rb +161 -0
  18. data/lib/archsight/import/handlers/gitlab.rb +202 -0
  19. data/lib/archsight/import/handlers/jira_base.rb +189 -0
  20. data/lib/archsight/import/handlers/jira_discover.rb +161 -0
  21. data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
  22. data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
  23. data/lib/archsight/import/handlers/repository.rb +439 -0
  24. data/lib/archsight/import/handlers/rest_api.rb +293 -0
  25. data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
  26. data/lib/archsight/import/progress.rb +91 -0
  27. data/lib/archsight/import/registry.rb +54 -0
  28. data/lib/archsight/import/shared_file_writer.rb +67 -0
  29. data/lib/archsight/import/team_matcher.rb +195 -0
  30. data/lib/archsight/import.rb +14 -0
  31. data/lib/archsight/resources/analysis.rb +91 -0
  32. data/lib/archsight/resources/application_component.rb +2 -2
  33. data/lib/archsight/resources/application_service.rb +12 -12
  34. data/lib/archsight/resources/business_product.rb +12 -12
  35. data/lib/archsight/resources/data_object.rb +1 -1
  36. data/lib/archsight/resources/import.rb +79 -0
  37. data/lib/archsight/resources/technology_artifact.rb +23 -2
  38. data/lib/archsight/version.rb +1 -1
  39. data/lib/archsight/web/api/docs.rb +17 -0
  40. data/lib/archsight/web/api/json_helpers.rb +164 -0
  41. data/lib/archsight/web/api/openapi/spec.yaml +500 -0
  42. data/lib/archsight/web/api/routes.rb +101 -0
  43. data/lib/archsight/web/application.rb +66 -43
  44. data/lib/archsight/web/doc/import.md +458 -0
  45. data/lib/archsight/web/doc/index.md.erb +1 -0
  46. data/lib/archsight/web/public/css/artifact.css +10 -0
  47. data/lib/archsight/web/public/css/graph.css +14 -0
  48. data/lib/archsight/web/public/css/instance.css +489 -0
  49. data/lib/archsight/web/views/api_docs.erb +19 -0
  50. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
  51. data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
  52. data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
  53. data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
  54. data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
  55. data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
  56. data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
  57. data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
  58. 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