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,109 @@
|
|
|
1
|
+
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'
|
|
2
|
+
|
|
3
|
+
export type RouteDefinition<M extends Method> = {
|
|
4
|
+
url: string
|
|
5
|
+
method: M
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type RouteOptions = {
|
|
9
|
+
query?: Record<string, unknown>
|
|
10
|
+
anchor?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let baseUrl = ''
|
|
14
|
+
let urlDefaults: Record<string, unknown> | (() => Record<string, unknown>) = {}
|
|
15
|
+
|
|
16
|
+
export function setBaseUrl(url: string): void {
|
|
17
|
+
baseUrl = url.replace(/\/+$/, '')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setUrlDefaults(defaults: Record<string, unknown> | (() => Record<string, unknown>)): void {
|
|
21
|
+
urlDefaults = defaults
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function addUrlDefault(key: string, value: unknown): void {
|
|
25
|
+
const current = typeof urlDefaults === 'function' ? urlDefaults : { ...urlDefaults }
|
|
26
|
+
if (typeof current === 'function') {
|
|
27
|
+
const fn = current
|
|
28
|
+
urlDefaults = () => ({ ...fn(), [key]: value })
|
|
29
|
+
} else {
|
|
30
|
+
current[key] = value
|
|
31
|
+
urlDefaults = current
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildUrl(
|
|
36
|
+
template: string,
|
|
37
|
+
params: Record<string, unknown> | string | number,
|
|
38
|
+
options?: RouteOptions,
|
|
39
|
+
): string {
|
|
40
|
+
const defaults = typeof urlDefaults === 'function' ? urlDefaults() : urlDefaults
|
|
41
|
+
let p: Record<string, unknown>
|
|
42
|
+
|
|
43
|
+
if (typeof params === 'string' || typeof params === 'number') {
|
|
44
|
+
const key = template.match(/[:*](\w+)/)?.[1]
|
|
45
|
+
p = key ? { ...defaults, [key]: params } : { ...defaults }
|
|
46
|
+
} else {
|
|
47
|
+
p = { ...defaults, ...params }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Optional segments: fill or remove
|
|
51
|
+
let url = template.replace(/\(\/:?(\w+)\)/g, (_, key) => {
|
|
52
|
+
const val = getParam(p, key)
|
|
53
|
+
if (val != null) return `/${encodeURIComponent(String(val))}`
|
|
54
|
+
return ''
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Glob params
|
|
58
|
+
url = url.replace(/\*(\w+)/g, (match, key) => {
|
|
59
|
+
const val = getParam(p, key)
|
|
60
|
+
if (val == null) return match
|
|
61
|
+
return Array.isArray(val) ? encodeURI(val.map(String).join('/')) : encodeURI(String(val))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Required params
|
|
65
|
+
url = url.replace(/:(\w+)/g, (match, key) => {
|
|
66
|
+
const val = getParam(p, key)
|
|
67
|
+
if (val != null) return encodeURIComponent(String(val))
|
|
68
|
+
return match
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Query string
|
|
72
|
+
if (options?.query) {
|
|
73
|
+
const qs = buildQuery(options.query)
|
|
74
|
+
if (qs) url += `?${qs}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Anchor
|
|
78
|
+
if (options?.anchor) url += `#${options.anchor}`
|
|
79
|
+
|
|
80
|
+
return baseUrl + url
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Accepts both snake_case and camelCase param keys by looking up
|
|
84
|
+
// the snake_case key from the URL template, then its camelCase equivalent.
|
|
85
|
+
function getParam(params: Record<string, unknown>, key: string): unknown {
|
|
86
|
+
if (key in params) return params[key]
|
|
87
|
+
return params[toCamelCase(key)]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toCamelCase(key: string): string {
|
|
91
|
+
return key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildQuery(params: Record<string, unknown>, prefix?: string): string {
|
|
95
|
+
const parts: string[] = []
|
|
96
|
+
for (const [key, value] of Object.entries(params)) {
|
|
97
|
+
const k = prefix ? `${prefix}[${key}]` : key
|
|
98
|
+
if (value === null || value === undefined) continue
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
value.forEach(v => parts.push(`${k}[]=${encodeURIComponent(String(v))}`))
|
|
101
|
+
} else if (typeof value === 'object') {
|
|
102
|
+
const nested = buildQuery(value as Record<string, unknown>, k)
|
|
103
|
+
if (nested) parts.push(nested)
|
|
104
|
+
} else {
|
|
105
|
+
parts.push(`${k}=${encodeURIComponent(String(value))}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return parts.join('&')
|
|
109
|
+
}
|
|
@@ -15,19 +15,27 @@ module Typelizer
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def transform_properties(props)
|
|
19
|
+
return props unless config.properties_transformer
|
|
20
|
+
|
|
21
|
+
props = config.properties_transformer.call(props)
|
|
22
|
+
props.map do |prop|
|
|
23
|
+
next prop unless prop.type.is_a?(Shape)
|
|
24
|
+
|
|
25
|
+
prop.with(type: Shape.new(properties: transform_properties(prop.type.properties)))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
def infer_nested_property_types(prop)
|
|
19
|
-
return prop unless prop.
|
|
30
|
+
return prop unless prop.type.is_a?(Shape)
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
inferred = prop.nested_properties.map do |sub_prop|
|
|
23
|
-
dsl_type = typelizes[sub_prop.column_name.to_sym] || typelizes[sub_prop.name.to_sym]
|
|
32
|
+
inferred = prop.type.map_properties do |sub_prop|
|
|
24
33
|
sub_prop
|
|
25
|
-
.then { |p|
|
|
34
|
+
.then { |p| p.type ? p : apply_model_inference(p) }
|
|
26
35
|
.then { |p| apply_metadata(p) }
|
|
27
36
|
.then { |p| infer_nested_property_types(p) }
|
|
28
37
|
end
|
|
29
|
-
|
|
30
|
-
prop.with(nested_properties: inferred)
|
|
38
|
+
prop.with(type: inferred)
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def model_class
|
|
@@ -23,6 +23,7 @@ module Typelizer
|
|
|
23
23
|
|
|
24
24
|
def parse(type_def, **options)
|
|
25
25
|
return options if type_def.nil?
|
|
26
|
+
return parse_shape(type_def, **options) if type_def.is_a?(Hash)
|
|
26
27
|
return parse_array(type_def, **options) if type_def.is_a?(Array)
|
|
27
28
|
|
|
28
29
|
type_str = type_def.to_s
|
|
@@ -49,8 +50,35 @@ module Typelizer
|
|
|
49
50
|
type_str.end_with?("?", "[]")
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
# Strips a trailing `?` from an attribute key, returning [clean_name, optional?].
|
|
54
|
+
def parse_key(name)
|
|
55
|
+
str = name.to_s
|
|
56
|
+
str.end_with?("?") ? [str.chomp("?").to_sym, true] : [name.to_sym, false]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def apply_optional_key(parsed, optional_from_key)
|
|
60
|
+
parsed[:optional] = true if optional_from_key && !parsed.key?(:optional)
|
|
61
|
+
parsed
|
|
62
|
+
end
|
|
63
|
+
|
|
52
64
|
private
|
|
53
65
|
|
|
66
|
+
def parse_shape(hash, **options)
|
|
67
|
+
properties = hash.map do |name, value|
|
|
68
|
+
clean_name, optional_from_key = parse_key(name)
|
|
69
|
+
|
|
70
|
+
# parse_declaration returns Hash args verbatim (options-bag form); nested
|
|
71
|
+
# shapes need parse to dispatch back here and build a Shape.
|
|
72
|
+
parsed = value.is_a?(Hash) ? parse(value) : parse_declaration(value)
|
|
73
|
+
apply_optional_key(parsed, optional_from_key)
|
|
74
|
+
|
|
75
|
+
property_attrs = parsed.slice(*Property.members).tap { |h| h[:name] = clean_name }
|
|
76
|
+
Property.new(optional: false, nullable: false, multi: false, **property_attrs)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
{type: Shape.new(properties: properties)}.merge(options)
|
|
80
|
+
end
|
|
81
|
+
|
|
54
82
|
def parse_array(type_defs, **options)
|
|
55
83
|
raise ArgumentError, "Empty array passed to typelize" if type_defs.empty?
|
|
56
84
|
|
data/lib/typelizer/version.rb
CHANGED
data/lib/typelizer/writer.rb
CHANGED
|
@@ -60,9 +60,9 @@ module Typelizer
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def write_enums(enums)
|
|
63
|
-
fingerprint = [enums.map(&:fingerprint), config.properties_sort_order, config.prefer_double_quotes].inspect
|
|
63
|
+
fingerprint = [enums.map(&:fingerprint), config.properties_sort_order, config.prefer_double_quotes, config.enum_runtime].inspect
|
|
64
64
|
write_file("Enums.ts", fingerprint) do
|
|
65
|
-
render_template("enums.ts.erb", enums: enums, sort_order: config.properties_sort_order, prefer_double_quotes: config.prefer_double_quotes)
|
|
65
|
+
render_template("enums.ts.erb", enums: enums, sort_order: config.properties_sort_order, prefer_double_quotes: config.prefer_double_quotes, enum_runtime: config.enum_runtime)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
@@ -74,7 +74,7 @@ module Typelizer
|
|
|
74
74
|
}
|
|
75
75
|
].inspect
|
|
76
76
|
write_file("index.ts", fingerprint) do
|
|
77
|
-
render_template("index.ts.erb", interfaces: interfaces, enums: enums, index_dir: config.output_dir.to_s, imports_sort_order: config.imports_sort_order, prefer_double_quotes: config.prefer_double_quotes)
|
|
77
|
+
render_template("index.ts.erb", interfaces: interfaces, enums: enums, index_dir: config.output_dir.to_s, imports_sort_order: config.imports_sort_order, prefer_double_quotes: config.prefer_double_quotes, enum_runtime: config.enum_runtime)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -88,7 +88,7 @@ module Typelizer
|
|
|
88
88
|
def write_file(filename, fingerprint, output_dir: config.output_dir)
|
|
89
89
|
output_file = File.join(output_dir, filename)
|
|
90
90
|
existing_content = File.exist?(output_file) ? File.read(output_file) : nil
|
|
91
|
-
digest = render_template("fingerprint.
|
|
91
|
+
digest = render_template("fingerprint.erb", fingerprint: fingerprint)
|
|
92
92
|
|
|
93
93
|
return output_file if existing_content&.start_with?(digest)
|
|
94
94
|
|
data/lib/typelizer.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "typelizer/version"
|
|
4
4
|
require_relative "typelizer/union_type_sorter"
|
|
5
|
+
require_relative "typelizer/shape"
|
|
5
6
|
require_relative "typelizer/property"
|
|
6
7
|
require_relative "typelizer/model_plugins/auto"
|
|
7
8
|
require_relative "typelizer/serializer_plugins/auto"
|
|
@@ -18,6 +19,9 @@ require_relative "typelizer/renderer"
|
|
|
18
19
|
require_relative "typelizer/writer"
|
|
19
20
|
require_relative "typelizer/openapi"
|
|
20
21
|
require_relative "typelizer/generator"
|
|
22
|
+
require_relative "typelizer/route_config"
|
|
23
|
+
require_relative "typelizer/route_generator"
|
|
24
|
+
require_relative "typelizer/route_writer"
|
|
21
25
|
require_relative "typelizer/type_parser"
|
|
22
26
|
require_relative "typelizer/dsl"
|
|
23
27
|
|
|
@@ -44,10 +48,14 @@ module Typelizer
|
|
|
44
48
|
# writers
|
|
45
49
|
def_delegators :configuration, :dirs=, :reject_class=, :listen=
|
|
46
50
|
|
|
51
|
+
# Is Typelizer active?
|
|
52
|
+
#
|
|
53
|
+
# Precedence: TYPELIZER env var > development? detection
|
|
47
54
|
def enabled?
|
|
48
|
-
|
|
55
|
+
val = ENV["TYPELIZER"]
|
|
56
|
+
return val == "true" || val == "1" if val
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
development?
|
|
51
59
|
end
|
|
52
60
|
|
|
53
61
|
attr_accessor :logger
|
|
@@ -84,6 +92,12 @@ module Typelizer
|
|
|
84
92
|
|
|
85
93
|
private
|
|
86
94
|
|
|
95
|
+
def development?
|
|
96
|
+
return Rails.env.development? if defined?(Rails) && Rails.respond_to?(:env)
|
|
97
|
+
|
|
98
|
+
ENV["RAILS_ENV"] == "development" || ENV["RACK_ENV"] == "development"
|
|
99
|
+
end
|
|
100
|
+
|
|
87
101
|
def load_serializers
|
|
88
102
|
dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] }.each { |file| require file }
|
|
89
103
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: typelizer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 6.
|
|
18
|
+
version: 6.1.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 6.
|
|
25
|
+
version: 6.1.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: benchmark
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -62,6 +62,7 @@ files:
|
|
|
62
62
|
- lib/typelizer/import_sorter.rb
|
|
63
63
|
- lib/typelizer/interface.rb
|
|
64
64
|
- lib/typelizer/listen.rb
|
|
65
|
+
- lib/typelizer/middleware.rb
|
|
65
66
|
- lib/typelizer/model_plugins/active_record.rb
|
|
66
67
|
- lib/typelizer/model_plugins/auto.rb
|
|
67
68
|
- lib/typelizer/model_plugins/poro.rb
|
|
@@ -70,6 +71,9 @@ files:
|
|
|
70
71
|
- lib/typelizer/property_sorter.rb
|
|
71
72
|
- lib/typelizer/railtie.rb
|
|
72
73
|
- lib/typelizer/renderer.rb
|
|
74
|
+
- lib/typelizer/route_config.rb
|
|
75
|
+
- lib/typelizer/route_generator.rb
|
|
76
|
+
- lib/typelizer/route_writer.rb
|
|
73
77
|
- lib/typelizer/serializer_config_layer.rb
|
|
74
78
|
- lib/typelizer/serializer_plugins/alba.rb
|
|
75
79
|
- lib/typelizer/serializer_plugins/alba/block_attribute_collector.rb
|
|
@@ -79,13 +83,18 @@ files:
|
|
|
79
83
|
- lib/typelizer/serializer_plugins/base.rb
|
|
80
84
|
- lib/typelizer/serializer_plugins/oj_serializers.rb
|
|
81
85
|
- lib/typelizer/serializer_plugins/panko.rb
|
|
86
|
+
- lib/typelizer/shape.rb
|
|
82
87
|
- lib/typelizer/templates/comment.ts.erb
|
|
83
88
|
- lib/typelizer/templates/enums.ts.erb
|
|
84
|
-
- lib/typelizer/templates/fingerprint.
|
|
89
|
+
- lib/typelizer/templates/fingerprint.erb
|
|
85
90
|
- lib/typelizer/templates/index.ts.erb
|
|
86
91
|
- lib/typelizer/templates/inheritance.ts.erb
|
|
87
92
|
- lib/typelizer/templates/inline_type.ts.erb
|
|
88
93
|
- lib/typelizer/templates/interface.ts.erb
|
|
94
|
+
- lib/typelizer/templates/route_controller.erb
|
|
95
|
+
- lib/typelizer/templates/route_index.erb
|
|
96
|
+
- lib/typelizer/templates/route_runtime.js
|
|
97
|
+
- lib/typelizer/templates/route_runtime.ts
|
|
89
98
|
- lib/typelizer/type_inference.rb
|
|
90
99
|
- lib/typelizer/type_parser.rb
|
|
91
100
|
- lib/typelizer/union_type_sorter.rb
|
|
File without changes
|