archunit 0.0.1
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 +13 -0
- data/LICENSE +7 -0
- data/README.md +375 -0
- data/lib/archunit/common/assertion/empty_test_violation.rb +50 -0
- data/lib/archunit/common/assertion/violation.rb +14 -0
- data/lib/archunit/common/extraction/edge.rb +46 -0
- data/lib/archunit/common/extraction/graph.rb +73 -0
- data/lib/archunit/common/extraction/import_kind.rb +21 -0
- data/lib/archunit/common/filter.rb +46 -0
- data/lib/archunit/common/fluentapi/check_options.rb +48 -0
- data/lib/archunit/common/fluentapi/checkable.rb +70 -0
- data/lib/archunit/common/logging/check_logger.rb +124 -0
- data/lib/archunit/common/logging/logging_options.rb +57 -0
- data/lib/archunit/common/pattern.rb +99 -0
- data/lib/archunit/common/pattern_matching.rb +76 -0
- data/lib/archunit/common/projection/cycles/johnson_cycles.rb +103 -0
- data/lib/archunit/common/projection/cycles/tarjan_scc.rb +82 -0
- data/lib/archunit/common/projection/edge_projections.rb +50 -0
- data/lib/archunit/common/projection/mapped_edge.rb +26 -0
- data/lib/archunit/common/projection/project_cycles.rb +92 -0
- data/lib/archunit/common/projection/project_edges.rb +69 -0
- data/lib/archunit/common/projection/project_to_nodes.rb +59 -0
- data/lib/archunit/common/projection/projected_edge.rb +35 -0
- data/lib/archunit/common/projection/projected_node.rb +36 -0
- data/lib/archunit/common/regex_factory.rb +96 -0
- data/lib/archunit/error/technical_error.rb +6 -0
- data/lib/archunit/error/user_error.rb +6 -0
- data/lib/archunit/extraction/enumerate_source_files.rb +131 -0
- data/lib/archunit/extraction/extract_dependencies.rb +61 -0
- data/lib/archunit/extraction/extract_graph.rb +89 -0
- data/lib/archunit/extraction/extract_imports.rb +197 -0
- data/lib/archunit/extraction/locate_project.rb +80 -0
- data/lib/archunit/extraction/resolve_import.rb +76 -0
- data/lib/archunit/extraction/resolved_import.rb +46 -0
- data/lib/archunit/files/assertion/custom_file_condition.rb +89 -0
- data/lib/archunit/files/assertion/cycle_free.rb +58 -0
- data/lib/archunit/files/assertion/depend_on_external_modules.rb +116 -0
- data/lib/archunit/files/assertion/depend_on_files.rb +110 -0
- data/lib/archunit/files/assertion/matching_files.rb +87 -0
- data/lib/archunit/files/extraction/extract_file_info.rb +84 -0
- data/lib/archunit/files/extraction/file_info.rb +46 -0
- data/lib/archunit/files/fluentapi/custom_file_condition.rb +73 -0
- data/lib/archunit/files/fluentapi/cycle_free_file_condition.rb +55 -0
- data/lib/archunit/files/fluentapi/depend_on_external_module_condition.rb +69 -0
- data/lib/archunit/files/fluentapi/depend_on_external_module_condition_builder.rb +36 -0
- data/lib/archunit/files/fluentapi/depend_on_file_condition.rb +80 -0
- data/lib/archunit/files/fluentapi/depend_on_file_condition_builder.rb +49 -0
- data/lib/archunit/files/fluentapi/file_condition_builder.rb +71 -0
- data/lib/archunit/files/fluentapi/file_rule_support.rb +24 -0
- data/lib/archunit/files/fluentapi/files.rb +31 -0
- data/lib/archunit/files/fluentapi/match_pattern_file_condition.rb +59 -0
- data/lib/archunit/files/fluentapi/match_pattern_file_condition_builder.rb +66 -0
- data/lib/archunit/files/fluentapi/negated_match_pattern_file_condition_builder.rb +16 -0
- data/lib/archunit/files/fluentapi/positive_match_pattern_file_condition_builder.rb +21 -0
- data/lib/archunit/graph/fluentapi/graph.rb +29 -0
- data/lib/archunit/graph/fluentapi/project_graph_builder.rb +106 -0
- data/lib/archunit/graph/projection/aggregate_edges.rb +48 -0
- data/lib/archunit/graph/projection/collapse_node.rb +33 -0
- data/lib/archunit/graph/projection/create_snapshot.rb +84 -0
- data/lib/archunit/graph/projection/folder_depth_collapse.rb +18 -0
- data/lib/archunit/graph/projection/graph_query_options.rb +81 -0
- data/lib/archunit/graph/projection/graph_report_edge.rb +50 -0
- data/lib/archunit/graph/projection/graph_report_node.rb +22 -0
- data/lib/archunit/graph/projection/graph_report_snapshot.rb +43 -0
- data/lib/archunit/graph/projection/graph_report_summary.rb +20 -0
- data/lib/archunit/graph/projection/node_selection.rb +111 -0
- data/lib/archunit/graph/projection/pattern_collapse.rb +24 -0
- data/lib/archunit/graph/rendering/csv_renderer.rb +30 -0
- data/lib/archunit/graph/rendering/d2_renderer.rb +30 -0
- data/lib/archunit/graph/rendering/dot_renderer.rb +51 -0
- data/lib/archunit/graph/rendering/escaping.rb +31 -0
- data/lib/archunit/graph/rendering/export_report.rb +29 -0
- data/lib/archunit/graph/rendering/graph_renderer.rb +54 -0
- data/lib/archunit/graph/rendering/html_document.rb +46 -0
- data/lib/archunit/graph/rendering/html_renderer.rb +102 -0
- data/lib/archunit/graph/rendering/json_renderer.rb +38 -0
- data/lib/archunit/graph/rendering/mermaid_renderer.rb +33 -0
- data/lib/archunit/graph/rendering/rendering_support.rb +24 -0
- data/lib/archunit/layers/assertion/layer_definition.rb +56 -0
- data/lib/archunit/layers/assertion/layer_dependencies.rb +91 -0
- data/lib/archunit/layers/assertion/layer_dependency_violation.rb +59 -0
- data/lib/archunit/layers/fluentapi/layer_definition_builder.rb +46 -0
- data/lib/archunit/layers/fluentapi/layer_dependency_rule_builder.rb +44 -0
- data/lib/archunit/layers/fluentapi/layered_architecture.rb +133 -0
- data/lib/archunit/layers/fluentapi/layered_architecture_values.rb +51 -0
- data/lib/archunit/layers/fluentapi/layers.rb +29 -0
- data/lib/archunit/metrics/assertion/custom_metric.rb +73 -0
- data/lib/archunit/metrics/assertion/metric_predicate.rb +66 -0
- data/lib/archunit/metrics/assertion/metric_threshold.rb +99 -0
- data/lib/archunit/metrics/assertion/metric_zone.rb +57 -0
- data/lib/archunit/metrics/calculation/count.rb +45 -0
- data/lib/archunit/metrics/calculation/distance.rb +84 -0
- data/lib/archunit/metrics/calculation/lcom.rb +103 -0
- data/lib/archunit/metrics/calculation/metric.rb +52 -0
- data/lib/archunit/metrics/calculation/numeric_value.rb +25 -0
- data/lib/archunit/metrics/extraction/distance_info.rb +60 -0
- data/lib/archunit/metrics/extraction/extract_distance_info.rb +50 -0
- data/lib/archunit/metrics/extraction/extract_project_info.rb +90 -0
- data/lib/archunit/metrics/extraction/metric_info.rb +159 -0
- data/lib/archunit/metrics/extraction/source_metrics_visitor.rb +278 -0
- data/lib/archunit/metrics/fluentapi/count_metrics_builder.rb +49 -0
- data/lib/archunit/metrics/fluentapi/custom_metric_builder.rb +30 -0
- data/lib/archunit/metrics/fluentapi/custom_metric_condition.rb +40 -0
- data/lib/archunit/metrics/fluentapi/distance_metrics_builder.rb +48 -0
- data/lib/archunit/metrics/fluentapi/lcom_metrics_builder.rb +39 -0
- data/lib/archunit/metrics/fluentapi/metric_measurement.rb +36 -0
- data/lib/archunit/metrics/fluentapi/metric_predicate_condition.rb +42 -0
- data/lib/archunit/metrics/fluentapi/metric_report_builder.rb +34 -0
- data/lib/archunit/metrics/fluentapi/metric_selection.rb +54 -0
- data/lib/archunit/metrics/fluentapi/metric_threshold_condition.rb +47 -0
- data/lib/archunit/metrics/fluentapi/metrics.rb +21 -0
- data/lib/archunit/metrics/fluentapi/metrics_builder.rb +136 -0
- data/lib/archunit/metrics/fluentapi/zone_condition.rb +40 -0
- data/lib/archunit/metrics/reporting/metrics_export_options.rb +57 -0
- data/lib/archunit/metrics/reporting/metrics_exporter.rb +130 -0
- data/lib/archunit/slices/assertion/adhere_to_diagram.rb +57 -0
- data/lib/archunit/slices/assertion/contain_dependency.rb +42 -0
- data/lib/archunit/slices/assertion/diagram_adherence_options.rb +37 -0
- data/lib/archunit/slices/assertion/slice_dependency_violation.rb +70 -0
- data/lib/archunit/slices/fluentapi/diagram_slice_condition.rb +65 -0
- data/lib/archunit/slices/fluentapi/diagram_source.rb +36 -0
- data/lib/archunit/slices/fluentapi/forbidden_slice_dependency_condition.rb +57 -0
- data/lib/archunit/slices/fluentapi/negative_slice_condition_builder.rb +27 -0
- data/lib/archunit/slices/fluentapi/positive_slice_condition_builder.rb +51 -0
- data/lib/archunit/slices/fluentapi/slice_scope_builder.rb +83 -0
- data/lib/archunit/slices/fluentapi/slices.rb +29 -0
- data/lib/archunit/slices/projection/slice_projection.rb +76 -0
- data/lib/archunit/slices/projection/slicing_projections.rb +98 -0
- data/lib/archunit/slices/uml/export_diagram.rb +70 -0
- data/lib/archunit/slices/uml/parse_diagram.rb +53 -0
- data/lib/archunit/slices/uml/plant_uml_dependency.rb +25 -0
- data/lib/archunit/slices/uml/plant_uml_diagram.rb +44 -0
- data/lib/archunit/testing/assert_passes.rb +24 -0
- data/lib/archunit/testing/assertion_failure.rb +22 -0
- data/lib/archunit/testing/color_utils.rb +66 -0
- data/lib/archunit/testing/layer_violation_formatter.rb +26 -0
- data/lib/archunit/testing/metric_violation_formatter.rb +58 -0
- data/lib/archunit/testing/minitest_adapter.rb +33 -0
- data/lib/archunit/testing/result_factory.rb +93 -0
- data/lib/archunit/testing/rspec_adapter.rb +74 -0
- data/lib/archunit/testing/slice_violation_formatter.rb +25 -0
- data/lib/archunit/testing/test_result.rb +26 -0
- data/lib/archunit/testing/test_violation.rb +23 -0
- data/lib/archunit/testing/violation_factory.rb +138 -0
- data/lib/archunit/testing.rb +39 -0
- data/lib/archunit/version.rb +5 -0
- data/lib/archunit.rb +110 -0
- metadata +251 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi/escape'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
require_relative 'metrics_export_options'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Metrics
|
|
10
|
+
# Self-contained metrics report rendering and export.
|
|
11
|
+
module Reporting
|
|
12
|
+
# Renders caller-supplied metric data as a complete offline HTML document.
|
|
13
|
+
module MetricsExporter
|
|
14
|
+
DEFAULT_CSS = <<~CSS
|
|
15
|
+
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
|
|
16
|
+
body { max-width: 960px; margin: 0 auto; padding: 2rem; }
|
|
17
|
+
h1 { margin-bottom: .25rem; }
|
|
18
|
+
.timestamp { color: #666; margin-top: 0; }
|
|
19
|
+
table { border-collapse: collapse; margin-top: 1.5rem; width: 100%; }
|
|
20
|
+
th, td { border: 1px solid #bbb; padding: .6rem .8rem; text-align: left; }
|
|
21
|
+
th { background: rgba(127, 127, 127, .14); }
|
|
22
|
+
tr:nth-child(even) { background: rgba(127, 127, 127, .07); }
|
|
23
|
+
footer { color: #666; font-size: .85rem; margin-top: 2rem; }
|
|
24
|
+
CSS
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def export_as_html(data, options = nil)
|
|
29
|
+
raise ArgumentError, 'data must be a Hash' unless data.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
options = MetricsExportOptions.resolve(options)
|
|
32
|
+
html = render(data, options).freeze
|
|
33
|
+
write(options.output_path, html) if options.output_path
|
|
34
|
+
html
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def render(data, options)
|
|
38
|
+
title = escape(options.title)
|
|
39
|
+
<<~HTML
|
|
40
|
+
<!DOCTYPE html>
|
|
41
|
+
<html lang="en">
|
|
42
|
+
#{document_head(title, options)}
|
|
43
|
+
#{document_body(title, data, options)}
|
|
44
|
+
</html>
|
|
45
|
+
HTML
|
|
46
|
+
end
|
|
47
|
+
private_class_method :render
|
|
48
|
+
|
|
49
|
+
def document_head(title, options)
|
|
50
|
+
<<~HTML.chomp
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="utf-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
54
|
+
<title>#{title}</title>
|
|
55
|
+
<style>#{safe_css(options.custom_css || DEFAULT_CSS)}</style>
|
|
56
|
+
</head>
|
|
57
|
+
HTML
|
|
58
|
+
end
|
|
59
|
+
private_class_method :document_head
|
|
60
|
+
|
|
61
|
+
def document_body(title, data, options)
|
|
62
|
+
<<~HTML.chomp
|
|
63
|
+
<body>
|
|
64
|
+
<main>
|
|
65
|
+
<h1>#{title}</h1>
|
|
66
|
+
#{timestamp(options)}
|
|
67
|
+
#{metric_table(data)}
|
|
68
|
+
</main>
|
|
69
|
+
<footer>Generated by ArchUnitRuby</footer>
|
|
70
|
+
</body>
|
|
71
|
+
HTML
|
|
72
|
+
end
|
|
73
|
+
private_class_method :document_body
|
|
74
|
+
|
|
75
|
+
def metric_table(data)
|
|
76
|
+
<<~HTML.chomp
|
|
77
|
+
<table>
|
|
78
|
+
<thead><tr><th>Metric</th><th>Value</th></tr></thead>
|
|
79
|
+
<tbody>#{rows(data)}</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
HTML
|
|
82
|
+
end
|
|
83
|
+
private_class_method :metric_table
|
|
84
|
+
|
|
85
|
+
def rows(data)
|
|
86
|
+
return '<tr><td colspan="2">No metric data.</td></tr>' if data.empty?
|
|
87
|
+
|
|
88
|
+
data.map do |name, value|
|
|
89
|
+
"<tr><td>#{escape(name)}</td><td>#{escape(value)}</td></tr>"
|
|
90
|
+
end.join
|
|
91
|
+
end
|
|
92
|
+
private_class_method :rows
|
|
93
|
+
|
|
94
|
+
def timestamp(options)
|
|
95
|
+
return '' unless options.include_timestamp
|
|
96
|
+
|
|
97
|
+
value = Time.now.utc.iso8601
|
|
98
|
+
%(<p class="timestamp">Generated: #{escape(value)}</p>)
|
|
99
|
+
end
|
|
100
|
+
private_class_method :timestamp
|
|
101
|
+
|
|
102
|
+
def write(output_path, html)
|
|
103
|
+
path = html_path(output_path)
|
|
104
|
+
directory = File.dirname(path)
|
|
105
|
+
FileUtils.mkdir_p(directory) unless directory == '.'
|
|
106
|
+
File.binwrite(path, html.encode(Encoding::UTF_8))
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
private_class_method :write
|
|
110
|
+
|
|
111
|
+
def html_path(path)
|
|
112
|
+
return path if File.extname(path).casecmp?('.html')
|
|
113
|
+
|
|
114
|
+
"#{path}.html"
|
|
115
|
+
end
|
|
116
|
+
private_class_method :html_path
|
|
117
|
+
|
|
118
|
+
def escape(value)
|
|
119
|
+
CGI.escapeHTML(value.to_s)
|
|
120
|
+
end
|
|
121
|
+
private_class_method :escape
|
|
122
|
+
|
|
123
|
+
def safe_css(value)
|
|
124
|
+
value.gsub(%r{</style}i, '<\\/style')
|
|
125
|
+
end
|
|
126
|
+
private_class_method :safe_css
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/projection/projected_edge'
|
|
4
|
+
require_relative '../uml/plant_uml_diagram'
|
|
5
|
+
require_relative 'diagram_adherence_options'
|
|
6
|
+
require_relative 'slice_dependency_violation'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Slices
|
|
10
|
+
# Pure assertions over projected slice dependencies.
|
|
11
|
+
module Assertion
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def gather_diagram_adherence_violations(edges, diagram, options = nil)
|
|
15
|
+
validate_diagram_arguments(edges, diagram, options)
|
|
16
|
+
options ||= DiagramAdherenceOptions.new
|
|
17
|
+
|
|
18
|
+
edges.filter_map do |edge|
|
|
19
|
+
next if ignored_diagram_edge?(edge, diagram, options)
|
|
20
|
+
next if diagram.allows?(edge.source_label, edge.target_label)
|
|
21
|
+
|
|
22
|
+
SliceDependencyViolation.new(
|
|
23
|
+
dependency: edge, source_slice: edge.source_label,
|
|
24
|
+
target_slice: edge.target_label, rule: :adhere_to_diagram, is_negated: false
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ignored_diagram_edge?(edge, diagram, options)
|
|
30
|
+
return true if options.ignore_external_slices? && external_dependency?(edge)
|
|
31
|
+
return false unless options.ignore_orphan_slices?
|
|
32
|
+
|
|
33
|
+
!diagram.components.include?(edge.source_label) ||
|
|
34
|
+
!diagram.components.include?(edge.target_label)
|
|
35
|
+
end
|
|
36
|
+
private_class_method :ignored_diagram_edge?
|
|
37
|
+
|
|
38
|
+
def external_dependency?(edge)
|
|
39
|
+
edge.cumulated_edges.any?(&:external)
|
|
40
|
+
end
|
|
41
|
+
private_class_method :external_dependency?
|
|
42
|
+
|
|
43
|
+
def validate_diagram_arguments(edges, diagram, options)
|
|
44
|
+
unless edges.is_a?(Array) && edges.all?(Common::Projection::ProjectedEdge)
|
|
45
|
+
raise ArgumentError, 'edges must be an Array of ProjectedEdge values'
|
|
46
|
+
end
|
|
47
|
+
unless diagram.is_a?(Uml::PlantUmlDiagram)
|
|
48
|
+
raise ArgumentError, 'diagram must be a PlantUmlDiagram'
|
|
49
|
+
end
|
|
50
|
+
return if options.nil? || options.is_a?(DiagramAdherenceOptions)
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, 'options must be DiagramAdherenceOptions or nil'
|
|
53
|
+
end
|
|
54
|
+
private_class_method :validate_diagram_arguments
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/projection/projected_edge'
|
|
4
|
+
require_relative 'slice_dependency_violation'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Slices
|
|
8
|
+
# Pure assertions over projected slice dependencies.
|
|
9
|
+
module Assertion
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def gather_forbidden_slice_dependency_violations(edges, source_slice, target_slice)
|
|
13
|
+
validate_edges(edges)
|
|
14
|
+
source_slice = validated_slice_name(source_slice, :source_slice)
|
|
15
|
+
target_slice = validated_slice_name(target_slice, :target_slice)
|
|
16
|
+
|
|
17
|
+
edges.filter_map do |edge|
|
|
18
|
+
next unless edge.source_label == source_slice && edge.target_label == target_slice
|
|
19
|
+
|
|
20
|
+
SliceDependencyViolation.new(
|
|
21
|
+
dependency: edge, source_slice:, target_slice:,
|
|
22
|
+
rule: :contain_dependency, is_negated: true
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_edges(value)
|
|
28
|
+
return if value.is_a?(Array) && value.all?(Common::Projection::ProjectedEdge)
|
|
29
|
+
|
|
30
|
+
raise ArgumentError, 'edges must be an Array of ProjectedEdge values'
|
|
31
|
+
end
|
|
32
|
+
private_class_method :validate_edges
|
|
33
|
+
|
|
34
|
+
def validated_slice_name(value, attribute)
|
|
35
|
+
return value if value.is_a?(String) && !value.strip.empty?
|
|
36
|
+
|
|
37
|
+
raise ArgumentError, "#{attribute} must be a non-empty String"
|
|
38
|
+
end
|
|
39
|
+
private_class_method :validated_slice_name
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchUnit
|
|
4
|
+
module Slices
|
|
5
|
+
# Pure assertions over projected slice dependencies.
|
|
6
|
+
module Assertion
|
|
7
|
+
# Immutable modifiers controlling diagram coherence checks.
|
|
8
|
+
DiagramAdherenceOptions = Data.define(:ignore_orphan_slices, :ignore_external_slices) do
|
|
9
|
+
def initialize(ignore_orphan_slices: false, ignore_external_slices: false)
|
|
10
|
+
validate_boolean(ignore_orphan_slices, :ignore_orphan_slices)
|
|
11
|
+
validate_boolean(ignore_external_slices, :ignore_external_slices)
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def with(**overrides)
|
|
16
|
+
self.class.new(**to_h, **overrides)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ignore_orphan_slices?
|
|
20
|
+
ignore_orphan_slices
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ignore_external_slices?
|
|
24
|
+
ignore_external_slices
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def validate_boolean(value, attribute)
|
|
30
|
+
return if [true, false].include?(value)
|
|
31
|
+
|
|
32
|
+
raise ArgumentError, "#{attribute} must be true or false"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/assertion/violation'
|
|
4
|
+
require_relative '../../common/projection/projected_edge'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Slices
|
|
8
|
+
# Pure assertions over projected slice dependencies.
|
|
9
|
+
module Assertion
|
|
10
|
+
# Structured evidence for a dependency rejected by a slice rule.
|
|
11
|
+
class SliceDependencyViolation < Common::Assertion::Violation
|
|
12
|
+
RULES = %i[contain_dependency adhere_to_diagram].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :dependency, :source_slice, :target_slice, :rule, :is_negated
|
|
15
|
+
|
|
16
|
+
def initialize(dependency:, source_slice:, target_slice:, rule:, is_negated:)
|
|
17
|
+
@dependency = projected_edge(dependency)
|
|
18
|
+
@source_slice = slice_name(source_slice, :source_slice)
|
|
19
|
+
@target_slice = slice_name(target_slice, :target_slice)
|
|
20
|
+
@rule = rule_value(rule)
|
|
21
|
+
@is_negated = boolean(is_negated, :is_negated)
|
|
22
|
+
super()
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def negated?
|
|
26
|
+
is_negated
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ==(other)
|
|
30
|
+
other.is_a?(self.class) && values == other.send(:values)
|
|
31
|
+
end
|
|
32
|
+
alias eql? ==
|
|
33
|
+
|
|
34
|
+
def hash
|
|
35
|
+
[self.class, *values].hash
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def values
|
|
41
|
+
[dependency, source_slice, target_slice, rule, is_negated]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def projected_edge(value)
|
|
45
|
+
return value if value.is_a?(Common::Projection::ProjectedEdge)
|
|
46
|
+
|
|
47
|
+
raise ArgumentError, 'dependency must be a ProjectedEdge'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def slice_name(value, attribute)
|
|
51
|
+
return value.dup.freeze if value.is_a?(String) && !value.strip.empty?
|
|
52
|
+
|
|
53
|
+
raise ArgumentError, "#{attribute} must be a non-empty String"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def rule_value(value)
|
|
57
|
+
return value if RULES.include?(value)
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "rule must be one of: #{RULES.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def boolean(value, attribute)
|
|
63
|
+
return value if [true, false].include?(value)
|
|
64
|
+
|
|
65
|
+
raise ArgumentError, "#{attribute} must be true or false"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../../common/projection/project_edges'
|
|
5
|
+
require_relative '../../extraction/extract_graph'
|
|
6
|
+
require_relative '../assertion/adhere_to_diagram'
|
|
7
|
+
require_relative '../uml/parse_diagram'
|
|
8
|
+
|
|
9
|
+
module ArchUnit
|
|
10
|
+
module Slices
|
|
11
|
+
module FluentApi
|
|
12
|
+
# Executable `slices should adhere to diagram` rule.
|
|
13
|
+
class DiagramSliceCondition
|
|
14
|
+
include Common::FluentApi::Checkable
|
|
15
|
+
|
|
16
|
+
attr_reader :scope, :project_locator, :projection, :diagram_source, :options
|
|
17
|
+
|
|
18
|
+
def initialize(scope, diagram_source, options:)
|
|
19
|
+
@scope = scope_value(scope)
|
|
20
|
+
@project_locator = scope.project_locator
|
|
21
|
+
@projection = scope.projection
|
|
22
|
+
@diagram_source = diagram_source_value(diagram_source)
|
|
23
|
+
@options = options_value(options)
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def negated?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def perform_check(check_options)
|
|
34
|
+
graph = ArchUnit::Extraction.extract_graph(project_locator, options: check_options)
|
|
35
|
+
empty_test = empty_test_violation(
|
|
36
|
+
projection.slice_labels(graph), filters: [], negated: false, options: check_options
|
|
37
|
+
)
|
|
38
|
+
return empty_test if empty_test
|
|
39
|
+
|
|
40
|
+
diagram = Uml::PlantUmlParser.parse(diagram_source.read)
|
|
41
|
+
edges = Common::Projection.project_edges(graph, projection)
|
|
42
|
+
Assertion.gather_diagram_adherence_violations(edges, diagram, options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def scope_value(value)
|
|
46
|
+
return value if value.is_a?(SliceScopeBuilder)
|
|
47
|
+
|
|
48
|
+
raise ArgumentError, 'scope must be a SliceScopeBuilder'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def diagram_source_value(value)
|
|
52
|
+
return value if value.is_a?(DiagramSource)
|
|
53
|
+
|
|
54
|
+
raise ArgumentError, 'diagram_source must be a DiagramSource'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def options_value(value)
|
|
58
|
+
return value if value.is_a?(Assertion::DiagramAdherenceOptions)
|
|
59
|
+
|
|
60
|
+
raise ArgumentError, 'options must be DiagramAdherenceOptions'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchUnit
|
|
4
|
+
module Slices
|
|
5
|
+
module FluentApi
|
|
6
|
+
DIAGRAM_SOURCE_KINDS = %i[inline file].freeze
|
|
7
|
+
|
|
8
|
+
# Immutable inline or file-backed PlantUML source, read only by a terminal check.
|
|
9
|
+
DiagramSource = Data.define(:kind, :value) do
|
|
10
|
+
def self.inline(text)
|
|
11
|
+
new(kind: :inline, value: text)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.file(path)
|
|
15
|
+
path = path.to_path if path.respond_to?(:to_path)
|
|
16
|
+
new(kind: :file, value: path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(kind:, value:)
|
|
20
|
+
unless DIAGRAM_SOURCE_KINDS.include?(kind)
|
|
21
|
+
raise ArgumentError, "kind must be one of: #{DIAGRAM_SOURCE_KINDS.join(', ')}"
|
|
22
|
+
end
|
|
23
|
+
unless value.is_a?(String) && !value.empty?
|
|
24
|
+
raise ArgumentError, 'diagram source value must be a non-empty String'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
super(kind:, value: value.dup.freeze)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def read
|
|
31
|
+
kind == :inline ? value : File.read(value, encoding: Encoding::UTF_8)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../../common/projection/project_edges'
|
|
5
|
+
require_relative '../../extraction/extract_graph'
|
|
6
|
+
require_relative '../assertion/contain_dependency'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Slices
|
|
10
|
+
module FluentApi
|
|
11
|
+
# Executable `slices should not contain dependency` rule.
|
|
12
|
+
class ForbiddenSliceDependencyCondition
|
|
13
|
+
include Common::FluentApi::Checkable
|
|
14
|
+
|
|
15
|
+
attr_reader :scope, :project_locator, :projection, :source_slice, :target_slice
|
|
16
|
+
|
|
17
|
+
def initialize(scope, source_slice:, target_slice:)
|
|
18
|
+
unless scope.is_a?(SliceScopeBuilder)
|
|
19
|
+
raise ArgumentError, 'scope must be a SliceScopeBuilder'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@scope = scope
|
|
23
|
+
@project_locator = scope.project_locator
|
|
24
|
+
@projection = scope.projection
|
|
25
|
+
@source_slice = immutable_slice_name(source_slice, :source_slice)
|
|
26
|
+
@target_slice = immutable_slice_name(target_slice, :target_slice)
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def negated?
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def perform_check(options)
|
|
37
|
+
graph = ArchUnit::Extraction.extract_graph(project_locator, options:)
|
|
38
|
+
empty_test = empty_test_violation(
|
|
39
|
+
projection.slice_labels(graph), filters: [], negated: true, options:
|
|
40
|
+
)
|
|
41
|
+
return empty_test if empty_test
|
|
42
|
+
|
|
43
|
+
edges = Common::Projection.project_edges(graph, projection)
|
|
44
|
+
Assertion.gather_forbidden_slice_dependency_violations(
|
|
45
|
+
edges, source_slice, target_slice
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def immutable_slice_name(value, attribute)
|
|
50
|
+
return value.dup.freeze if value.is_a?(String) && !value.strip.empty?
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, "#{attribute} must be a non-empty String"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'forbidden_slice_dependency_condition'
|
|
4
|
+
|
|
5
|
+
module ArchUnit
|
|
6
|
+
module Slices
|
|
7
|
+
module FluentApi
|
|
8
|
+
# Immutable negated mood for forbidden slice dependencies.
|
|
9
|
+
class NegativeSliceConditionBuilder
|
|
10
|
+
attr_reader :scope
|
|
11
|
+
|
|
12
|
+
def initialize(scope)
|
|
13
|
+
unless scope.is_a?(SliceScopeBuilder)
|
|
14
|
+
raise ArgumentError, 'scope must be a SliceScopeBuilder'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@scope = scope
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def contain_dependency(source_slice, target_slice)
|
|
22
|
+
ForbiddenSliceDependencyCondition.new(scope, source_slice:, target_slice:)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../assertion/diagram_adherence_options'
|
|
4
|
+
require_relative 'diagram_slice_condition'
|
|
5
|
+
require_relative 'diagram_source'
|
|
6
|
+
|
|
7
|
+
module ArchUnit
|
|
8
|
+
module Slices
|
|
9
|
+
module FluentApi
|
|
10
|
+
# Immutable positive mood and optional modifiers for diagram adherence.
|
|
11
|
+
class PositiveSliceConditionBuilder
|
|
12
|
+
attr_reader :scope, :options
|
|
13
|
+
|
|
14
|
+
def initialize(scope, options: Assertion::DiagramAdherenceOptions.new)
|
|
15
|
+
unless scope.is_a?(SliceScopeBuilder)
|
|
16
|
+
raise ArgumentError, 'scope must be a SliceScopeBuilder'
|
|
17
|
+
end
|
|
18
|
+
unless options.is_a?(Assertion::DiagramAdherenceOptions)
|
|
19
|
+
raise ArgumentError, 'options must be DiagramAdherenceOptions'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@scope = scope
|
|
23
|
+
@options = options
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ignoring_orphan_slices
|
|
28
|
+
copy(options.with(ignore_orphan_slices: true))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ignoring_external_slices
|
|
32
|
+
copy(options.with(ignore_external_slices: true))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def adhere_to_diagram(text)
|
|
36
|
+
DiagramSliceCondition.new(scope, DiagramSource.inline(text), options:)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def adhere_to_diagram_in_file(path)
|
|
40
|
+
DiagramSliceCondition.new(scope, DiagramSource.file(path), options:)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def copy(new_options)
|
|
46
|
+
self.class.new(scope, options: new_options)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../projection/slicing_projections'
|
|
4
|
+
require_relative 'negative_slice_condition_builder'
|
|
5
|
+
require_relative 'positive_slice_condition_builder'
|
|
6
|
+
require_relative '../uml/export_diagram'
|
|
7
|
+
require_relative '../../common/fluentapi/check_options'
|
|
8
|
+
require_relative '../../common/projection/project_edges'
|
|
9
|
+
require_relative '../../extraction/extract_graph'
|
|
10
|
+
|
|
11
|
+
module ArchUnit
|
|
12
|
+
module Slices
|
|
13
|
+
# Sentence-like builders for slice architecture rules.
|
|
14
|
+
module FluentApi
|
|
15
|
+
# Immutable scope describing how project files become named slices.
|
|
16
|
+
class SliceScopeBuilder
|
|
17
|
+
attr_reader :project_locator, :projection
|
|
18
|
+
|
|
19
|
+
def initialize(project_locator: nil, projection: Projection.identity)
|
|
20
|
+
@project_locator = immutable_project_locator(project_locator)
|
|
21
|
+
@projection = projection_value(projection)
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def defined_by(pattern, except: nil)
|
|
26
|
+
copy(projection: Projection.slice_by_pattern(pattern, except:))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def defined_by_regex(regexp, except: nil)
|
|
30
|
+
copy(projection: Projection.slice_by_regex(regexp, except:))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def should_not
|
|
34
|
+
NegativeSliceConditionBuilder.new(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def should
|
|
38
|
+
PositiveSliceConditionBuilder.new(self)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_plantuml(options = nil)
|
|
42
|
+
graph = extract_graph(options)
|
|
43
|
+
edges = Common::Projection.project_edges(graph, projection)
|
|
44
|
+
Uml::PlantUmlRenderer.render(edges, components: projection.slice_labels(graph))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def export_as_plantuml(output_path, options = nil)
|
|
48
|
+
graph = extract_graph(options)
|
|
49
|
+
edges = Common::Projection.project_edges(graph, projection)
|
|
50
|
+
Uml::PlantUmlRenderer.export(
|
|
51
|
+
edges, output_path, components: projection.slice_labels(graph)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def copy(projection: self.projection)
|
|
58
|
+
self.class.new(project_locator:, projection:)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_graph(options)
|
|
62
|
+
options = Common::FluentApi::CheckOptions.resolve(options)
|
|
63
|
+
ArchUnit::Extraction.extract_graph(project_locator, options:)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def immutable_project_locator(locator)
|
|
67
|
+
return if locator.nil?
|
|
68
|
+
|
|
69
|
+
locator = locator.to_path if locator.respond_to?(:to_path)
|
|
70
|
+
return locator.dup.freeze if locator.is_a?(String) && !locator.empty?
|
|
71
|
+
|
|
72
|
+
raise ArgumentError, 'project_locator must be a non-empty path or nil'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def projection_value(value)
|
|
76
|
+
return value if value.is_a?(Projection::SliceProjection)
|
|
77
|
+
|
|
78
|
+
raise ArgumentError, 'projection must be a SliceProjection'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'slice_scope_builder'
|
|
4
|
+
|
|
5
|
+
# Public ArchUnitRuby entry points for slice architecture rules.
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Slices
|
|
8
|
+
# Sentence-like entry points and builders for slice architecture rules.
|
|
9
|
+
module FluentApi
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def project_slices(project_locator = nil)
|
|
13
|
+
SliceScopeBuilder.new(project_locator:)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
alias slices project_slices
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.project_slices(project_locator = nil)
|
|
23
|
+
Slices::FluentApi.project_slices(project_locator)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
alias slices project_slices
|
|
28
|
+
end
|
|
29
|
+
end
|