types_from_serializers 0.1.3 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ddc6e7ca4c4edfab72abe33e0a3688871bd8e746f00f0ea00c832e9b4bad700e
4
- data.tar.gz: cf128002a889f0446b6ddc0c8a212dbbd5885af27c6aee1552a8de9b5f723a22
3
+ metadata.gz: 107bcbbe1708fd38efdb7943673c11112350eeee1e457e67e50934641e8081e7
4
+ data.tar.gz: dbb8a8732f99088318ae2d7c16b11dc219bb96357347d1d2174b106216e82599
5
5
  SHA512:
6
- metadata.gz: 4f05eada087b1d751525c2439ad60918e0179fce0766230d75502e3ad1d8970af74057e1262c89eaf04b372885574b01d645f852b284d56f73bb88eeab6e072e
7
- data.tar.gz: 98c5830eb74ae87c10313ad07eb4f72f35f444f8f4c75db8e79640200017e67dff65e45464aa24afcfb68fad3261735ee7ae3b9e3a155a090e91160c8bb9fac3
6
+ metadata.gz: d6e0dc57e64062a6934f0e3f336412c897ec4fe5d7497b65536bf45c59c270f39e278fc183f299bfe0cdc6b55769e652d3f6b153bb298bc2f311a0edf2a8e3ad
7
+ data.tar.gz: 00f07fd96b3e9fa907b457969d227e641cfcce1c9e2f67a56bb9f5d9f07b8222d8861144737eb443508ed50024d31992836a3285a1c739e9588817912a6ca168
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [2.0.0](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@0.1.2...types_from_serializers@2.0.0) (2023-04-02)
2
+
3
+ This version adds support for `oj_serializers-2.0.2`, supporting all changes in:
4
+
5
+ - https://github.com/ElMassimo/oj_serializers/pull/9
6
+
7
+ ### Features ✨
8
+
9
+ - Now keys will match the [`transform_keys`](https://github.com/ElMassimo/oj_serializers#transforming-attribute-keys-) configuration instead of always being camelized
10
+ - Support for [`flat_one`](https://github.com/ElMassimo/oj_serializers#composing-serializers-)
11
+ - Use relative paths for imports to make the output configuration more flexible
12
+ - Define the order of properties in the interface with `sort_properties_by`
13
+
1
14
  ## [0.1.3](https://github.com/ElMassimo/types_from_serializers/compare/types_from_serializers@0.1.2...types_from_serializers@0.1.3) (2022-07-12)
2
15
 
3
16
 
@@ -3,7 +3,7 @@
3
3
  require "active_support/concern"
4
4
 
5
5
  # Internal: A DSL to specify types for serializer attributes.
6
- module TypesFromSerializer
6
+ module TypesFromSerializers
7
7
  module DSL
8
8
  extend ActiveSupport::Concern
9
9
 
@@ -16,74 +16,30 @@ module TypesFromSerializer
16
16
  def object_as(name, model: nil, types_from: nil)
17
17
  # NOTE: Avoid taking memory for type information that won't be used.
18
18
  if Rails.env.development?
19
- model ||= name.is_a?(Symbol) ? name : try(:_serializer_model_name)
20
- @_serializer_model_name = model || name
21
- @_serializer_types_from = types_from if types_from
19
+ model ||= name.is_a?(Symbol) ? name : try(:_serializer_model_name) || name
20
+ define_singleton_method(:_serializer_model_name) { model }
21
+ define_singleton_method(:_serializer_types_from) { types_from } if types_from
22
22
  end
23
23
 
24
24
  super(name)
25
25
  end
26
26
 
27
- # Public: Like `attributes`, but providing type information for each field.
28
- def typed_attributes(attrs)
29
- attributes(*attrs.keys)
30
-
31
- # NOTE: Avoid taking memory for type information that won't be used.
32
- if Rails.env.development?
33
- _typed_attributes.update(attrs.map { |key, type|
34
- [key.to_s, type.is_a?(Hash) ? type : {type: type}]
35
- }.to_h)
36
- end
37
- end
38
-
39
- # Public: Allows to specify the type for a serializer method that will
40
- # be defined immediately after calling this method.
41
- def type(type = :unknown, optional: false)
42
- @_current_attribute_type = {type: type, optional: optional}
43
- end
44
-
45
- # Internal: Intercept a method definition, tying a type that was
46
- # previously specified to the name of the attribute.
47
- def method_added(name)
48
- super(name)
49
- if @_current_attribute_type
50
- serializer_attributes name
51
-
52
- # NOTE: Avoid taking memory for type information that won't be used.
53
- if Rails.env.development?
54
- _typed_attributes[name.to_s] = @_current_attribute_type
55
- end
56
-
57
- @_current_attribute_type = nil
58
- end
27
+ # Public: Shortcut for typing a serializer attribute.
28
+ #
29
+ # It specifies the type for a serializer method that will be defined
30
+ # immediately after calling this method.
31
+ def type(type, **options)
32
+ attribute type: type, **options
59
33
  end
60
34
 
61
- # NOTE: Avoid taking memory for type information that won't be used.
62
- if Rails.env.development?
63
- # Internal: Contains type information for serializer attributes.
64
- def _typed_attributes
65
- unless defined?(@_typed_attributes)
66
- @_typed_attributes = superclass.try(:_typed_attributes)&.dup || {}
67
- end
68
- @_typed_attributes
69
- end
70
-
71
- # Internal: The name of the model that will be serialized by this
72
- # serializer, used to infer field types from the SQL columns.
73
- def _serializer_model_name
74
- unless defined?(@_serializer_model_name)
75
- @_serializer_model_name = superclass.try(:_serializer_model_name)
76
- end
77
- @_serializer_model_name
78
- end
35
+ private
79
36
 
80
- # Internal: The TypeScript interface that will be used by default to
81
- # infer the serializer field types when not explicitly provided.
82
- def _serializer_types_from
83
- unless defined?(@_serializer_types_from)
84
- @_serializer_types_from = superclass.try(:_serializer_types_from)
85
- end
86
- @_serializer_types_from
37
+ # Override: Remove unnecessary options in production, types are only
38
+ # used when generating code in development.
39
+ unless Rails.env.development?
40
+ def add_attribute(name, options)
41
+ options.except!(:type, :optional)
42
+ super
87
43
  end
88
44
  end
89
45
  end
@@ -6,36 +6,7 @@ require "pathname"
6
6
 
7
7
  # Public: Automatically generates TypeScript interfaces for Ruby serializers.
8
8
  module TypesFromSerializers
9
- # Internal: The configuration for TypeScript generation.
10
- Config = Struct.new(
11
- :base_serializers,
12
- :serializers_dirs,
13
- :output_dir,
14
- :name_from_serializer,
15
- :native_types,
16
- :sql_to_typescript_type_mapping,
17
- keyword_init: true,
18
- ) do
19
- def unknown_type
20
- sql_to_typescript_type_mapping.default
21
- end
22
- end
23
-
24
- # Internal: The type metadata for a serializer.
25
- SerializerMetadata = Struct.new(
26
- :attributes,
27
- :associations,
28
- :model_name,
29
- :types_from,
30
- keyword_init: true,
31
- )
32
-
33
- # Internal: The type metadata for a serializer field.
34
- FieldMetadata = Struct.new(:name, :type, :optional, :many, keyword_init: true) do
35
- def typescript_name
36
- name.to_s.camelize(:lower)
37
- end
38
- end
9
+ DEFAULT_TRANSFORM_KEYS = ->(key) { key.camelize(:lower).chomp("?") }
39
10
 
40
11
  # Internal: Extensions that simplify the implementation of the generator.
41
12
  module SerializerRefinements
@@ -58,91 +29,185 @@ module TypesFromSerializers
58
29
 
59
30
  refine Class do
60
31
  # Internal: Name of the TypeScript interface.
61
- def typescript_interface_name
32
+ def ts_name
62
33
  TypesFromSerializers.config.name_from_serializer.call(name).tr_s(":", "")
63
34
  end
64
35
 
65
36
  # Internal: The base name of the TypeScript file to be written.
66
- def typescript_interface_basename
37
+ def ts_filename
67
38
  TypesFromSerializers.config.name_from_serializer.call(name).gsub("::", "/")
68
39
  end
69
40
 
70
- # Internal: A first pass of gathering types for the serializer fields.
71
- def typescript_metadata
72
- SerializerMetadata.new(
73
- model_name: _serializer_model_name,
74
- types_from: _serializer_types_from,
75
- attributes: _attributes.map { |key, options|
76
- typed_attrs = _typed_attributes.fetch(key, {})
77
- FieldMetadata.new(
78
- **typed_attrs,
79
- name: key,
80
- optional: typed_attrs[:optional] || options.key?(:if),
81
- )
82
- },
83
- associations: _associations.map { |key, options|
84
- FieldMetadata.new(
85
- name: options.fetch(:root, key),
86
- type: options.fetch(:serializer),
87
- optional: options.key?(:if),
88
- many: options.fetch(:write_method) == :write_many,
89
- )
90
- },
91
- )
41
+ # Internal: The columns corresponding to the serializer model, if it's a
42
+ # record.
43
+ def model_columns
44
+ @model_columns ||= _serializer_model_name&.to_model.try(:columns_hash) || {}
92
45
  end
93
46
 
94
- # Internal: Infers field types by checking the SQL columns for the model
95
- # serialized, or from a TypeScript interface if provided.
96
- def typescript_infer_types(metadata)
97
- model = metadata.model_name&.to_model
98
- interface = metadata.types_from
99
-
100
- metadata.attributes.reject(&:type).each do |meta|
101
- if model&.respond_to?(:columns_hash) && (column = model.columns_hash[meta.name.to_s])
102
- meta[:type] = TypesFromSerializers.config.sql_to_typescript_type_mapping[column.type]
103
- meta[:optional] ||= column.null
104
- elsif interface
105
- meta[:type] = "#{interface}['#{meta.typescript_name}']"
106
- end
47
+ # Internal: The TypeScript properties of the serialzeir interface.
48
+ def ts_properties
49
+ @ts_properties ||= begin
50
+ types_from = try(:_serializer_types_from)
51
+
52
+ prepare_attributes(
53
+ sort_by: TypesFromSerializers.config.sort_properties_by,
54
+ transform_keys: TypesFromSerializers.config.transform_keys || try(:_transform_keys) || DEFAULT_TRANSFORM_KEYS,
55
+ )
56
+ .flat_map { |key, options|
57
+ if options[:association] == :flat
58
+ options.fetch(:serializer).ts_properties
59
+ else
60
+ Property.new(
61
+ name: key,
62
+ type: options[:serializer] || options[:type],
63
+ optional: options[:optional] || options.key?(:if),
64
+ multi: options[:association] == :many,
65
+ column_name: options.fetch(:value_from),
66
+ ).tap do |property|
67
+ property.infer_type_from(model_columns, types_from)
68
+ end
69
+ end
70
+ }
107
71
  end
108
72
  end
109
73
 
110
- def typescript_imports(metadata)
111
- assoc_imports = metadata.associations.map { |meta|
112
- [meta.type.typescript_interface_name, "~/types/serializers/#{meta.type.typescript_interface_basename}"]
74
+ # Internal: A first pass of gathering types for the serializer attributes.
75
+ def ts_interface
76
+ @ts_interface ||= Interface.new(
77
+ name: ts_name,
78
+ filename: ts_filename,
79
+ properties: ts_properties,
80
+ )
81
+ end
82
+ end
83
+ end
84
+
85
+ # Internal: The configuration for TypeScript generation.
86
+ Config = Struct.new(
87
+ :base_serializers,
88
+ :serializers_dirs,
89
+ :output_dir,
90
+ :custom_types_dir,
91
+ :name_from_serializer,
92
+ :global_types,
93
+ :sort_properties_by,
94
+ :sql_to_typescript_type_mapping,
95
+ :skip_serializer_if,
96
+ :transform_keys,
97
+ keyword_init: true,
98
+ ) do
99
+ def relative_custom_types_dir
100
+ @relative_custom_types_dir ||= (custom_types_dir || output_dir.parent).relative_path_from(output_dir)
101
+ end
102
+
103
+ def unknown_type
104
+ sql_to_typescript_type_mapping.default
105
+ end
106
+ end
107
+
108
+ # Internal: Information to generate a TypeScript interface for a serializer.
109
+ Interface = Struct.new(
110
+ :name,
111
+ :filename,
112
+ :properties,
113
+ keyword_init: true,
114
+ ) do
115
+ using SerializerRefinements
116
+
117
+ def inspect
118
+ to_h.inspect
119
+ end
120
+
121
+ # Internal: Returns a list of imports for types used in this interface.
122
+ def used_imports
123
+ association_serializers, attribute_types = properties.map(&:type).compact.uniq
124
+ .partition { |type| type.respond_to?(:ts_interface) }
125
+
126
+ serializer_type_imports = association_serializers.map(&:ts_interface)
127
+ .map { |type| [type.name, relative_path(type.pathname, pathname)] }
128
+
129
+ custom_type_imports = attribute_types
130
+ .flat_map { |type| extract_typescript_types(type.to_s) }
131
+ .uniq
132
+ .reject { |type| global_type?(type) }
133
+ .map { |type|
134
+ type_path = TypesFromSerializers.config.relative_custom_types_dir.join(type)
135
+ [type, relative_path(type_path, pathname)]
113
136
  }
114
137
 
115
- attr_imports = metadata.attributes
116
- .flat_map { |meta| extract_typescript_types(meta.type.to_s) }
117
- .uniq
118
- .reject { |type| typescript_native_type?(type) }
119
- .map { |type|
120
- [type, "~/types/#{type}"]
121
- }
122
-
123
- (assoc_imports + attr_imports).uniq.map { |interface, filename|
124
- "import type #{interface} from '#{filename}'\n"
125
- }.uniq
126
- end
138
+ (custom_type_imports + serializer_type_imports)
139
+ .map { |interface, filename| "import type #{interface} from '#{filename}'\n" }
140
+ end
127
141
 
128
- # Internal: Extracts any types inside generics or array types.
129
- def extract_typescript_types(type)
130
- type.split(/[<>\[\],\s|]+/)
131
- end
142
+ def as_typescript
143
+ <<~TS
144
+ interface #{name} {
145
+ #{properties.index_by(&:name).values.map(&:as_typescript).join("\n ")}
146
+ }
147
+ TS
148
+ end
149
+
150
+ protected
132
151
 
133
- # NOTE: Treat uppercase names as custom types.
134
- # Lowercase names would be native types, such as :string and :boolean.
135
- def typescript_native_type?(type)
136
- type[0] == type[0].downcase || TypesFromSerializers.config.native_types.include?(type)
152
+ def pathname
153
+ @pathname ||= Pathname.new(filename)
154
+ end
155
+
156
+ # Internal: Calculates a relative path that can be used in an import.
157
+ def relative_path(target_path, importer_path)
158
+ path = target_path.relative_path_from(importer_path.parent).to_s
159
+ path.start_with?(".") ? path : "./#{path}"
160
+ end
161
+
162
+ # Internal: Extracts any types inside generics or array types.
163
+ def extract_typescript_types(type)
164
+ type.split(/[<>\[\],\s|]+/)
165
+ end
166
+
167
+ # NOTE: Treat uppercase names as custom types.
168
+ # Lowercase names would be native types, such as :string and :boolean.
169
+ def global_type?(type)
170
+ type[0] == type[0].downcase || TypesFromSerializers.config.global_types.include?(type)
171
+ end
172
+ end
173
+
174
+ # Internal: The type metadata for a serializer attribute.
175
+ Property = Struct.new(
176
+ :name,
177
+ :type,
178
+ :optional,
179
+ :multi,
180
+ :column_name,
181
+ keyword_init: true,
182
+ ) do
183
+ using SerializerRefinements
184
+
185
+ def inspect
186
+ to_h.inspect
187
+ end
188
+
189
+ # Internal: Infers the property's type by checking a corresponding SQL
190
+ # column, or falling back to a TypeScript interface if provided.
191
+ def infer_type_from(columns_hash, ts_interface)
192
+ if type
193
+ type
194
+ elsif (column = columns_hash[column_name.to_s])
195
+ self.multi = true if column.try(:array)
196
+ self.optional = true if column.null && !column.default
197
+ self.type = TypesFromSerializers.config.sql_to_typescript_type_mapping[column.type]
198
+ elsif ts_interface
199
+ self.type = "#{ts_interface}['#{name}']"
137
200
  end
201
+ end
138
202
 
139
- def typescript_fields(metadata)
140
- (metadata.attributes + metadata.associations).map { |meta|
141
- type = meta.type.is_a?(Class) ? meta.type.typescript_interface_name : meta.type || TypesFromSerializers.config.unknown_type
142
- type = meta.many ? "#{type}[]" : type
143
- " #{meta.typescript_name}#{"?" if meta.optional}: #{type}"
144
- }
203
+ def as_typescript
204
+ type_str = if type.respond_to?(:ts_name)
205
+ type.ts_name
206
+ else
207
+ type || TypesFromSerializers.config.unknown_type
145
208
  end
209
+
210
+ "#{name}#{"?" if optional}: #{type_str}#{"[]" if multi}"
146
211
  end
147
212
  end
148
213
 
@@ -222,12 +287,10 @@ module TypesFromSerializers
222
287
 
223
288
  # Internal: Defines a TypeScript interface for the serializer.
224
289
  def generate_interface_for(serializer)
225
- metadata = serializer.typescript_metadata
226
- filename = serializer.typescript_interface_basename
290
+ interface = serializer.ts_interface
227
291
 
228
- write_if_changed(filename: filename, cache_key: metadata.inspect) {
229
- serializer.typescript_infer_types(metadata)
230
- serializer_interface_content(serializer, metadata)
292
+ write_if_changed(filename: interface.filename, cache_key: interface.inspect) {
293
+ serializer_interface_content(interface)
231
294
  }
232
295
  end
233
296
 
@@ -241,8 +304,11 @@ module TypesFromSerializers
241
304
  end
242
305
 
243
306
  # Internal: Checks if it should avoid generating an interface.
244
- def skip_serializer?(name)
245
- name.include?("BaseSerializer") || name.in?(config.base_serializers)
307
+ def skip_serializer?(serializer)
308
+ serializer.name.in?(config.base_serializers) ||
309
+ config.skip_serializer_if.call(serializer) ||
310
+ # NOTE: Ignore inline serializers.
311
+ serializer.ts_name.include?("Serializer")
246
312
  end
247
313
 
248
314
  # Internal: Returns an object compatible with FileUpdateChecker.
@@ -273,7 +339,7 @@ module TypesFromSerializers
273
339
  .flat_map(&:descendants)
274
340
  .uniq
275
341
  .sort_by(&:name)
276
- .reject { |s| skip_serializer?(s.name) }
342
+ .reject { |s| skip_serializer?(s) }
277
343
  rescue NameError
278
344
  raise ArgumentError, "Please ensure all your serializers extend BaseSerializer, or configure `config.base_serializers`."
279
345
  end
@@ -293,12 +359,18 @@ module TypesFromSerializers
293
359
  name_from_serializer: ->(name) { name.delete_suffix("Serializer") },
294
360
 
295
361
  # Types that don't need to be imported in TypeScript.
296
- native_types: [
362
+ global_types: [
297
363
  "Array",
298
364
  "Record",
299
365
  "Date",
300
366
  ].to_set,
301
367
 
368
+ # Allows to choose a different sort order, alphabetical by default.
369
+ sort_properties_by: :name,
370
+
371
+ # Allows to avoid generating a serializer.
372
+ skip_serializer_if: ->(serializer) { false },
373
+
302
374
  # Maps SQL column types to TypeScript native and custom types.
303
375
  sql_to_typescript_type_mapping: {
304
376
  boolean: :boolean,
@@ -311,6 +383,9 @@ module TypesFromSerializers
311
383
  }.tap do |types|
312
384
  types.default = :unknown
313
385
  end,
386
+
387
+ # Allows to transform keys, useful when converting objects client-side.
388
+ transform_keys: nil,
314
389
  )
315
390
  end
316
391
 
@@ -335,18 +410,18 @@ module TypesFromSerializers
335
410
  <<~TS
336
411
  //
337
412
  // DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
338
- #{serializers.map { |s| "export type { default as #{s.typescript_interface_name} } from './#{s.typescript_interface_basename}'" }.join("\n")}
413
+ #{serializers.map { |s|
414
+ "export type { default as #{s.ts_name} } from './#{s.ts_filename}'"
415
+ }.join("\n")}
339
416
  TS
340
417
  end
341
418
 
342
- def serializer_interface_content(serializer, metadata)
419
+ def serializer_interface_content(interface)
343
420
  <<~TS
344
421
  //
345
422
  // DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
346
- #{serializer.typescript_imports(metadata).join}
347
- export default interface #{serializer.typescript_interface_name} {
348
- #{serializer.typescript_fields(metadata).join("\n")}
349
- }
423
+ #{interface.used_imports.join}
424
+ export default #{interface.as_typescript}
350
425
  TS
