rails-mmd 0.1.0
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 +10 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/SECURITY.md +40 -0
- data/docs/active-record-association-support.md +37 -0
- data/docs/assets/branding/rails-mmd-social-preview.jpg +0 -0
- data/docs/usage.md +126 -0
- data/exe/rails-mmd +6 -0
- data/fixtures/schemas/diagnostics/valid/catalog.json +39 -0
- data/lib/rails_mmd/artifact_ref_sanitizer.rb +59 -0
- data/lib/rails_mmd/attribute_types.rb +29 -0
- data/lib/rails_mmd/canonical_json.rb +19 -0
- data/lib/rails_mmd/cli.rb +86 -0
- data/lib/rails_mmd/config.rb +249 -0
- data/lib/rails_mmd/constant_resolver.rb +33 -0
- data/lib/rails_mmd/diagnostic_factory.rb +122 -0
- data/lib/rails_mmd/domain_resolver.rb +97 -0
- data/lib/rails_mmd/exit_policy.rb +27 -0
- data/lib/rails_mmd/generate_command.rb +116 -0
- data/lib/rails_mmd/generation_pipeline.rb +109 -0
- data/lib/rails_mmd/ir_builder.rb +122 -0
- data/lib/rails_mmd/mermaid_serializer.rb +168 -0
- data/lib/rails_mmd/metadata_shapes.rb +55 -0
- data/lib/rails_mmd/model_inventory.rb +144 -0
- data/lib/rails_mmd/ordering.rb +25 -0
- data/lib/rails_mmd/output_directory.rb +192 -0
- data/lib/rails_mmd/publisher.rb +166 -0
- data/lib/rails_mmd/rails_loader.rb +96 -0
- data/lib/rails_mmd/redactor.rb +143 -0
- data/lib/rails_mmd/relationship_builder.rb +982 -0
- data/lib/rails_mmd/render_plan_builder.rb +192 -0
- data/lib/rails_mmd/safe_token_assigner.rb +130 -0
- data/lib/rails_mmd/schema_probe.rb +376 -0
- data/lib/rails_mmd/schema_validator.rb +136 -0
- data/lib/rails_mmd/subject_ids.rb +17 -0
- data/lib/rails_mmd/tooling_tasks.rb +47 -0
- data/lib/rails_mmd/verification_runner.rb +143 -0
- data/lib/rails_mmd/version.rb +5 -0
- data/lib/rails_mmd.rb +15 -0
- data/schemas/config.schema.json +54 -0
- data/schemas/diagnostics.schema.json +418 -0
- data/schemas/ir.schema.json +84 -0
- data/schemas/render_plan.schema.json +136 -0
- metadata +135 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'json_schemer'
|
|
5
|
+
require 'rails_mmd/canonical_json'
|
|
6
|
+
require 'rails_mmd/ordering'
|
|
7
|
+
|
|
8
|
+
module RailsMmd
|
|
9
|
+
# Validates payloads against repository JSON Schemas.
|
|
10
|
+
class SchemaValidator
|
|
11
|
+
class InvalidPayload < ArgumentError; end
|
|
12
|
+
|
|
13
|
+
SCHEMA_FILES = {
|
|
14
|
+
config: 'config.schema.json',
|
|
15
|
+
diagnostics: 'diagnostics.schema.json',
|
|
16
|
+
ir: 'ir.schema.json',
|
|
17
|
+
render_plan: 'render_plan.schema.json'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(schema_root: Pathname(__dir__).join('../../schemas').expand_path)
|
|
21
|
+
@schema_root = Pathname(schema_root)
|
|
22
|
+
@schemas = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def valid?(schema_name, payload) = errors(schema_name, payload).empty?
|
|
26
|
+
|
|
27
|
+
def errors(schema_name, payload)
|
|
28
|
+
schema(schema_name).validate(payload).to_a + attribute_identity_errors(schema_name, payload)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate!(schema_name, payload)
|
|
32
|
+
return payload if valid?(schema_name, payload)
|
|
33
|
+
|
|
34
|
+
raise InvalidPayload, "#{schema_name.to_s.tr('_', ' ')} schema invalid"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_render_plan!(render_plan) = validate!(:render_plan, render_plan)
|
|
38
|
+
|
|
39
|
+
def validate_pre_output!(diagnostics)
|
|
40
|
+
validate!(:diagnostics, diagnostic_document('global', nil, diagnostics))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate_publication!(diagnostics:, artifacts:)
|
|
44
|
+
diagnostic_documents = diagnostic_documents(diagnostics)
|
|
45
|
+
diagnostic_documents.each { |document| validate!(:diagnostics, document) }
|
|
46
|
+
each_render_plan(artifacts) { |render_plan| validate_render_plan!(render_plan) }
|
|
47
|
+
validate_diagnostic_refs!(diagnostic_documents, artifacts)
|
|
48
|
+
diagnostic_documents
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
attr_reader :schema_root, :schemas
|
|
54
|
+
|
|
55
|
+
def schema(schema_name)
|
|
56
|
+
key = schema_name.to_sym
|
|
57
|
+
schemas[key] ||= JSONSchemer.schema(JSON.parse(schema_root.join(SCHEMA_FILES.fetch(key)).read))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def attribute_identity_errors(schema_name, payload)
|
|
61
|
+
return [] unless %i[ir render_plan].include?(schema_name.to_sym)
|
|
62
|
+
return [] unless payload.is_a?(Hash) && payload['entities'].is_a?(Array)
|
|
63
|
+
|
|
64
|
+
seen = {}
|
|
65
|
+
payload['entities'].each_with_index.flat_map { |entity, index| entity_attribute_errors(entity, index, seen) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def entity_attribute_errors(entity, entity_index, seen)
|
|
69
|
+
return [] unless entity.is_a?(Hash) && entity['attributes'].is_a?(Array)
|
|
70
|
+
|
|
71
|
+
entity['attributes'].each_with_index.filter_map do |attribute, index|
|
|
72
|
+
duplicate_attribute_error(attribute, "/entities/#{entity_index}/attributes/#{index}/attribute_id", seen)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def duplicate_attribute_error(attribute, pointer, seen)
|
|
77
|
+
return unless attribute.is_a?(Hash) && attribute['attribute_id'].is_a?(String)
|
|
78
|
+
|
|
79
|
+
attribute_id = attribute.fetch('attribute_id')
|
|
80
|
+
duplicate = seen.key?(attribute_id)
|
|
81
|
+
seen[attribute_id] = true
|
|
82
|
+
return unless duplicate
|
|
83
|
+
|
|
84
|
+
{ 'data_pointer' => pointer, 'type' => 'unique_attribute_identity' }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def each_render_plan(artifacts)
|
|
88
|
+
artifacts.each_value do |domain_artifacts|
|
|
89
|
+
domain_artifacts.each_value { |artifact| yield artifact.fetch(:render_plan) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_diagnostic_refs!(diagnostic_documents, artifacts)
|
|
94
|
+
diagnostic_ids = diagnostic_ids_by_domain(diagnostic_documents)
|
|
95
|
+
artifacts.each do |domain_id, domain_artifacts|
|
|
96
|
+
allowed_ids = diagnostic_ids.fetch(nil, []) + diagnostic_ids.fetch(domain_id, [])
|
|
97
|
+
each_render_plan(domain_id => domain_artifacts) do |render_plan|
|
|
98
|
+
unresolved = render_plan.fetch('diagnostic_ids') - allowed_ids
|
|
99
|
+
raise InvalidPayload, 'render plan diagnostic_ids unresolved' unless unresolved.empty?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def diagnostic_ids_by_domain(diagnostic_documents)
|
|
105
|
+
diagnostic_documents.each_with_object({}) do |document, ids|
|
|
106
|
+
domain_id = document.fetch('scope') == 'global' ? nil : document.fetch('domain_id')
|
|
107
|
+
ids[domain_id] ||= []
|
|
108
|
+
ids[domain_id].concat(document.fetch('diagnostics').map { |diagnostic| diagnostic.fetch('diagnostic_id') })
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def diagnostic_documents(diagnostics)
|
|
113
|
+
groups = diagnostics.group_by do |diagnostic|
|
|
114
|
+
diagnostic.fetch('scope') == 'invocation' ? ['global', nil] : ['domain', diagnostic_domain_id(diagnostic)]
|
|
115
|
+
end
|
|
116
|
+
groups.map do |(scope, domain_id), items|
|
|
117
|
+
diagnostic_document(scope, domain_id, items)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def diagnostic_document(scope, domain_id, diagnostics)
|
|
122
|
+
payload = {
|
|
123
|
+
'schema_version' => 1,
|
|
124
|
+
'scope' => scope,
|
|
125
|
+
'domain_id' => domain_id,
|
|
126
|
+
'diagnostics' => Ordering.sort_diagnostics(diagnostics),
|
|
127
|
+
'digest_sha256' => nil
|
|
128
|
+
}
|
|
129
|
+
payload.merge('digest_sha256' => CanonicalJson.digest_sha256(payload))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def diagnostic_domain_id(diagnostic)
|
|
133
|
+
diagnostic.fetch('metadata', {}).fetch('domain_id')
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsMmd
|
|
4
|
+
# Normalizes diagnostic subject identifiers without treating them as free text.
|
|
5
|
+
module SubjectIds
|
|
6
|
+
PATTERN = /\A[A-Za-z0-9_:.]+\z/
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def normalize(value, code:)
|
|
11
|
+
subject_id = value&.to_s
|
|
12
|
+
return subject_id if subject_id.nil? || subject_id.match?(PATTERN)
|
|
13
|
+
|
|
14
|
+
raise ArgumentError, "invalid diagnostic subject_id for #{code}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsMmd
|
|
4
|
+
# Shared command builders for repository Rake tooling.
|
|
5
|
+
module ToolingTasks
|
|
6
|
+
BUNDLE_AUDIT_COMMAND = %w[bundle exec bundler-audit check --no-update].freeze
|
|
7
|
+
BUNDLE_AUDIT_UPDATE_COMMAND = %w[bundle exec bundler-audit update].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def git_ref?(ref)
|
|
12
|
+
system('git', 'rev-parse', '--verify', '--quiet', "#{ref}^{commit}", out: File::NULL)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def undercover_compare_ref(default_ref)
|
|
16
|
+
env_ref = ENV.fetch('UNDERCOVER_COMPARE', nil)
|
|
17
|
+
unless env_ref.nil?
|
|
18
|
+
abort "UNDERCOVER_COMPARE does not resolve to a commit: #{env_ref.inspect}" unless git_ref?(env_ref)
|
|
19
|
+
|
|
20
|
+
return env_ref
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
[default_ref, 'origin/main', 'main', 'HEAD^'].find { |ref| git_ref?(ref) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def undercover_command(config)
|
|
27
|
+
compare_ref = undercover_compare_ref(config.fetch('compare'))
|
|
28
|
+
abort 'No usable Undercover compare ref found' unless compare_ref
|
|
29
|
+
|
|
30
|
+
[
|
|
31
|
+
{ 'SIMPLECOV_NO_DEFAULTS' => 'true' },
|
|
32
|
+
'bundle', 'exec', 'undercover',
|
|
33
|
+
*undercover_options(config, compare_ref)
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def undercover_options(config, compare_ref)
|
|
38
|
+
[
|
|
39
|
+
'--simplecov', config.fetch('simplecov'),
|
|
40
|
+
'--compare', compare_ref,
|
|
41
|
+
'--include-files', config.fetch('include_files'),
|
|
42
|
+
'--exclude-files', config.fetch('exclude_files'),
|
|
43
|
+
'--max-warnings', config.fetch('max_warnings').to_s
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module RailsMmd
|
|
6
|
+
# Owns pre-commit path classification, ordering, and command selection.
|
|
7
|
+
class VerificationRunner
|
|
8
|
+
class CommandFailed < StandardError; end
|
|
9
|
+
|
|
10
|
+
RUBY_PATH = %r{
|
|
11
|
+
\A(?:
|
|
12
|
+
[^/]+\.(?:rb|rake)|Rakefile|exe/rails-mmd|bin/.+|lib/.+\.rb|
|
|
13
|
+
spec/(?:.+/)?[^/]+\.rb|features/(?:.+/)?[^/]+\.rb
|
|
14
|
+
)\z
|
|
15
|
+
}x
|
|
16
|
+
DIRECT_SPEC_PATH = %r{\Aspec/.+_spec\.rb\z}
|
|
17
|
+
FULL_SPEC_SUITE_CONFIG_PATHS = %w[.rspec .simplecov .undercover.yml].freeze
|
|
18
|
+
DEPENDENCY_PATH = %r{\A(?:Gemfile(?:\.lock)?|[^/]+\.gemspec)\z}
|
|
19
|
+
CONTRACT_PATH = %r{\A(?:schemas/.+\.json|fixtures/.+\.(?:json|mmd)|AGENTS\.md|README\.md|docs/.+\.md)\z}
|
|
20
|
+
|
|
21
|
+
# The process seam has a production adapter and recording adapters in specs.
|
|
22
|
+
class ProcessAdapter
|
|
23
|
+
def run!(*command)
|
|
24
|
+
return if system(*command)
|
|
25
|
+
|
|
26
|
+
raise CommandFailed, "verification command failed: #{command.join(' ')}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def capture3(*command)
|
|
30
|
+
Open3.capture3(*command)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def success?(*command)
|
|
34
|
+
system(*command, out: File::NULL, err: File::NULL)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(process: ProcessAdapter.new, output: $stdout, error: $stderr)
|
|
39
|
+
@process = process
|
|
40
|
+
@output = output
|
|
41
|
+
@error = error
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run!(paths)
|
|
45
|
+
route(normalize_paths(paths))
|
|
46
|
+
rescue CommandFailed => e
|
|
47
|
+
error.puts(e.message)
|
|
48
|
+
raise SystemExit, 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
attr_reader :error, :output, :process
|
|
54
|
+
|
|
55
|
+
def route(paths)
|
|
56
|
+
run_ruby_checks(paths.grep(RUBY_PATH))
|
|
57
|
+
run_specs(paths)
|
|
58
|
+
run_dependency_checks(paths)
|
|
59
|
+
run_ruby_version_smoke(paths)
|
|
60
|
+
run_rubocop_config(paths)
|
|
61
|
+
run_contracts(paths)
|
|
62
|
+
report_p0_contract(paths)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def normalize_paths(paths)
|
|
66
|
+
paths.flatten.compact.map(&:to_s).reject(&:empty?).uniq
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def run_ruby_checks(paths)
|
|
70
|
+
return if paths.empty?
|
|
71
|
+
|
|
72
|
+
ensure_no_mixed_changes!(paths)
|
|
73
|
+
process.run!('bundle', 'exec', 'rubocop', '--autocorrect-all', '--force-exclusion', '--', *paths)
|
|
74
|
+
ensure_no_post_repair_diff!
|
|
75
|
+
process.run!('bundle', 'exec', 'rubocop', '--force-exclusion', '--', *paths)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ensure_no_mixed_changes!(paths)
|
|
79
|
+
mixed_paths = unstaged_changed_paths(paths)
|
|
80
|
+
return if mixed_paths.empty?
|
|
81
|
+
|
|
82
|
+
report_mixed_paths(mixed_paths)
|
|
83
|
+
raise SystemExit, 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def unstaged_changed_paths(paths)
|
|
87
|
+
stdout, stderr, status = process.capture3('git', 'diff', '--name-only', '--', *paths)
|
|
88
|
+
raise CommandFailed, stderr unless status.success?
|
|
89
|
+
|
|
90
|
+
stdout.lines.map(&:chomp).reject(&:empty?)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def report_mixed_paths(mixed_paths)
|
|
94
|
+
error.puts('pre-commit blocked: these staged files also have unstaged changes:')
|
|
95
|
+
mixed_paths.each { |path| error.puts(" #{path}") }
|
|
96
|
+
error.puts('Stage or stash the unstaged changes before repair commands run.')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ensure_no_post_repair_diff!
|
|
100
|
+
return if process.success?('git', 'diff', '--quiet')
|
|
101
|
+
|
|
102
|
+
error.puts('pre-commit blocked: repair changed tracked files.')
|
|
103
|
+
error.puts('Inspect the deterministic repair diff, then stage the intended changes.')
|
|
104
|
+
raise SystemExit, 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def run_specs(paths)
|
|
108
|
+
if paths.intersect?(FULL_SPEC_SUITE_CONFIG_PATHS)
|
|
109
|
+
process.run!('bundle', 'exec', 'rake', 'coverage')
|
|
110
|
+
elsif (specs = paths.grep(DIRECT_SPEC_PATH)).any?
|
|
111
|
+
process.run!('bundle', 'exec', 'rspec', *specs)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def run_dependency_checks(paths)
|
|
116
|
+
return unless paths.any? { |path| path.match?(DEPENDENCY_PATH) }
|
|
117
|
+
|
|
118
|
+
process.run!('bundle', 'check')
|
|
119
|
+
process.run!('bundle', 'exec', 'rake', 'bundle:audit')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def run_ruby_version_smoke(paths)
|
|
123
|
+
return unless paths.include?('.ruby-version')
|
|
124
|
+
|
|
125
|
+
process.run!('ruby', '-v')
|
|
126
|
+
process.run!('bundle', 'exec', 'exe/rails-mmd', '--version')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def run_rubocop_config(paths)
|
|
130
|
+
process.run!('bundle', 'exec', 'rake', 'rubocop') if paths.include?('.rubocop.yml')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def run_contracts(paths)
|
|
134
|
+
process.run!('bundle', 'exec', 'rspec', 'spec/contracts') if paths.any? { |path| path.match?(CONTRACT_PATH) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def report_p0_contract(paths)
|
|
138
|
+
return unless paths.include?('docs/p0-contract.md')
|
|
139
|
+
|
|
140
|
+
output.puts('docs/p0-contract.md changed; no P0 contract verifier exists yet.')
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/rails_mmd.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rails_mmd/version'
|
|
4
|
+
require_relative 'rails_mmd/config'
|
|
5
|
+
require_relative 'rails_mmd/constant_resolver'
|
|
6
|
+
require_relative 'rails_mmd/domain_resolver'
|
|
7
|
+
require_relative 'rails_mmd/model_inventory'
|
|
8
|
+
require_relative 'rails_mmd/rails_loader'
|
|
9
|
+
require_relative 'rails_mmd/relationship_builder'
|
|
10
|
+
require_relative 'rails_mmd/ir_builder'
|
|
11
|
+
require_relative 'rails_mmd/render_plan_builder'
|
|
12
|
+
require_relative 'rails_mmd/mermaid_serializer'
|
|
13
|
+
require_relative 'rails_mmd/publisher'
|
|
14
|
+
require_relative 'rails_mmd/generate_command'
|
|
15
|
+
require_relative 'rails_mmd/schema_probe'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://audiostakes.example/schemas/rails-mmd/config.schema.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": ["version", "domains"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"version": { "const": 1 },
|
|
9
|
+
"domains": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"minProperties": 1,
|
|
12
|
+
"propertyNames": {
|
|
13
|
+
"allOf": [
|
|
14
|
+
{ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$" },
|
|
15
|
+
{ "not": { "pattern": "\\s" } }
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"additionalProperties": { "$ref": "#/$defs/domain" }
|
|
19
|
+
},
|
|
20
|
+
"output": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"additionalProperties": false,
|
|
23
|
+
"properties": {
|
|
24
|
+
"directory": { "type": "string", "minLength": 1 },
|
|
25
|
+
"format": { "enum": ["er", "class", "both"] },
|
|
26
|
+
"attributes": { "enum": ["none", "keys"] },
|
|
27
|
+
"direction": { "enum": ["TB", "BT", "LR", "RL"] }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"$defs": {
|
|
32
|
+
"ruby_constant": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"pattern": "^[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*$"
|
|
35
|
+
},
|
|
36
|
+
"domain": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"required": ["include_models"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"include_models": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"minItems": 1,
|
|
44
|
+
"items": { "$ref": "#/$defs/ruby_constant" }
|
|
45
|
+
},
|
|
46
|
+
"exclude_models": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": { "$ref": "#/$defs/ruby_constant" },
|
|
49
|
+
"default": []
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|