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.
@@ -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.nested_properties&.any?
30
+ return prop unless prop.type.is_a?(Shape)
20
31
 
21
- typelizes = prop.nested_typelizes || {}
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| dsl_type&.any? ? p.with(**dsl_type) : apply_model_inference(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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typelizer
4
- VERSION = "0.11.0"
4
+ VERSION = "0.13.0"
5
5
  end
@@ -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.ts.erb", fingerprint: 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
- return false if ENV["DISABLE_TYPELIZER"] == "true" || ENV["DISABLE_TYPELIZER"] == "1"
55
+ val = ENV["TYPELIZER"]
56
+ return val == "true" || val == "1" if val
49
57
 
50
- ENV["RAILS_ENV"] == "development" || ENV["RACK_ENV"] == "development" || ENV["DISABLE_TYPELIZER"] == "false"
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.11.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.0.0
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.0.0
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.ts.erb
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