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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ class TypeDeriver
5
+ def initialize(source_definition, pick: nil, omit: nil, partial: false)
6
+ raise ArgumentError, 'Cannot use both :pick and :omit' if pick && omit
7
+ raise ArgumentError, 'Source must be an object type' unless source_definition.object?
8
+
9
+ @source = source_definition
10
+ @pick = pick&.map(&:to_sym)
11
+ @omit = omit&.map(&:to_sym)
12
+ @partial = partial
13
+ end
14
+
15
+ def apply(target_definition)
16
+ selected_attributes.each do |name, attribute|
17
+ copy_attribute(target_definition, name, attribute, optional: @partial && !attribute.optional?)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def copy_attribute(target, name, attribute, optional: false)
24
+ target.add_attribute(name,
25
+ type: attribute.type,
26
+ optional: optional || attribute.optional?,
27
+ nullable: attribute.nullable?,
28
+ format: attribute.format,
29
+ default: attribute.default,
30
+ min: attribute.min,
31
+ max: attribute.max,
32
+ enum: attribute.enum,
33
+ of: attribute.of,
34
+ reference_name: attribute.reference_name)
35
+ end
36
+
37
+ def selected_attributes
38
+ attributes = @source.attributes
39
+
40
+ if @pick
41
+ unknown = @pick - attributes.keys
42
+ raise ArgumentError, "Unknown attributes #{unknown.inspect} for type :#{@source.name}" if unknown.any?
43
+
44
+ attributes.slice(*@pick)
45
+ elsif @omit
46
+ unknown = @omit - attributes.keys
47
+ raise ArgumentError, "Unknown attributes #{unknown.inspect} for type :#{@source.name}" if unknown.any?
48
+
49
+ attributes.except(*@omit)
50
+ else
51
+ attributes
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ class TypeRegistry
5
+ include Enumerable
6
+
7
+ def self.global
8
+ @global ||= new
9
+ end
10
+
11
+ def initialize
12
+ @store = {}
13
+ end
14
+
15
+ def register(name, kind:, **)
16
+ name = name.to_sym
17
+ raise DuplicateTypeError, "Type :#{name} is already registered" if @store.key?(name)
18
+
19
+ @store[name] = Definition.new(name:, kind:, **)
20
+ end
21
+
22
+ def find(name)
23
+ @store[name.to_sym]
24
+ end
25
+
26
+ def find!(name)
27
+ @store.fetch(name.to_sym) { raise KeyError, "Type :#{name} is not registered" }
28
+ end
29
+
30
+ def exists?(name)
31
+ @store.key?(name.to_sym)
32
+ end
33
+
34
+ def each(&)
35
+ @store.each_value(&)
36
+ end
37
+
38
+ def clear!
39
+ @store.clear
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ class UnionBuilder
5
+ def initialize(definition)
6
+ @definition = definition
7
+ end
8
+
9
+ def variant(tag, &block)
10
+ variant_builder = VariantBuilder.new
11
+ variant_builder.instance_eval(&block) if block
12
+ @definition.add_variant(tag, attributes: variant_builder.attributes)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ class Variant
5
+ attr_reader :tag, :attributes
6
+
7
+ def initialize(tag:, attributes: {})
8
+ @tag = tag.to_sym
9
+ @attributes = attributes
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ class VariantBuilder
5
+ attr_reader :attributes
6
+
7
+ PRIMITIVES = TypeBuilder::PRIMITIVES
8
+
9
+ def initialize
10
+ @attributes = {}
11
+ end
12
+
13
+ PRIMITIVES.each do |primitive_type|
14
+ define_method(primitive_type) do |name, **options|
15
+ @attributes[name.to_sym] = Attribute.new(name:, type: primitive_type, **options)
16
+ end
17
+
18
+ define_method(:"#{primitive_type}?") do |name, **options|
19
+ @attributes[name.to_sym] = Attribute.new(name:, type: primitive_type, optional: true, **options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zodra
4
+ VERSION = '0.1.0'
5
+ end
data/lib/zodra.rb ADDED
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ require 'active_support/core_ext/string/inflections'
5
+
6
+ module Zodra
7
+ class Error < StandardError; end
8
+ class DuplicateTypeError < Error; end
9
+ class ConfigurationError < Error; end
10
+
11
+ class ParamsError < Error
12
+ attr_reader :errors
13
+
14
+ def initialize(errors)
15
+ @errors = errors
16
+ super('Params validation failed')
17
+ end
18
+ end
19
+
20
+ class << self
21
+ def logger
22
+ @logger || (defined?(Rails) ? Rails.logger : nil)
23
+ end
24
+
25
+ attr_writer :logger
26
+
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ def configure
32
+ yield configuration
33
+ end
34
+
35
+ def type(name, from: nil, pick: nil, omit: nil, partial: false, &block)
36
+ definition = TypeRegistry.global.register(name, kind: :object)
37
+
38
+ if from
39
+ source = TypeRegistry.global.find!(from)
40
+ TypeDeriver.new(source, pick:, omit:, partial:).apply(definition)
41
+ end
42
+
43
+ TypeBuilder.new(definition).instance_eval(&block) if block
44
+ definition
45
+ end
46
+
47
+ def enum(name, values:)
48
+ TypeRegistry.global.register(name, kind: :enum, values:)
49
+ end
50
+
51
+ def union(name, discriminator:, &block)
52
+ definition = TypeRegistry.global.register(name, kind: :union, discriminator:)
53
+ UnionBuilder.new(definition).instance_eval(&block) if block
54
+ definition
55
+ end
56
+
57
+ def contract(name, &block)
58
+ contract = ContractRegistry.global.register(name)
59
+ ContractBuilder.new(contract).instance_eval(&block) if block
60
+ contract
61
+ end
62
+
63
+ def api(base_path, &block)
64
+ api_definition = ApiRegistry.global.register(base_path)
65
+ ApiBuilder.new(api_definition).instance_eval(&block) if block
66
+ api_definition
67
+ end
68
+
69
+ def scalar(name, base:, &coercer)
70
+ ScalarRegistry.global.register(name, base:, coercer:)
71
+ end
72
+
73
+ def load_definitions!
74
+ return unless defined?(Rails)
75
+
76
+ ScalarRegistry.global.clear!
77
+ TypeRegistry.global.clear!
78
+ ContractRegistry.global.clear!
79
+ ApiRegistry.global.clear!
80
+
81
+ load_definition_dir(Rails.root.join('app/types'))
82
+ load_definition_dir(Rails.root.join('app/contracts'))
83
+ load_definition_dir(Rails.root.join('config/apis'))
84
+
85
+ resolve_routes!
86
+ end
87
+
88
+ def resolve_routes!
89
+ ApiRegistry.global.each do |api_definition|
90
+ api_definition.resources.each do |resource|
91
+ resolve_resource_routes(resource, api_definition.base_path)
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def load_definition_dir(path)
99
+ Dir[path.join('**/*.rb')].each { |file| load(file) }
100
+ end
101
+
102
+ def resolve_resource_routes(resource, base_path, parent_param: nil)
103
+ segment = resource.name.to_s
104
+ resource_path = parent_param ? "#{base_path}/#{parent_param}/#{segment}" : "#{base_path}/#{segment}"
105
+
106
+ contract = ContractRegistry.global.find(resource.contract_name)
107
+
108
+ if contract
109
+ resource.crud_actions.each do |action_name|
110
+ action = contract.find_action(action_name)
111
+ next unless action
112
+
113
+ crud = Resource::CRUD_ACTIONS[action_name]
114
+ action.http_method = crud[:http_method]
115
+ action.path = crud[:member] ? "#{resource_path}/:id" : resource_path
116
+ end
117
+
118
+ resource.custom_actions.each do |custom|
119
+ action = contract.find_action(custom[:name])
120
+ next unless action
121
+
122
+ action.http_method = custom[:http_method]
123
+ action.path = custom[:member] ? "#{resource_path}/:id/#{custom[:name]}" : "#{resource_path}/#{custom[:name]}"
124
+ end
125
+ end
126
+
127
+ resource.children.each do |child|
128
+ child_parent_param = resource.singular? ? nil : ":#{resource.name.to_s.singularize}_id"
129
+ resolve_resource_routes(child, resource_path, parent_param: child_parent_param)
130
+ end
131
+ end
132
+
133
+ def setup_autoload
134
+ @loader = Zeitwerk::Loader.for_gem.tap do |loader|
135
+ loader.inflector.inflect('dsl' => 'DSL')
136
+ loader.ignore("#{__dir__}/generators")
137
+ loader.ignore("#{__dir__}/zodra/tasks")
138
+ loader.ignore("#{__dir__}/zodra/railtie.rb")
139
+ loader.setup
140
+ end
141
+ end
142
+ end
143
+
144
+ setup_autoload
145
+
146
+ require 'zodra/railtie' if defined?(Rails::Railtie)
147
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zodra
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Zodra Contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ description: Define types once in Ruby DSL, generate TypeScript interfaces and Zod
41
+ schemas for runtime validation on both ends.
42
+ executables: []
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - CHANGELOG.md
47
+ - lib/generators/zodra/install_generator.rb
48
+ - lib/generators/zodra/templates/initializer.rb.tt
49
+ - lib/zodra.rb
50
+ - lib/zodra/action.rb
51
+ - lib/zodra/action_builder.rb
52
+ - lib/zodra/api_builder.rb
53
+ - lib/zodra/api_definition.rb
54
+ - lib/zodra/api_registry.rb
55
+ - lib/zodra/attribute.rb
56
+ - lib/zodra/configuration.rb
57
+ - lib/zodra/contract.rb
58
+ - lib/zodra/contract_builder.rb
59
+ - lib/zodra/contract_registry.rb
60
+ - lib/zodra/controller.rb
61
+ - lib/zodra/definition.rb
62
+ - lib/zodra/export.rb
63
+ - lib/zodra/export/contract_mapper.rb
64
+ - lib/zodra/export/surface_resolver.rb
65
+ - lib/zodra/export/type_analysis.rb
66
+ - lib/zodra/export/type_script_mapper.rb
67
+ - lib/zodra/export/writer.rb
68
+ - lib/zodra/export/zod_mapper.rb
69
+ - lib/zodra/params_coercer.rb
70
+ - lib/zodra/params_parser.rb
71
+ - lib/zodra/params_validator.rb
72
+ - lib/zodra/railtie.rb
73
+ - lib/zodra/resource.rb
74
+ - lib/zodra/resource_builder.rb
75
+ - lib/zodra/response_serializer.rb
76
+ - lib/zodra/route_helper.rb
77
+ - lib/zodra/router.rb
78
+ - lib/zodra/scalar_registry.rb
79
+ - lib/zodra/scalar_type.rb
80
+ - lib/zodra/tasks/zodra.rake
81
+ - lib/zodra/type_builder.rb
82
+ - lib/zodra/type_deriver.rb
83
+ - lib/zodra/type_registry.rb
84
+ - lib/zodra/union_builder.rb
85
+ - lib/zodra/variant.rb
86
+ - lib/zodra/variant_builder.rb
87
+ - lib/zodra/version.rb
88
+ homepage: https://github.com/supostat/zodra
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ rubygems_mfa_required: 'true'
93
+ homepage_uri: https://github.com/supostat/zodra
94
+ source_code_uri: https://github.com/supostat/zodra
95
+ changelog_uri: https://github.com/supostat/zodra/blob/main/gem/CHANGELOG.md
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3.2'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.6.9
111
+ specification_version: 4
112
+ summary: 'End-to-end type system for Rails: DSL → TypeScript + Zod'
113
+ test_files: []