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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/lib/generators/zodra/install_generator.rb +20 -0
- data/lib/generators/zodra/templates/initializer.rb.tt +12 -0
- data/lib/zodra/action.rb +43 -0
- data/lib/zodra/action_builder.rb +38 -0
- data/lib/zodra/api_builder.rb +23 -0
- data/lib/zodra/api_definition.rb +24 -0
- data/lib/zodra/api_registry.rb +33 -0
- data/lib/zodra/attribute.rb +46 -0
- data/lib/zodra/configuration.rb +18 -0
- data/lib/zodra/contract.rb +33 -0
- data/lib/zodra/contract_builder.rb +37 -0
- data/lib/zodra/contract_registry.rb +42 -0
- data/lib/zodra/controller.rb +141 -0
- data/lib/zodra/definition.rb +36 -0
- data/lib/zodra/export/contract_mapper.rb +76 -0
- data/lib/zodra/export/surface_resolver.rb +76 -0
- data/lib/zodra/export/type_analysis.rb +133 -0
- data/lib/zodra/export/type_script_mapper.rb +143 -0
- data/lib/zodra/export/writer.rb +51 -0
- data/lib/zodra/export/zod_mapper.rb +200 -0
- data/lib/zodra/export.rb +35 -0
- data/lib/zodra/params_coercer.rb +100 -0
- data/lib/zodra/params_parser.rb +90 -0
- data/lib/zodra/params_validator.rb +74 -0
- data/lib/zodra/railtie.rb +13 -0
- data/lib/zodra/resource.rb +51 -0
- data/lib/zodra/resource_builder.rb +57 -0
- data/lib/zodra/response_serializer.rb +63 -0
- data/lib/zodra/route_helper.rb +9 -0
- data/lib/zodra/router.rb +55 -0
- data/lib/zodra/scalar_registry.rb +32 -0
- data/lib/zodra/scalar_type.rb +13 -0
- data/lib/zodra/tasks/zodra.rake +41 -0
- data/lib/zodra/type_builder.rb +57 -0
- data/lib/zodra/type_deriver.rb +55 -0
- data/lib/zodra/type_registry.rb +42 -0
- data/lib/zodra/union_builder.rb +15 -0
- data/lib/zodra/variant.rb +12 -0
- data/lib/zodra/variant_builder.rb +23 -0
- data/lib/zodra/version.rb +5 -0
- data/lib/zodra.rb +147 -0
- 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,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
|
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: []
|