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.
Files changed (149) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +13 -0
  3. data/LICENSE +7 -0
  4. data/README.md +375 -0
  5. data/lib/archunit/common/assertion/empty_test_violation.rb +50 -0
  6. data/lib/archunit/common/assertion/violation.rb +14 -0
  7. data/lib/archunit/common/extraction/edge.rb +46 -0
  8. data/lib/archunit/common/extraction/graph.rb +73 -0
  9. data/lib/archunit/common/extraction/import_kind.rb +21 -0
  10. data/lib/archunit/common/filter.rb +46 -0
  11. data/lib/archunit/common/fluentapi/check_options.rb +48 -0
  12. data/lib/archunit/common/fluentapi/checkable.rb +70 -0
  13. data/lib/archunit/common/logging/check_logger.rb +124 -0
  14. data/lib/archunit/common/logging/logging_options.rb +57 -0
  15. data/lib/archunit/common/pattern.rb +99 -0
  16. data/lib/archunit/common/pattern_matching.rb +76 -0
  17. data/lib/archunit/common/projection/cycles/johnson_cycles.rb +103 -0
  18. data/lib/archunit/common/projection/cycles/tarjan_scc.rb +82 -0
  19. data/lib/archunit/common/projection/edge_projections.rb +50 -0
  20. data/lib/archunit/common/projection/mapped_edge.rb +26 -0
  21. data/lib/archunit/common/projection/project_cycles.rb +92 -0
  22. data/lib/archunit/common/projection/project_edges.rb +69 -0
  23. data/lib/archunit/common/projection/project_to_nodes.rb +59 -0
  24. data/lib/archunit/common/projection/projected_edge.rb +35 -0
  25. data/lib/archunit/common/projection/projected_node.rb +36 -0
  26. data/lib/archunit/common/regex_factory.rb +96 -0
  27. data/lib/archunit/error/technical_error.rb +6 -0
  28. data/lib/archunit/error/user_error.rb +6 -0
  29. data/lib/archunit/extraction/enumerate_source_files.rb +131 -0
  30. data/lib/archunit/extraction/extract_dependencies.rb +61 -0
  31. data/lib/archunit/extraction/extract_graph.rb +89 -0
  32. data/lib/archunit/extraction/extract_imports.rb +197 -0
  33. data/lib/archunit/extraction/locate_project.rb +80 -0
  34. data/lib/archunit/extraction/resolve_import.rb +76 -0
  35. data/lib/archunit/extraction/resolved_import.rb +46 -0
  36. data/lib/archunit/files/assertion/custom_file_condition.rb +89 -0
  37. data/lib/archunit/files/assertion/cycle_free.rb +58 -0
  38. data/lib/archunit/files/assertion/depend_on_external_modules.rb +116 -0
  39. data/lib/archunit/files/assertion/depend_on_files.rb +110 -0
  40. data/lib/archunit/files/assertion/matching_files.rb +87 -0
  41. data/lib/archunit/files/extraction/extract_file_info.rb +84 -0
  42. data/lib/archunit/files/extraction/file_info.rb +46 -0
  43. data/lib/archunit/files/fluentapi/custom_file_condition.rb +73 -0
  44. data/lib/archunit/files/fluentapi/cycle_free_file_condition.rb +55 -0
  45. data/lib/archunit/files/fluentapi/depend_on_external_module_condition.rb +69 -0
  46. data/lib/archunit/files/fluentapi/depend_on_external_module_condition_builder.rb +36 -0
  47. data/lib/archunit/files/fluentapi/depend_on_file_condition.rb +80 -0
  48. data/lib/archunit/files/fluentapi/depend_on_file_condition_builder.rb +49 -0
  49. data/lib/archunit/files/fluentapi/file_condition_builder.rb +71 -0
  50. data/lib/archunit/files/fluentapi/file_rule_support.rb +24 -0
  51. data/lib/archunit/files/fluentapi/files.rb +31 -0
  52. data/lib/archunit/files/fluentapi/match_pattern_file_condition.rb +59 -0
  53. data/lib/archunit/files/fluentapi/match_pattern_file_condition_builder.rb +66 -0
  54. data/lib/archunit/files/fluentapi/negated_match_pattern_file_condition_builder.rb +16 -0
  55. data/lib/archunit/files/fluentapi/positive_match_pattern_file_condition_builder.rb +21 -0
  56. data/lib/archunit/graph/fluentapi/graph.rb +29 -0
  57. data/lib/archunit/graph/fluentapi/project_graph_builder.rb +106 -0
  58. data/lib/archunit/graph/projection/aggregate_edges.rb +48 -0
  59. data/lib/archunit/graph/projection/collapse_node.rb +33 -0
  60. data/lib/archunit/graph/projection/create_snapshot.rb +84 -0
  61. data/lib/archunit/graph/projection/folder_depth_collapse.rb +18 -0
  62. data/lib/archunit/graph/projection/graph_query_options.rb +81 -0
  63. data/lib/archunit/graph/projection/graph_report_edge.rb +50 -0
  64. data/lib/archunit/graph/projection/graph_report_node.rb +22 -0
  65. data/lib/archunit/graph/projection/graph_report_snapshot.rb +43 -0
  66. data/lib/archunit/graph/projection/graph_report_summary.rb +20 -0
  67. data/lib/archunit/graph/projection/node_selection.rb +111 -0
  68. data/lib/archunit/graph/projection/pattern_collapse.rb +24 -0
  69. data/lib/archunit/graph/rendering/csv_renderer.rb +30 -0
  70. data/lib/archunit/graph/rendering/d2_renderer.rb +30 -0
  71. data/lib/archunit/graph/rendering/dot_renderer.rb +51 -0
  72. data/lib/archunit/graph/rendering/escaping.rb +31 -0
  73. data/lib/archunit/graph/rendering/export_report.rb +29 -0
  74. data/lib/archunit/graph/rendering/graph_renderer.rb +54 -0
  75. data/lib/archunit/graph/rendering/html_document.rb +46 -0
  76. data/lib/archunit/graph/rendering/html_renderer.rb +102 -0
  77. data/lib/archunit/graph/rendering/json_renderer.rb +38 -0
  78. data/lib/archunit/graph/rendering/mermaid_renderer.rb +33 -0
  79. data/lib/archunit/graph/rendering/rendering_support.rb +24 -0
  80. data/lib/archunit/layers/assertion/layer_definition.rb +56 -0
  81. data/lib/archunit/layers/assertion/layer_dependencies.rb +91 -0
  82. data/lib/archunit/layers/assertion/layer_dependency_violation.rb +59 -0
  83. data/lib/archunit/layers/fluentapi/layer_definition_builder.rb +46 -0
  84. data/lib/archunit/layers/fluentapi/layer_dependency_rule_builder.rb +44 -0
  85. data/lib/archunit/layers/fluentapi/layered_architecture.rb +133 -0
  86. data/lib/archunit/layers/fluentapi/layered_architecture_values.rb +51 -0
  87. data/lib/archunit/layers/fluentapi/layers.rb +29 -0
  88. data/lib/archunit/metrics/assertion/custom_metric.rb +73 -0
  89. data/lib/archunit/metrics/assertion/metric_predicate.rb +66 -0
  90. data/lib/archunit/metrics/assertion/metric_threshold.rb +99 -0
  91. data/lib/archunit/metrics/assertion/metric_zone.rb +57 -0
  92. data/lib/archunit/metrics/calculation/count.rb +45 -0
  93. data/lib/archunit/metrics/calculation/distance.rb +84 -0
  94. data/lib/archunit/metrics/calculation/lcom.rb +103 -0
  95. data/lib/archunit/metrics/calculation/metric.rb +52 -0
  96. data/lib/archunit/metrics/calculation/numeric_value.rb +25 -0
  97. data/lib/archunit/metrics/extraction/distance_info.rb +60 -0
  98. data/lib/archunit/metrics/extraction/extract_distance_info.rb +50 -0
  99. data/lib/archunit/metrics/extraction/extract_project_info.rb +90 -0
  100. data/lib/archunit/metrics/extraction/metric_info.rb +159 -0
  101. data/lib/archunit/metrics/extraction/source_metrics_visitor.rb +278 -0
  102. data/lib/archunit/metrics/fluentapi/count_metrics_builder.rb +49 -0
  103. data/lib/archunit/metrics/fluentapi/custom_metric_builder.rb +30 -0
  104. data/lib/archunit/metrics/fluentapi/custom_metric_condition.rb +40 -0
  105. data/lib/archunit/metrics/fluentapi/distance_metrics_builder.rb +48 -0
  106. data/lib/archunit/metrics/fluentapi/lcom_metrics_builder.rb +39 -0
  107. data/lib/archunit/metrics/fluentapi/metric_measurement.rb +36 -0
  108. data/lib/archunit/metrics/fluentapi/metric_predicate_condition.rb +42 -0
  109. data/lib/archunit/metrics/fluentapi/metric_report_builder.rb +34 -0
  110. data/lib/archunit/metrics/fluentapi/metric_selection.rb +54 -0
  111. data/lib/archunit/metrics/fluentapi/metric_threshold_condition.rb +47 -0
  112. data/lib/archunit/metrics/fluentapi/metrics.rb +21 -0
  113. data/lib/archunit/metrics/fluentapi/metrics_builder.rb +136 -0
  114. data/lib/archunit/metrics/fluentapi/zone_condition.rb +40 -0
  115. data/lib/archunit/metrics/reporting/metrics_export_options.rb +57 -0
  116. data/lib/archunit/metrics/reporting/metrics_exporter.rb +130 -0
  117. data/lib/archunit/slices/assertion/adhere_to_diagram.rb +57 -0
  118. data/lib/archunit/slices/assertion/contain_dependency.rb +42 -0
  119. data/lib/archunit/slices/assertion/diagram_adherence_options.rb +37 -0
  120. data/lib/archunit/slices/assertion/slice_dependency_violation.rb +70 -0
  121. data/lib/archunit/slices/fluentapi/diagram_slice_condition.rb +65 -0
  122. data/lib/archunit/slices/fluentapi/diagram_source.rb +36 -0
  123. data/lib/archunit/slices/fluentapi/forbidden_slice_dependency_condition.rb +57 -0
  124. data/lib/archunit/slices/fluentapi/negative_slice_condition_builder.rb +27 -0
  125. data/lib/archunit/slices/fluentapi/positive_slice_condition_builder.rb +51 -0
  126. data/lib/archunit/slices/fluentapi/slice_scope_builder.rb +83 -0
  127. data/lib/archunit/slices/fluentapi/slices.rb +29 -0
  128. data/lib/archunit/slices/projection/slice_projection.rb +76 -0
  129. data/lib/archunit/slices/projection/slicing_projections.rb +98 -0
  130. data/lib/archunit/slices/uml/export_diagram.rb +70 -0
  131. data/lib/archunit/slices/uml/parse_diagram.rb +53 -0
  132. data/lib/archunit/slices/uml/plant_uml_dependency.rb +25 -0
  133. data/lib/archunit/slices/uml/plant_uml_diagram.rb +44 -0
  134. data/lib/archunit/testing/assert_passes.rb +24 -0
  135. data/lib/archunit/testing/assertion_failure.rb +22 -0
  136. data/lib/archunit/testing/color_utils.rb +66 -0
  137. data/lib/archunit/testing/layer_violation_formatter.rb +26 -0
  138. data/lib/archunit/testing/metric_violation_formatter.rb +58 -0
  139. data/lib/archunit/testing/minitest_adapter.rb +33 -0
  140. data/lib/archunit/testing/result_factory.rb +93 -0
  141. data/lib/archunit/testing/rspec_adapter.rb +74 -0
  142. data/lib/archunit/testing/slice_violation_formatter.rb +25 -0
  143. data/lib/archunit/testing/test_result.rb +26 -0
  144. data/lib/archunit/testing/test_violation.rb +23 -0
  145. data/lib/archunit/testing/violation_factory.rb +138 -0
  146. data/lib/archunit/testing.rb +39 -0
  147. data/lib/archunit/version.rb +5 -0
  148. data/lib/archunit.rb +110 -0
  149. 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