351
426
  end
352
427
 
@@ -36,8 +36,11 @@ class TypesFromSerializers::Railtie < Rails::Railtie
36
36
  desc "Generates TypeScript interfaces for each serializer in the app."
37
37
  task generate: :environment do
38
38
  require_relative "generator"
39
+ start_time = Time.zone.now
40
+ print "Generating TypeScript interfaces..."
39
41
  serializers = TypesFromSerializers.generate(force: true)
40
- puts "Generated TypeScript interfaces for #{serializers.size} serializers:"
42
+ puts "completed in #{(Time.zone.now - start_time).round(2)} seconds.\n"
43
+ puts "Found #{serializers.size} serializers:"
41
44
  puts serializers.map { |s| "\t#{s.name}" }.join("\n")
42
45
  end
43
46
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module TypesFromSerializers
4
4
  # Public: This library adheres to semantic versioning.
5
- VERSION = "0.1.3"
5
+ VERSION = "2.0.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: types_from_serializers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Máximo Mussini
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-12 00:00:00.000000000 Z
11
+ date: 2023-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -17,9 +17,6 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '5.1'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '8'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,23 +24,26 @@ dependencies:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '5.1'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '8'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: oj_serializers
35
29
  requirement: !ruby/object:Gem::Requirement
36
30
  requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.2
37
34
  - - "~>"
38
35
  - !ruby/object:Gem::Version
39
- version: '1.0'
36
+ version: '2.0'
40
37
  type: :runtime
41
38
  prerelease: false
42
39
  version_requirements: !ruby/object:Gem::Requirement
43
40
  requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.0.2
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.0'
46
+ version: '2.0'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: listen
49
49
  requirement: !ruby/object:Gem::Requirement