zodra 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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/lib/generators/zodra/install_generator.rb +20 -0
  4. data/lib/generators/zodra/templates/initializer.rb.tt +12 -0
  5. data/lib/zodra/action.rb +43 -0
  6. data/lib/zodra/action_builder.rb +38 -0
  7. data/lib/zodra/api_builder.rb +23 -0
  8. data/lib/zodra/api_definition.rb +24 -0
  9. data/lib/zodra/api_registry.rb +33 -0
  10. data/lib/zodra/attribute.rb +46 -0
  11. data/lib/zodra/configuration.rb +18 -0
  12. data/lib/zodra/contract.rb +33 -0
  13. data/lib/zodra/contract_builder.rb +37 -0
  14. data/lib/zodra/contract_registry.rb +42 -0
  15. data/lib/zodra/controller.rb +141 -0
  16. data/lib/zodra/definition.rb +36 -0
  17. data/lib/zodra/export/contract_mapper.rb +76 -0
  18. data/lib/zodra/export/surface_resolver.rb +76 -0
  19. data/lib/zodra/export/type_analysis.rb +133 -0
  20. data/lib/zodra/export/type_script_mapper.rb +143 -0
  21. data/lib/zodra/export/writer.rb +51 -0
  22. data/lib/zodra/export/zod_mapper.rb +200 -0
  23. data/lib/zodra/export.rb +35 -0
  24. data/lib/zodra/params_coercer.rb +100 -0
  25. data/lib/zodra/params_parser.rb +90 -0
  26. data/lib/zodra/params_validator.rb +74 -0
  27. data/lib/zodra/railtie.rb +13 -0
  28. data/lib/zodra/resource.rb +51 -0
  29. data/lib/zodra/resource_builder.rb +57 -0
  30. data/lib/zodra/response_serializer.rb +63 -0
  31. data/lib/zodra/route_helper.rb +9 -0
  32. data/lib/zodra/router.rb +55 -0
  33. data/lib/zodra/scalar_registry.rb +32 -0
  34. data/lib/zodra/scalar_type.rb +13 -0
  35. data/lib/zodra/tasks/zodra.rake +41 -0
  36. data/lib/zodra/type_builder.rb +57 -0
  37. data/lib/zodra/type_deriver.rb +55 -0
  38. data/lib/zodra/type_registry.rb +42 -0
  39. data/lib/zodra/union_builder.rb +15 -0
  40. data/lib/zodra/variant.rb +12 -0
  41. data/lib/zodra/variant_builder.rb +23 -0
  42. data/lib/zodra/version.rb +5 -0
  43. data/lib/zodra.rb +147 -0
  44. metadata +113 -0
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ module Export
5
+ class SurfaceResolver
6
+ def self.call(definitions, contracts)
7
+ return definitions if contracts.empty?
8
+
9
+ new(definitions, contracts).resolve
10
+ end
11
+
12
+ def initialize(definitions, contracts)
13
+ @definitions_by_name = definitions.to_h { |d| [d.name, d] }
14
+ @contracts = contracts
15
+ end
16
+
17
+ def resolve
18
+ reachable = expand_dependencies(collect_seed_types)
19
+ @definitions_by_name.values.select { |d| reachable.include?(d.name) }
20
+ end
21
+
22
+ private
23
+
24
+ def collect_seed_types
25
+ names = Set.new
26
+
27
+ @contracts.each do |contract|
28
+ contract.actions.each_value do |action|
29
+ names << action.response_type if action.response_type
30
+ collect_attribute_references(action.params, names)
31
+ collect_attribute_references(action.response_definition, names) unless action.response_type
32
+ end
33
+ end
34
+
35
+ names
36
+ end
37
+
38
+ def expand_dependencies(seed_names)
39
+ visited = Set.new
40
+ queue = seed_names.to_a
41
+
42
+ while (name = queue.shift)
43
+ next if visited.include?(name)
44
+
45
+ visited << name
46
+ definition = @definitions_by_name[name]
47
+ next unless definition
48
+
49
+ collect_definition_dependencies(definition).each do |dep|
50
+ queue << dep unless visited.include?(dep)
51
+ end
52
+ end
53
+
54
+ visited
55
+ end
56
+
57
+ def collect_definition_dependencies(definition)
58
+ deps = Set.new
59
+ definition.attributes.each_value { |attr| add_dependency(attr, deps) }
60
+ definition.variants.each do |variant|
61
+ variant.attributes.each_value { |attr| add_dependency(attr, deps) }
62
+ end
63
+ deps
64
+ end
65
+
66
+ def collect_attribute_references(definition, names)
67
+ definition.attributes.each_value { |attr| add_dependency(attr, names) }
68
+ end
69
+
70
+ def add_dependency(attribute, set)
71
+ dep = attribute.dependency_name
72
+ set << dep if dep
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ module Export
5
+ class TypeAnalysis
6
+ Result = Data.define(:sorted, :cycles)
7
+
8
+ def self.call(definitions)
9
+ new(definitions).analyze
10
+ end
11
+
12
+ def initialize(definitions)
13
+ @definitions_by_name = definitions.to_h { |d| [d.name, d] }
14
+ @graph = build_graph
15
+ end
16
+
17
+ def analyze
18
+ cycles = detect_cycles
19
+ sorted = topological_sort(cycles)
20
+ Result.new(sorted:, cycles:)
21
+ end
22
+
23
+ private
24
+
25
+ def build_graph
26
+ @definitions_by_name.each_with_object({}) do |(name, definition), graph|
27
+ graph[name] = collect_dependencies(definition)
28
+ end
29
+ end
30
+
31
+ def collect_dependencies(definition)
32
+ deps = Set.new
33
+
34
+ definition.attributes.each_value do |attr|
35
+ dep = attr.dependency_name
36
+ deps << dep if dep && @definitions_by_name.key?(dep)
37
+ end
38
+
39
+ definition.variants.each do |variant|
40
+ variant.attributes.each_value do |attr|
41
+ dep = attr.dependency_name
42
+ deps << dep if dep && @definitions_by_name.key?(dep)
43
+ end
44
+ end
45
+
46
+ deps
47
+ end
48
+
49
+ # Tarjan's SCC algorithm — detects strongly connected components
50
+ def detect_cycles
51
+ @index_counter = 0
52
+ @stack = []
53
+ @indices = {}
54
+ @lowlinks = {}
55
+ @on_stack = Set.new
56
+ cycles = Set.new
57
+
58
+ @graph.each_key do |name|
59
+ tarjan(name, cycles) unless @indices.key?(name)
60
+ end
61
+
62
+ cycles
63
+ end
64
+
65
+ def tarjan(name, cycles)
66
+ @indices[name] = @index_counter
67
+ @lowlinks[name] = @index_counter
68
+ @index_counter += 1
69
+ @stack.push(name)
70
+ @on_stack << name
71
+
72
+ @graph[name].each do |dep|
73
+ if !@indices.key?(dep)
74
+ tarjan(dep, cycles)
75
+ @lowlinks[name] = [@lowlinks[name], @lowlinks[dep]].min
76
+ elsif @on_stack.include?(dep)
77
+ @lowlinks[name] = [@lowlinks[name], @indices[dep]].min
78
+ end
79
+ end
80
+
81
+ return unless @lowlinks[name] == @indices[name]
82
+
83
+ component = []
84
+ loop do
85
+ node = @stack.pop
86
+ @on_stack.delete(node)
87
+ component << node
88
+ break if node == name
89
+ end
90
+
91
+ return unless component.size > 1 || @graph[name].include?(name)
92
+
93
+ component.each { |n| cycles << n }
94
+ end
95
+
96
+ # Kahn's algorithm with reversed edges — dependencies come before dependents
97
+ def topological_sort(cycles)
98
+ in_degree = Hash.new(0)
99
+ reverse_adjacency = {}
100
+
101
+ @graph.each_key do |name|
102
+ reverse_adjacency[name] ||= []
103
+ end
104
+
105
+ @graph.each do |name, deps|
106
+ deps.each do |dep|
107
+ next if cycles.include?(name) && cycles.include?(dep)
108
+
109
+ reverse_adjacency[dep] ||= []
110
+ reverse_adjacency[dep] << name
111
+ in_degree[name] += 1
112
+ end
113
+ end
114
+
115
+ queue = @graph.keys.select { |n| in_degree[n].zero? }
116
+ sorted = []
117
+
118
+ while (name = queue.shift)
119
+ sorted << name
120
+ reverse_adjacency[name]&.each do |dependent|
121
+ in_degree[dependent] -= 1
122
+ queue << dependent if in_degree[dependent].zero?
123
+ end
124
+ end
125
+
126
+ remaining = @graph.keys - sorted
127
+ sorted.concat(remaining)
128
+
129
+ sorted.filter_map { |name| @definitions_by_name[name] }
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ module Export
5
+ class TypeScriptMapper
6
+ PRIMITIVE_MAP = {
7
+ string: 'string',
8
+ integer: 'number',
9
+ decimal: 'number',
10
+ number: 'number',
11
+ boolean: 'boolean',
12
+ datetime: 'string',
13
+ date: 'string',
14
+ uuid: 'string',
15
+ binary: 'string'
16
+ }.freeze
17
+
18
+ def initialize(key_format: :keep)
19
+ @key_format = key_format
20
+ end
21
+
22
+ def map_definition(definition)
23
+ case definition.kind
24
+ when :object then map_object(definition)
25
+ when :enum then map_enum(definition)
26
+ when :union then map_union(definition)
27
+ end
28
+ end
29
+
30
+ def map_definitions(definitions, cycles: Set.new)
31
+ definitions.map { |definition| map_definition(definition) }.join("\n\n")
32
+ end
33
+
34
+ def map_contract(contract)
35
+ params_output = map_contract_params(contract)
36
+ descriptor_output = map_contract_descriptor(contract)
37
+ [params_output, descriptor_output].compact.reject(&:empty?).join("\n\n")
38
+ end
39
+
40
+ def map_contracts(contracts)
41
+ contracts.map { |contract| map_contract(contract) }.join("\n\n")
42
+ end
43
+
44
+ private
45
+
46
+ def map_object(definition)
47
+ properties = definition.attributes.values.map { |attr| map_property(attr) }
48
+ "export interface #{pascal_case(definition.name)} {\n#{properties.join("\n")}\n}"
49
+ end
50
+
51
+ def map_enum(definition)
52
+ values = definition.values.map { |value| "'#{value}'" }.join(' | ')
53
+ "export type #{pascal_case(definition.name)} = #{values};"
54
+ end
55
+
56
+ def map_union(definition)
57
+ variants = definition.variants.map { |variant| map_variant(variant, definition.discriminator) }
58
+ "export type #{pascal_case(definition.name)} =\n#{variants.join("\n")};"
59
+ end
60
+
61
+ def map_variant(variant, discriminator)
62
+ discriminator_key = transform_key(discriminator)
63
+ properties = variant.attributes.values.map do |attr|
64
+ "#{transform_key(attr.name)}: #{map_type(attr)}"
65
+ end
66
+ all_properties = ["#{discriminator_key}: '#{variant.tag}'"] + properties
67
+ " | { #{all_properties.join('; ')} }"
68
+ end
69
+
70
+ def map_property(attribute)
71
+ key = transform_key(attribute.name)
72
+ optional_marker = attribute.optional? ? '?' : ''
73
+ type_string = map_type(attribute)
74
+ type_string = [type_string, 'null'].sort.join(' | ') if attribute.nullable?
75
+ " #{key}#{optional_marker}: #{type_string};"
76
+ end
77
+
78
+ def map_type(attribute)
79
+ if attribute.enum
80
+ attribute.enum.map { |v| "'#{v}'" }.join(' | ')
81
+ elsif attribute.reference?
82
+ pascal_case(attribute.reference_name)
83
+ elsif attribute.array?
84
+ "#{pascal_case(attribute.of)}[]"
85
+ else
86
+ resolve_primitive_type(attribute.type)
87
+ end
88
+ end
89
+
90
+ def transform_key(key)
91
+ key_string = key.to_s
92
+ case @key_format
93
+ when :camel then camel_case(key_string)
94
+ when :pascal then pascal_case_string(key_string)
95
+ else key_string
96
+ end
97
+ end
98
+
99
+ def map_contract_params(contract)
100
+ contract.actions.values.map do |action|
101
+ params_name = :"#{action.name}_#{contract.name}_params"
102
+ renamed = Definition.new(name: params_name, kind: :object)
103
+ action.params.attributes.each { |key, attr| renamed.attributes[key] = attr }
104
+ map_object(renamed)
105
+ end.join("\n\n")
106
+ end
107
+
108
+ def map_contract_descriptor(contract)
109
+ name = pascal_case(contract.name)
110
+ return "export interface #{name}Contract {}" if contract.actions.empty?
111
+
112
+ entries = contract.actions.values.map do |action|
113
+ params_type = "#{pascal_case(action.name)}#{name}Params"
114
+ parts = ["method: '#{action.http_method.to_s.upcase}'", "path: '#{action.path}'", "params: #{params_type}"]
115
+ parts << "response: #{pascal_case(action.response_type)}" if action.response_type
116
+ " #{action.name}: { #{parts.join('; ')} };"
117
+ end
118
+
119
+ "export interface #{name}Contract {\n#{entries.join("\n")}\n}"
120
+ end
121
+
122
+ def resolve_primitive_type(type)
123
+ PRIMITIVE_MAP.fetch(type) do
124
+ scalar = ScalarRegistry.global.find(type)
125
+ scalar ? PRIMITIVE_MAP.fetch(scalar.base, 'unknown') : 'unknown'
126
+ end
127
+ end
128
+
129
+ def pascal_case(name)
130
+ name.to_s.split('_').map(&:capitalize).join
131
+ end
132
+
133
+ def pascal_case_string(string)
134
+ string.split('_').map(&:capitalize).join
135
+ end
136
+
137
+ def camel_case(string)
138
+ parts = string.split('_')
139
+ parts.first + parts[1..].map(&:capitalize).join
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Zodra
6
+ module Export
7
+ class Writer
8
+ HEADER = '// Auto-generated by Zodra — DO NOT EDIT'
9
+
10
+ FORMATS = {
11
+ zod: 'schemas.ts',
12
+ typescript: 'types.ts'
13
+ }.freeze
14
+
15
+ def initialize(configuration)
16
+ @configuration = configuration
17
+ end
18
+
19
+ def write(format)
20
+ content = Export.generate(format,
21
+ key_format: @configuration.key_format,
22
+ zod_import: @configuration.zod_import)
23
+
24
+ write_file(FORMATS.fetch(format), content)
25
+ end
26
+
27
+ def write_contracts
28
+ content = Export.generate_contracts
29
+ return if content.empty?
30
+
31
+ write_file('contracts.ts', content)
32
+ end
33
+
34
+ def write_all
35
+ paths = FORMATS.keys.map { |format| write(format) }
36
+ contracts_path = write_contracts
37
+ paths << contracts_path if contracts_path
38
+ paths
39
+ end
40
+
41
+ private
42
+
43
+ def write_file(filename, content)
44
+ FileUtils.mkdir_p(@configuration.output_path)
45
+ filepath = File.join(@configuration.output_path, filename)
46
+ File.write(filepath, "#{HEADER}\n#{content}\n")
47
+ filepath
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ module Export
5
+ class ZodMapper
6
+ PRIMITIVE_MAP = {
7
+ string: 'z.string()',
8
+ integer: 'z.number().int()',
9
+ decimal: 'z.number()',
10
+ number: 'z.number()',
11
+ boolean: 'z.boolean()',
12
+ datetime: 'z.iso.datetime()',
13
+ date: 'z.iso.date()',
14
+ uuid: 'z.uuid()',
15
+ binary: 'z.string()'
16
+ }.freeze
17
+
18
+ def initialize(key_format: :keep)
19
+ @key_format = key_format
20
+ end
21
+
22
+ def map_definition(definition, lazy: false)
23
+ case definition.kind
24
+ when :object then map_object(definition, lazy:)
25
+ when :enum then map_enum(definition)
26
+ when :union then map_union(definition, lazy:)
27
+ end
28
+ end
29
+
30
+ def map_definitions(definitions, cycles: Set.new)
31
+ definitions.map { |definition| map_definition(definition, lazy: cycles.include?(definition.name)) }.join("\n\n")
32
+ end
33
+
34
+ def map_contract(contract)
35
+ params_output = map_contract_params(contract)
36
+ error_types_output = map_contract_error_types(contract)
37
+ descriptor_output = map_contract_descriptor(contract)
38
+ [params_output, error_types_output, descriptor_output].compact.reject(&:empty?).join("\n\n")
39
+ end
40
+
41
+ def map_contracts(contracts)
42
+ contracts.map { |contract| map_contract(contract) }.join("\n\n")
43
+ end
44
+
45
+ private
46
+
47
+ def map_object(definition, lazy: false)
48
+ name = pascal_case(definition.name)
49
+ properties = definition.attributes.values.map { |attr| " #{map_property(attr)}," }
50
+ body = "z.object({\n#{properties.join("\n")}\n})"
51
+
52
+ if lazy
53
+ "export const #{name}Schema: z.ZodType<#{name}> = z.lazy(() => #{body});"
54
+ else
55
+ "export const #{name}Schema = #{body};"
56
+ end
57
+ end
58
+
59
+ def map_enum(definition)
60
+ values = definition.values.map { |value| "'#{value}'" }.join(', ')
61
+ "export const #{pascal_case(definition.name)}Schema = z.enum([#{values}]);"
62
+ end
63
+
64
+ def map_union(definition, lazy: false)
65
+ name = pascal_case(definition.name)
66
+ discriminator_key = transform_key(definition.discriminator)
67
+ variants = definition.variants.map { |variant| map_variant(variant, discriminator_key) }
68
+ indent = ' '
69
+ body = "z.discriminatedUnion('#{discriminator_key}', [\n" \
70
+ "#{variants.map { |v| "#{indent}#{v}" }.join(",\n")},\n])"
71
+
72
+ if lazy
73
+ "export const #{name}Schema: z.ZodType<#{name}> = z.lazy(() => #{body});"
74
+ else
75
+ "export const #{name}Schema = #{body};"
76
+ end
77
+ end
78
+
79
+ def map_variant(variant, discriminator_key)
80
+ properties = variant.attributes.values.map { |attr| "#{transform_key(attr.name)}: #{map_zod_type(attr)}" }
81
+ all_properties = ["#{discriminator_key}: z.literal('#{variant.tag}')"] + properties
82
+ "z.object({ #{all_properties.join(', ')} })"
83
+ end
84
+
85
+ def map_property(attribute)
86
+ key = transform_key(attribute.name)
87
+ "#{key}: #{map_zod_type(attribute)}"
88
+ end
89
+
90
+ def map_zod_type(attribute)
91
+ base = resolve_base_type(attribute)
92
+ base = apply_constraints(base, attribute)
93
+ apply_modifiers(base, attribute)
94
+ end
95
+
96
+ def resolve_base_type(attribute)
97
+ if attribute.enum
98
+ values = attribute.enum.map { |v| "'#{v}'" }.join(', ')
99
+ "z.enum([#{values}])"
100
+ elsif attribute.reference?
101
+ "#{pascal_case(attribute.reference_name)}Schema"
102
+ elsif attribute.array?
103
+ "z.array(#{pascal_case(attribute.of)}Schema)"
104
+ else
105
+ resolve_primitive_type(attribute.type)
106
+ end
107
+ end
108
+
109
+ def apply_constraints(base, attribute)
110
+ base = "#{base}.min(#{attribute.min})" if attribute.min
111
+ base = "#{base}.max(#{attribute.max})" if attribute.max
112
+ base
113
+ end
114
+
115
+ def apply_modifiers(base, attribute)
116
+ base = "#{base}.default(#{format_default(attribute.default)})" unless attribute.default.nil?
117
+ base = "#{base}.nullable()" if attribute.nullable?
118
+ base = "#{base}.optional()" if attribute.optional?
119
+ base
120
+ end
121
+
122
+ def format_default(value)
123
+ case value
124
+ when String then "'#{value}'"
125
+ when Symbol then "'#{value}'"
126
+ else value
127
+ end
128
+ end
129
+
130
+ def transform_key(key)
131
+ key_string = key.to_s
132
+ case @key_format
133
+ when :camel then camel_case(key_string)
134
+ when :pascal then pascal_case(key_string)
135
+ else key_string
136
+ end
137
+ end
138
+
139
+ def map_contract_params(contract)
140
+ contract.actions.values.map do |action|
141
+ params_name = :"#{action.name}_#{contract.name}_params"
142
+ renamed = Definition.new(name: params_name, kind: :object)
143
+ action.params.attributes.each { |key, attr| renamed.attributes[key] = attr }
144
+ map_object(renamed)
145
+ end.join("\n\n")
146
+ end
147
+
148
+ def map_contract_error_types(contract)
149
+ parts = contract.actions.values.filter_map do |action|
150
+ next if action.errors.empty?
151
+
152
+ action_name = pascal_case(action.name)
153
+ contract_name = pascal_case(contract.name)
154
+
155
+ codes = action.errors.values.map { |e| "'#{e[:code]}'" }.join(' | ')
156
+ "export type #{action_name}#{contract_name}BusinessError = { code: #{codes}; message: string };"
157
+ end
158
+
159
+ parts.join("\n\n")
160
+ end
161
+
162
+ def map_contract_descriptor(contract)
163
+ name = pascal_case(contract.name)
164
+ return "export const #{name}Contract = {} as const;" if contract.actions.empty?
165
+
166
+ entries = contract.actions.values.map do |action|
167
+ params_schema = "#{pascal_case(action.name)}#{name}ParamsSchema"
168
+ parts = ["method: '#{action.http_method.to_s.upcase}' as const", "path: '#{action.path}' as const",
169
+ "params: #{params_schema}"]
170
+ parts << "response: #{pascal_case(action.response_type)}Schema" if action.response_type
171
+ if action.errors.any?
172
+ error_entries = action.errors.values.map do |e|
173
+ "{ code: '#{e[:code]}' as const, status: #{e[:status]} as const }"
174
+ end
175
+ parts << "errors: [#{error_entries.join(', ')}] as const"
176
+ end
177
+ " #{action.name}: { #{parts.join(', ')} }"
178
+ end
179
+
180
+ "export const #{name}Contract = {\n#{entries.join(",\n")},\n} as const;"
181
+ end
182
+
183
+ def resolve_primitive_type(type)
184
+ PRIMITIVE_MAP.fetch(type) do
185
+ scalar = ScalarRegistry.global.find(type)
186
+ scalar ? PRIMITIVE_MAP.fetch(scalar.base, 'z.unknown()') : 'z.unknown()'
187
+ end
188
+ end
189
+
190
+ def pascal_case(name)
191
+ name.to_s.split('_').map(&:capitalize).join
192
+ end
193
+
194
+ def camel_case(string)
195
+ parts = string.split('_')
196
+ parts.first + parts[1..].map(&:capitalize).join
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ module Export
5
+ MAPPERS = {
6
+ typescript: TypeScriptMapper,
7
+ zod: ZodMapper
8
+ }.freeze
9
+
10
+ def self.generate(format, key_format: :camel, zod_import: 'zod')
11
+ mapper_class = MAPPERS.fetch(format) do
12
+ raise ConfigurationError, "Unknown export format: #{format}. Available: #{MAPPERS.keys.join(', ')}"
13
+ end
14
+
15
+ mapper = mapper_class.new(key_format:)
16
+ contracts = ContractRegistry.global.to_a
17
+
18
+ definitions = SurfaceResolver.call(TypeRegistry.global.to_a, contracts)
19
+ analysis = TypeAnalysis.call(definitions)
20
+
21
+ parts = []
22
+ parts << "import { z } from '#{zod_import}';" if format == :zod
23
+ parts << mapper.map_definitions(analysis.sorted, cycles: analysis.cycles)
24
+ parts << mapper.map_contracts(contracts) unless contracts.empty?
25
+ parts.reject(&:empty?).join("\n\n")
26
+ end
27
+
28
+ def self.generate_contracts
29
+ contracts = ContractRegistry.global.to_a
30
+ api_definitions = ApiRegistry.global.to_a
31
+
32
+ ContractMapper.new(api_definitions, contracts).generate
33
+ end
34
+ end
35
+ end