types_from_serializers 0.1.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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