typelizer 0.11.0 → 0.13.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 +4 -4
- data/CHANGELOG.md +45 -1
- data/README.md +32 -680
- data/lib/tasks/generate.rake +50 -15
- data/lib/typelizer/config.rb +10 -3
- data/lib/typelizer/configuration.rb +4 -0
- data/lib/typelizer/dsl.rb +14 -5
- data/lib/typelizer/generator.rb +2 -2
- data/lib/typelizer/interface.rb +28 -21
- data/lib/typelizer/listen.rb +26 -7
- data/lib/typelizer/middleware.rb +52 -0
- data/lib/typelizer/model_plugins/active_record.rb +3 -0
- data/lib/typelizer/openapi.rb +11 -17
- data/lib/typelizer/property.rb +40 -27
- data/lib/typelizer/railtie.rb +12 -9
- data/lib/typelizer/route_config.rb +50 -0
- data/lib/typelizer/route_generator.rb +149 -0
- data/lib/typelizer/route_writer.rb +171 -0
- data/lib/typelizer/serializer_plugins/alba/trait_interface.rb +2 -1
- data/lib/typelizer/serializer_plugins/alba.rb +8 -14
- data/lib/typelizer/shape.rb +39 -0
- data/lib/typelizer/templates/enums.ts.erb +3 -0
- data/lib/typelizer/templates/index.ts.erb +2 -1
- data/lib/typelizer/templates/route_controller.erb +36 -0
- data/lib/typelizer/templates/route_index.erb +14 -0
- data/lib/typelizer/templates/route_runtime.js +97 -0
- data/lib/typelizer/templates/route_runtime.ts +109 -0
- data/lib/typelizer/type_inference.rb +15 -7
- data/lib/typelizer/type_parser.rb +28 -0
- data/lib/typelizer/version.rb +1 -1
- data/lib/typelizer/writer.rb +4 -4
- data/lib/typelizer.rb +16 -2
- metadata +13 -4
- /data/lib/typelizer/templates/{fingerprint.ts.erb → fingerprint.erb} +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
RouteConfig = Struct.new(
|
|
5
|
+
:enabled,
|
|
6
|
+
:output_dir,
|
|
7
|
+
:include,
|
|
8
|
+
:exclude,
|
|
9
|
+
:camel_case,
|
|
10
|
+
:format,
|
|
11
|
+
keyword_init: true
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
class RouteConfig
|
|
15
|
+
def self.defaults
|
|
16
|
+
{
|
|
17
|
+
enabled: false,
|
|
18
|
+
output_dir: nil,
|
|
19
|
+
include: nil,
|
|
20
|
+
exclude: nil,
|
|
21
|
+
camel_case: true,
|
|
22
|
+
format: :ts
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ts?
|
|
27
|
+
format != :js
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def js?
|
|
31
|
+
format == :js
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def file_ext
|
|
35
|
+
js? ? "js" : "ts"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.build(**overrides)
|
|
39
|
+
new(**defaults.merge(overrides))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def output_dir
|
|
43
|
+
self[:output_dir] || begin
|
|
44
|
+
root_path = defined?(Rails) ? Rails.root : Pathname.pwd
|
|
45
|
+
js_root = defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/javascript"
|
|
46
|
+
root_path.join(js_root, "routes")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Typelizer
|
|
6
|
+
class RouteGenerator
|
|
7
|
+
FORMAT_SUFFIX = /\(\.:format\)$/
|
|
8
|
+
|
|
9
|
+
def self.call(**args)
|
|
10
|
+
new.call(**args)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(force: false, skip_check: false)
|
|
14
|
+
return [] if !skip_check && !(Typelizer.enabled? && config.enabled)
|
|
15
|
+
|
|
16
|
+
routes = collect_routes
|
|
17
|
+
return [] if routes.empty?
|
|
18
|
+
|
|
19
|
+
RouteWriter.new(config).call(routes, force: force)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def config
|
|
25
|
+
Typelizer.configuration.routes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def collect_routes
|
|
29
|
+
return [] unless defined?(Rails) && Rails.application
|
|
30
|
+
|
|
31
|
+
# Rails 8+ lazily loads routes
|
|
32
|
+
if Rails.application.respond_to?(:reload_routes_unless_loaded)
|
|
33
|
+
Rails.application.reload_routes_unless_loaded
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
name_by_action, name_by_path = build_name_lookups(Rails.application.routes.named_routes)
|
|
37
|
+
|
|
38
|
+
routes = Rails.application.routes.routes.flat_map do |route|
|
|
39
|
+
app = route.app.app
|
|
40
|
+
if app.is_a?(Class) && app < Rails::Engine
|
|
41
|
+
collect_engine_routes(route, app) || []
|
|
42
|
+
else
|
|
43
|
+
build_route_info(route, name_by_action, name_by_path)
|
|
44
|
+
end
|
|
45
|
+
end.compact
|
|
46
|
+
|
|
47
|
+
# Skip PUT where PATCH exists for the same route (Rails adds both for `resources`)
|
|
48
|
+
patch_keys = routes.select { |r| r[:verb] == "patch" }
|
|
49
|
+
.map { |r| [r[:controller], r[:action], r[:path]] }.to_set
|
|
50
|
+
routes.reject! { |r| r[:verb] == "put" && patch_keys.include?([r[:controller], r[:action], r[:path]]) }
|
|
51
|
+
|
|
52
|
+
if config.include
|
|
53
|
+
patterns = Array(config.include)
|
|
54
|
+
routes = routes.select { |r| patterns.any? { |p| r[:path].match?(p) } }
|
|
55
|
+
end
|
|
56
|
+
if config.exclude
|
|
57
|
+
patterns = Array(config.exclude)
|
|
58
|
+
routes = routes.reject { |r| patterns.any? { |p| r[:path].match?(p) } }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
routes
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_name_lookups(named_routes, path_prefix: "", name_prefix: "")
|
|
65
|
+
name_by_action = {}
|
|
66
|
+
name_by_path = {}
|
|
67
|
+
|
|
68
|
+
named_routes.each do |name, route|
|
|
69
|
+
controller = route.requirements[:controller]
|
|
70
|
+
action = route.requirements[:action]
|
|
71
|
+
next unless controller && action
|
|
72
|
+
|
|
73
|
+
path = path_prefix + strip_format(route.path.spec.to_s)
|
|
74
|
+
prefixed_name = "#{name_prefix}#{name}"
|
|
75
|
+
name_by_action[[controller, action]] = prefixed_name
|
|
76
|
+
name_by_path[[controller, path]] = prefixed_name
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
[name_by_action, name_by_path]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def strip_format(path)
|
|
83
|
+
path.sub(FORMAT_SUFFIX, "")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_route_info(route, name_by_action, name_by_path)
|
|
87
|
+
controller = route.requirements[:controller]
|
|
88
|
+
action = route.requirements[:action]
|
|
89
|
+
|
|
90
|
+
path = strip_format(route.path.spec.to_s)
|
|
91
|
+
|
|
92
|
+
if controller.present? && action.present?
|
|
93
|
+
has_own_name = !!route.name
|
|
94
|
+
name = route.name || name_by_action[[controller, action]]
|
|
95
|
+
name ||= name_by_path[[controller, path]] ? action : nil
|
|
96
|
+
elsif route.name.present?
|
|
97
|
+
has_own_name = true
|
|
98
|
+
name = route.name.to_s
|
|
99
|
+
controller = "_routes"
|
|
100
|
+
action = name
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
return unless name
|
|
104
|
+
|
|
105
|
+
verb = extract_verb(route)
|
|
106
|
+
return unless verb
|
|
107
|
+
|
|
108
|
+
required_parts = route.required_parts.map(&:to_s)
|
|
109
|
+
optional_parts = (route.path.optional_names || []).map(&:to_s) - ["format"]
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
name: name,
|
|
113
|
+
named: has_own_name || !!name_by_action[[controller, action]],
|
|
114
|
+
controller: controller,
|
|
115
|
+
action: action,
|
|
116
|
+
verb: verb,
|
|
117
|
+
path: path,
|
|
118
|
+
required_parts: required_parts,
|
|
119
|
+
optional_parts: optional_parts
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def collect_engine_routes(mount_route, engine)
|
|
124
|
+
mount_prefix = mount_route.path.spec.to_s
|
|
125
|
+
engine_name = mount_route.name
|
|
126
|
+
return unless engine_name
|
|
127
|
+
|
|
128
|
+
name_by_action, name_by_path = build_name_lookups(
|
|
129
|
+
engine.routes.named_routes,
|
|
130
|
+
path_prefix: mount_prefix,
|
|
131
|
+
name_prefix: "#{engine_name}_"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
engine.routes.routes.filter_map do |engine_route|
|
|
135
|
+
info = build_route_info(engine_route, name_by_action, name_by_path)
|
|
136
|
+
next unless info
|
|
137
|
+
info[:path] = mount_prefix + info[:path]
|
|
138
|
+
info
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def extract_verb(route)
|
|
143
|
+
verb = route.verb
|
|
144
|
+
return nil if verb.blank?
|
|
145
|
+
|
|
146
|
+
verb.split("|").first&.strip&.downcase
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Typelizer
|
|
6
|
+
class RouteWriter
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
@template_cache = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(routes, force:)
|
|
13
|
+
FileUtils.rm_rf(config.output_dir) if force
|
|
14
|
+
|
|
15
|
+
written_files = []
|
|
16
|
+
|
|
17
|
+
controllers = routes.group_by { |r| r[:controller] }
|
|
18
|
+
named = build_named_routes(routes, controllers)
|
|
19
|
+
|
|
20
|
+
controllers.each do |controller, controller_routes|
|
|
21
|
+
written_files << write_controller(controller, controller_routes)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
written_files << write_index(controllers, named)
|
|
25
|
+
|
|
26
|
+
written_files << write_runtime
|
|
27
|
+
|
|
28
|
+
cleanup_stale_files(written_files) unless force
|
|
29
|
+
|
|
30
|
+
Typelizer.logger.debug("Generated #{written_files.size} route files in #{config.output_dir}")
|
|
31
|
+
|
|
32
|
+
written_files
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :config, :template_cache
|
|
38
|
+
|
|
39
|
+
def write_controller(controller, routes)
|
|
40
|
+
ctrl_path = controller_filename(controller)
|
|
41
|
+
filename = "#{ctrl_path}.#{config.file_ext}"
|
|
42
|
+
|
|
43
|
+
prepared_routes = routes.map do |route|
|
|
44
|
+
key = route_key(route, routes)
|
|
45
|
+
|
|
46
|
+
required = route[:required_parts].map { |p| camelize_key(p) }
|
|
47
|
+
optional = route[:optional_parts].map { |p| camelize_key(p) }
|
|
48
|
+
single_required = required.size == 1 && optional.empty?
|
|
49
|
+
|
|
50
|
+
route.merge(
|
|
51
|
+
key: key,
|
|
52
|
+
required_params: required,
|
|
53
|
+
optional_params: optional,
|
|
54
|
+
single_required: single_required
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
runtime_import = (ctrl_path.count("/") > 0) ? "#{"../" * ctrl_path.count("/")}runtime" : "./runtime"
|
|
59
|
+
content = render_template("route_controller.erb", ts: config.ts?, routes: prepared_routes, runtime_import: runtime_import)
|
|
60
|
+
|
|
61
|
+
write_file(filename, content) do
|
|
62
|
+
content
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def write_index(controllers, named)
|
|
67
|
+
entries = controllers.map do |controller, _routes|
|
|
68
|
+
{
|
|
69
|
+
namespace: controller_namespace_name(controller),
|
|
70
|
+
file: controller_filename(controller)
|
|
71
|
+
}
|
|
72
|
+
end.sort_by { |e| e[:namespace] }
|
|
73
|
+
|
|
74
|
+
sorted_named = named.sort_by { |n| n[:export_name] }
|
|
75
|
+
content = render_template("route_index.erb", entries: entries, named_routes: sorted_named)
|
|
76
|
+
|
|
77
|
+
write_file("index.#{config.file_ext}", content) do
|
|
78
|
+
content
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
RUNTIME_TEMPLATES = {
|
|
83
|
+
"ts" => File.read(File.join(__dir__, "templates/route_runtime.ts")),
|
|
84
|
+
"js" => File.read(File.join(__dir__, "templates/route_runtime.js"))
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
def write_runtime
|
|
88
|
+
content = RUNTIME_TEMPLATES.fetch(config.file_ext)
|
|
89
|
+
|
|
90
|
+
write_file("runtime.#{config.file_ext}", content) do
|
|
91
|
+
content
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def write_file(filename, fingerprint)
|
|
96
|
+
output_file = File.join(config.output_dir, filename)
|
|
97
|
+
digest = render_template("fingerprint.erb", fingerprint: fingerprint)
|
|
98
|
+
|
|
99
|
+
existing_header = begin
|
|
100
|
+
File.read(output_file, digest.bytesize)
|
|
101
|
+
rescue
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
return output_file if existing_header == digest
|
|
105
|
+
|
|
106
|
+
content = yield
|
|
107
|
+
|
|
108
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
|
109
|
+
|
|
110
|
+
File.write(output_file, digest + content)
|
|
111
|
+
output_file
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def cleanup_stale_files(written_files)
|
|
115
|
+
existing_files = Dir[File.join(config.output_dir, "**/*.#{config.file_ext}")]
|
|
116
|
+
stale_files = existing_files - written_files
|
|
117
|
+
|
|
118
|
+
File.delete(*stale_files) unless stale_files.empty?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def render_template(template, **context)
|
|
122
|
+
template_cache[template] ||= Renderer.new(template)
|
|
123
|
+
template_cache[template].call(**context)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def build_named_routes(routes, controllers)
|
|
127
|
+
controller_namespaces = controllers.keys.map { |c| controller_namespace_name(c) }.to_set
|
|
128
|
+
|
|
129
|
+
routes.filter_map do |route|
|
|
130
|
+
next unless route[:named]
|
|
131
|
+
export_name = camelize_key(route[:name])
|
|
132
|
+
next if controller_namespaces.include?(export_name)
|
|
133
|
+
|
|
134
|
+
ctrl_routes = controllers[route[:controller]]
|
|
135
|
+
key = route_key(route, ctrl_routes)
|
|
136
|
+
controller_var = "_#{controller_namespace_name(route[:controller])}"
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
export_name: export_name,
|
|
140
|
+
key: key,
|
|
141
|
+
controller_file: controller_filename(route[:controller]),
|
|
142
|
+
controller_var: controller_var
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def route_key(route, controller_routes)
|
|
148
|
+
collides = controller_routes.count { |r| r[:action] == route[:action] } > 1
|
|
149
|
+
camelize_key(collides ? route[:name] : route[:action])
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def camelize_key(key)
|
|
153
|
+
config.camel_case ? key.camelize(:lower) : key
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def controller_filename(controller)
|
|
157
|
+
@controller_filenames ||= {}
|
|
158
|
+
@controller_filenames[controller] ||= begin
|
|
159
|
+
parts = controller.split("/")
|
|
160
|
+
parts.map!(&:camelize)
|
|
161
|
+
parts[-1] = "#{parts[-1]}Controller"
|
|
162
|
+
parts.join("/")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def controller_namespace_name(controller)
|
|
167
|
+
name = controller.tr("/", "_")
|
|
168
|
+
camelize_key(name)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -29,6 +29,7 @@ module Typelizer
|
|
|
29
29
|
@properties ||= begin
|
|
30
30
|
props, typelizes = plugin.trait_properties(trait_name)
|
|
31
31
|
props = infer_types(props, typelizes)
|
|
32
|
+
props = transform_properties(props)
|
|
32
33
|
PropertySorter.sort(props, config.properties_sort_order)
|
|
33
34
|
end
|
|
34
35
|
end
|
|
@@ -37,7 +38,7 @@ module Typelizer
|
|
|
37
38
|
|
|
38
39
|
def infer_types(props, typelizes)
|
|
39
40
|
props.map do |prop|
|
|
40
|
-
dsl_type =
|
|
41
|
+
dsl_type = prop.lookup_in(typelizes)
|
|
41
42
|
prop
|
|
42
43
|
.then { |p| dsl_type&.any? ? p.with(**dsl_type) : apply_model_inference(p) }
|
|
43
44
|
.then { |p| apply_metadata(p) }
|
|
@@ -47,7 +47,6 @@ module Typelizer
|
|
|
47
47
|
trait_block = traits[trait_name]
|
|
48
48
|
return [], {} unless trait_block
|
|
49
49
|
|
|
50
|
-
# Create a collector to capture attributes defined in the trait block
|
|
51
50
|
collector = BlockAttributeCollector.new
|
|
52
51
|
collector.instance_exec(&trait_block)
|
|
53
52
|
|
|
@@ -76,16 +75,13 @@ module Typelizer
|
|
|
76
75
|
)
|
|
77
76
|
when BlockAttributeCollector::BlockNestedAttribute
|
|
78
77
|
prop_name = has_transform_key?(serializer) ? fetch_key(serializer, name) : name
|
|
79
|
-
nested_props, nested_typelizes = collect_nested_block(attr.block)
|
|
80
78
|
Property.new(
|
|
81
79
|
name: prop_name,
|
|
82
|
-
type:
|
|
80
|
+
type: Shape.new(properties: collect_nested_block(attr.block)),
|
|
83
81
|
optional: false,
|
|
84
82
|
nullable: false,
|
|
85
83
|
multi: false,
|
|
86
|
-
column_name: name
|
|
87
|
-
nested_properties: nested_props,
|
|
88
|
-
nested_typelizes: nested_typelizes
|
|
84
|
+
column_name: name
|
|
89
85
|
)
|
|
90
86
|
else
|
|
91
87
|
build_property(name, attr)
|
|
@@ -179,16 +175,13 @@ module Typelizer
|
|
|
179
175
|
)
|
|
180
176
|
when ::Alba::NestedAttribute
|
|
181
177
|
block = attr.instance_variable_get(:@block)
|
|
182
|
-
nested_props, nested_typelizes = collect_nested_block(block)
|
|
183
178
|
Property.new(
|
|
184
179
|
name: name,
|
|
185
|
-
type:
|
|
180
|
+
type: Shape.new(properties: collect_nested_block(block)),
|
|
186
181
|
optional: false,
|
|
187
182
|
nullable: false,
|
|
188
183
|
multi: false,
|
|
189
184
|
column_name: column_name,
|
|
190
|
-
nested_properties: nested_props,
|
|
191
|
-
nested_typelizes: nested_typelizes,
|
|
192
185
|
**options
|
|
193
186
|
)
|
|
194
187
|
when ::Alba::ConditionalAttribute
|
|
@@ -217,13 +210,14 @@ module Typelizer
|
|
|
217
210
|
def collect_nested_block(block)
|
|
218
211
|
collector = BlockAttributeCollector.new
|
|
219
212
|
collector.instance_exec(&block)
|
|
213
|
+
typelizes = collector.collected_typelizes
|
|
220
214
|
|
|
221
|
-
|
|
215
|
+
collector.collected_attributes.map do |attr_name, attr|
|
|
222
216
|
attr_name_str = attr_name.is_a?(Symbol) ? attr_name.name : attr_name
|
|
223
|
-
build_collected_property(attr_name_str, attr)
|
|
217
|
+
prop = build_collected_property(attr_name_str, attr)
|
|
218
|
+
override = prop.lookup_in(typelizes)
|
|
219
|
+
override&.any? ? prop.with(**override) : prop
|
|
224
220
|
end
|
|
225
|
-
|
|
226
|
-
[props, collector.collected_typelizes]
|
|
227
221
|
end
|
|
228
222
|
end
|
|
229
223
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
class Shape
|
|
5
|
+
attr_reader :properties
|
|
6
|
+
|
|
7
|
+
def initialize(properties:)
|
|
8
|
+
@properties = properties.freeze
|
|
9
|
+
@fingerprint = ["Shape", properties.map(&:fingerprint)].freeze
|
|
10
|
+
@hash = @fingerprint.hash
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def map_properties
|
|
15
|
+
self.class.new(properties: properties.map { |p| yield p })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(sort_order: :none, prefer_double_quotes: false)
|
|
19
|
+
inner = properties.map { |p|
|
|
20
|
+
(p.render(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes) + ";")
|
|
21
|
+
.gsub(/^/, " ")
|
|
22
|
+
}.join("\n")
|
|
23
|
+
"{\n#{inner}\n}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
alias_method :to_s, :render
|
|
27
|
+
|
|
28
|
+
attr_reader :fingerprint, :hash
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(Shape) && fingerprint == other.fingerprint
|
|
32
|
+
end
|
|
33
|
+
alias_method :eql?, :==
|
|
34
|
+
|
|
35
|
+
def inspect
|
|
36
|
+
"<Typelizer::Shape properties=#{properties.inspect}>"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
<%- enums.each do |property| -%>
|
|
2
2
|
export <%= property.enum_definition(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes) %>;
|
|
3
|
+
<%- if enum_runtime -%>
|
|
4
|
+
export <%= property.enum_runtime_definition(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes) %>;
|
|
5
|
+
<%- end -%>
|
|
3
6
|
<%- end -%>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%- if enums.any? -%>
|
|
2
2
|
<%- enums_path = prefer_double_quotes ? '"./Enums"' : "'./Enums'" -%>
|
|
3
|
-
|
|
3
|
+
<%- enum_keyword = enum_runtime ? "export" : "export type" -%>
|
|
4
|
+
<%= enum_keyword %> { <%= ImportSorter.sort(enums.map(&:enum_type_name), imports_sort_order).join(", ") %> } from <%= enums_path %>
|
|
4
5
|
<%- end -%>
|
|
5
6
|
<%- interfaces.each do |interface| -%>
|
|
6
7
|
<%- sorted_traits = ImportSorter.sort(interface.trait_interfaces.map(&:name), interface.config.imports_sort_order) -%>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<%- if ts -%>
|
|
2
|
+
import type { RouteDefinition, RouteOptions } from '<%= runtime_import %>'
|
|
3
|
+
<%- end -%>
|
|
4
|
+
import { buildUrl } from '<%= runtime_import %>'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
<%- routes.each_with_index do |route, idx| -%>
|
|
8
|
+
<%= "\n" if idx > 0 -%>
|
|
9
|
+
<%- parts = route[:required_params].map { |p| "#{p}#{ts ? ": string | number" : ""}" } -%>
|
|
10
|
+
<%- parts += route[:optional_params].map { |p| "#{p}#{ts ? "?: string | number" : ""}" } -%>
|
|
11
|
+
<%- has_params = parts.any? -%>
|
|
12
|
+
/** <%= route[:verb].upcase %> <%= route[:path] %> */
|
|
13
|
+
<%- if has_params -%>
|
|
14
|
+
<%- if ts -%>
|
|
15
|
+
<%= route[:key] %>: (
|
|
16
|
+
params: { <%= parts.join('; ') %> }<%= " | string | number" if route[:single_required] %>,
|
|
17
|
+
options?: RouteOptions,
|
|
18
|
+
): RouteDefinition<<%= "'#{route[:verb]}'" %>> => ({
|
|
19
|
+
<%- else -%>
|
|
20
|
+
<%= route[:key] %>: (params, options) => ({
|
|
21
|
+
<%- end -%>
|
|
22
|
+
url: buildUrl('<%= route[:path] %>', params, options),
|
|
23
|
+
method: '<%= route[:verb] %>',
|
|
24
|
+
}),
|
|
25
|
+
<%- else -%>
|
|
26
|
+
<%- if ts -%>
|
|
27
|
+
<%= route[:key] %>: (options?: RouteOptions): RouteDefinition<<%= "'#{route[:verb]}'" %>> => ({
|
|
28
|
+
<%- else -%>
|
|
29
|
+
<%= route[:key] %>: (options) => ({
|
|
30
|
+
<%- end -%>
|
|
31
|
+
url: buildUrl('<%= route[:path] %>', {}, options),
|
|
32
|
+
method: '<%= route[:verb] %>',
|
|
33
|
+
}),
|
|
34
|
+
<%- end -%>
|
|
35
|
+
<%- end -%>
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%- entries.each do |entry| -%>
|
|
2
|
+
export { default as <%= entry[:namespace] %> } from './<%= entry[:file] %>'
|
|
3
|
+
<%- end -%>
|
|
4
|
+
<%- if named_routes.any? -%>
|
|
5
|
+
<%- controllers = named_routes.group_by { |r| r[:controller_file] } -%>
|
|
6
|
+
|
|
7
|
+
<%- controllers.each do |file, routes| -%>
|
|
8
|
+
import <%= routes.first[:controller_var] %> from './<%= file %>'
|
|
9
|
+
<%- end -%>
|
|
10
|
+
|
|
11
|
+
<%- named_routes.each do |route| -%>
|
|
12
|
+
export const <%= route[:export_name] %> = <%= route[:controller_var] %>.<%= route[:key] %>
|
|
13
|
+
<%- end -%>
|
|
14
|
+
<%- end -%>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
let baseUrl = ''
|
|
2
|
+
let urlDefaults = {}
|
|
3
|
+
|
|
4
|
+
export function setBaseUrl(url) {
|
|
5
|
+
baseUrl = url.replace(/\/+$/, '')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function setUrlDefaults(defaults) {
|
|
9
|
+
urlDefaults = defaults
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function addUrlDefault(key, value) {
|
|
13
|
+
const current = typeof urlDefaults === 'function' ? urlDefaults : { ...urlDefaults }
|
|
14
|
+
if (typeof current === 'function') {
|
|
15
|
+
const fn = current
|
|
16
|
+
urlDefaults = () => ({ ...fn(), [key]: value })
|
|
17
|
+
} else {
|
|
18
|
+
current[key] = value
|
|
19
|
+
urlDefaults = current
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildUrl(
|
|
24
|
+
template,
|
|
25
|
+
params,
|
|
26
|
+
options,
|
|
27
|
+
) {
|
|
28
|
+
const defaults = typeof urlDefaults === 'function' ? urlDefaults() : urlDefaults
|
|
29
|
+
let p
|
|
30
|
+
|
|
31
|
+
if (typeof params === 'string' || typeof params === 'number') {
|
|
32
|
+
const key = template.match(/[:*](\w+)/)?.[1]
|
|
33
|
+
p = key ? { ...defaults, [key]: params } : { ...defaults }
|
|
34
|
+
} else {
|
|
35
|
+
p = { ...defaults, ...params }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Optional segments: fill or remove
|
|
39
|
+
let url = template.replace(/\(\/:?(\w+)\)/g, (_, key) => {
|
|
40
|
+
const val = getParam(p, key)
|
|
41
|
+
if (val != null) return `/${encodeURIComponent(String(val))}`
|
|
42
|
+
return ''
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Glob params
|
|
46
|
+
url = url.replace(/\*(\w+)/g, (match, key) => {
|
|
47
|
+
const val = getParam(p, key)
|
|
48
|
+
if (val == null) return match
|
|
49
|
+
return Array.isArray(val) ? encodeURI(val.map(String).join('/')) : encodeURI(String(val))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Required params
|
|
53
|
+
url = url.replace(/:(\w+)/g, (match, key) => {
|
|
54
|
+
const val = getParam(p, key)
|
|
55
|
+
if (val != null) return encodeURIComponent(String(val))
|
|
56
|
+
return match
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Query string
|
|
60
|
+
if (options?.query) {
|
|
61
|
+
const qs = buildQuery(options.query)
|
|
62
|
+
if (qs) url += `?${qs}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Anchor
|
|
66
|
+
if (options?.anchor) url += `#${options.anchor}`
|
|
67
|
+
|
|
68
|
+
return baseUrl + url
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Accepts both snake_case and camelCase param keys by looking up
|
|
72
|
+
// the snake_case key from the URL template, then its camelCase equivalent.
|
|
73
|
+
function getParam(params, key) {
|
|
74
|
+
if (key in params) return params[key]
|
|
75
|
+
return params[toCamelCase(key)]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toCamelCase(key) {
|
|
79
|
+
return key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildQuery(params, prefix) {
|
|
83
|
+
const parts = []
|
|
84
|
+
for (const [key, value] of Object.entries(params)) {
|
|
85
|
+
const k = prefix ? `${prefix}[${key}]` : key
|
|
86
|
+
if (value === null || value === undefined) continue
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
value.forEach(v => parts.push(`${k}[]=${encodeURIComponent(String(v))}`))
|
|
89
|
+
} else if (typeof value === 'object') {
|
|
90
|
+
const nested = buildQuery(value, k)
|
|
91
|
+
if (nested) parts.push(nested)
|
|
92
|
+
} else {
|
|
93
|
+
parts.push(`${k}=${encodeURIComponent(String(value))}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return parts.join('&')
|
|
97
|
+
}
|