From 49dc61da2718256e9b5f5743b5a65c4746d64c2f Mon Sep 17 00:00:00 2001 From: Tijmen Brommet Date: Fri, 23 Aug 2024 16:44:33 +0200 Subject: [PATCH] feat: infer type from enums (#20) * Extract union type from enums This adds support for enums in Rails. Defined enums will be converted to a union type with it's allowed values. * fix: ensure aliased attributes also infer types from enums --------- Co-authored-by: Maximo Mussini --- playground/vanilla/Gemfile.lock | 2 +- playground/vanilla/app/models/song.rb | 3 +++ .../vanilla/app/serializers/song_serializer.rb | 2 ++ .../20240227112250_add_enums_to_composers.rb | 6 ++++++ playground/vanilla/db/schema.rb | 5 ++++- .../__snapshots__/interfaces_SongSerializer.snap | 4 +++- .../interfaces_SongWithVideosSerializer.snap | 4 +++- .../namespace_interfaces_SongSerializer.snap | 4 +++- ...space_interfaces_SongWithVideosSerializer.snap | 4 +++- .../lib/types_from_serializers/generator.rb | 15 +++++++-------- 10 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 playground/vanilla/db/migrate/20240227112250_add_enums_to_composers.rb diff --git a/playground/vanilla/Gemfile.lock b/playground/vanilla/Gemfile.lock index cee2713..fab67be 100644 --- a/playground/vanilla/Gemfile.lock +++ b/playground/vanilla/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: ../.. specs: - types_from_serializers (2.0.2) + types_from_serializers (2.1.0) listen (~> 3.2) oj_serializers (~> 2.0, >= 2.0.2) railties (>= 5.1) diff --git a/playground/vanilla/app/models/song.rb b/playground/vanilla/app/models/song.rb index 2f4f893..41471c9 100644 --- a/playground/vanilla/app/models/song.rb +++ b/playground/vanilla/app/models/song.rb @@ -1,4 +1,7 @@ class Song < ApplicationRecord belongs_to :composer has_many :video_clips + + enum genre: { disco: "disco", rock: "rock", classical: "classical" } + enum tempo: %w[slow medium fast] end diff --git a/playground/vanilla/app/serializers/song_serializer.rb b/playground/vanilla/app/serializers/song_serializer.rb index f80e0c0..0816b92 100644 --- a/playground/vanilla/app/serializers/song_serializer.rb +++ b/playground/vanilla/app/serializers/song_serializer.rb @@ -2,6 +2,8 @@ class SongSerializer < BaseSerializer attributes( :id, :title, + :genre, + :tempo, ) has_one :composer, serializer: ComposerSerializer diff --git a/playground/vanilla/db/migrate/20240227112250_add_enums_to_composers.rb b/playground/vanilla/db/migrate/20240227112250_add_enums_to_composers.rb new file mode 100644 index 0000000..9a74243 --- /dev/null +++ b/playground/vanilla/db/migrate/20240227112250_add_enums_to_composers.rb @@ -0,0 +1,6 @@ +class AddEnumsToComposers < ActiveRecord::Migration[6.0] + def change + add_column :composers, :genre, :string, null: false + add_column :composers, :tempo, :integer, null: true + end +end diff --git a/playground/vanilla/db/schema.rb b/playground/vanilla/db/schema.rb index f63aa62..6cb2c05 100644 --- a/playground/vanilla/db/schema.rb +++ b/playground/vanilla/db/schema.rb @@ -10,12 +10,15 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_07_09_151259) do +ActiveRecord::Schema.define(version: 2024_02_27_112250) do + create_table "composers", force: :cascade do |t| t.text "first_name" t.text "last_name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.string "genre", null: false + t.integer "tempo" end create_table "songs", force: :cascade do |t| diff --git a/spec/types_from_serializers/__snapshots__/interfaces_SongSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_SongSerializer.snap index 283a9b4..28e7de8 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_SongSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_SongSerializer.snap @@ -1,4 +1,4 @@ -// TypesFromSerializers CacheKey c3af64a41d21e71dfae56644517994e1 +// TypesFromSerializers CacheKey 460b4a83a2e81c9d693ac5490f9e0b76 // // DO NOT MODIFY: This file was automatically generated by TypesFromSerializers. import type Composer from './Composer' @@ -6,5 +6,7 @@ import type Composer from './Composer' export default interface Song { id: number composer: Composer + genre: "disco" | "rock" | "classical" + tempo: "slow" | "medium" | "fast" title?: string } diff --git a/spec/types_from_serializers/__snapshots__/interfaces_SongWithVideosSerializer.snap b/spec/types_from_serializers/__snapshots__/interfaces_SongWithVideosSerializer.snap index 9ee0a0f..c587ad9 100644 --- a/spec/types_from_serializers/__snapshots__/interfaces_SongWithVideosSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/interfaces_SongWithVideosSerializer.snap @@ -1,4 +1,4 @@ -// TypesFromSerializers CacheKey 6774f7cbf07614cf9b4136fbd0c8b441 +// TypesFromSerializers CacheKey 3aa811bd4673913bbd09f2967979b304 // // DO NOT MODIFY: This file was automatically generated by TypesFromSerializers. import type Composer from './Composer' @@ -7,6 +7,8 @@ import type Video from './Video' export default interface SongWithVideos { id: number composer: Composer + genre: "disco" | "rock" | "classical" + tempo: "slow" | "medium" | "fast" title?: string videos: Video[] } diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongSerializer.snap index 2636995..294700e 100644 --- a/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongSerializer.snap @@ -1,4 +1,4 @@ -// TypesFromSerializers CacheKey c3af64a41d21e71dfae56644517994e1 +// TypesFromSerializers CacheKey 460b4a83a2e81c9d693ac5490f9e0b76 // // DO NOT MODIFY: This file was automatically generated by TypesFromSerializers. import type Composer from './Composer' @@ -8,6 +8,8 @@ declare global { interface Song { id: number composer: Composer + genre: "disco" | "rock" | "classical" + tempo: "slow" | "medium" | "fast" title?: string } } diff --git a/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongWithVideosSerializer.snap b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongWithVideosSerializer.snap index eabd0a9..2e2262f 100644 --- a/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongWithVideosSerializer.snap +++ b/spec/types_from_serializers/__snapshots__/namespace_interfaces_SongWithVideosSerializer.snap @@ -1,4 +1,4 @@ -// TypesFromSerializers CacheKey 6774f7cbf07614cf9b4136fbd0c8b441 +// TypesFromSerializers CacheKey 3aa811bd4673913bbd09f2967979b304 // // DO NOT MODIFY: This file was automatically generated by TypesFromSerializers. import type Composer from './Composer' @@ -9,6 +9,8 @@ declare global { interface SongWithVideos { id: number composer: Composer + genre: "disco" | "rock" | "classical" + tempo: "slow" | "medium" | "fast" title?: string videos: Video[] } diff --git a/types_from_serializers/lib/types_from_serializers/generator.rb b/types_from_serializers/lib/types_from_serializers/generator.rb index 9984b40..7390ad8 100644 --- a/types_from_serializers/lib/types_from_serializers/generator.rb +++ b/types_from_serializers/lib/types_from_serializers/generator.rb @@ -38,15 +38,12 @@ def ts_filename TypesFromSerializers.config.name_from_serializer.call(name).gsub("::", "/") end - # Internal: The columns corresponding to the serializer model, if it's a - # record. - def model_columns - @model_columns ||= _serializer_model_name&.to_model.try(:columns_hash) || {} - end - # Internal: The TypeScript properties of the serialzeir interface. def ts_properties @ts_properties ||= begin + model_class = _serializer_model_name&.to_model + model_columns = model_class.try(:columns_hash) || {} + model_enums = model_class.try(:defined_enums) || {} types_from = try(:_serializer_types_from) prepare_attributes( @@ -64,7 +61,7 @@ def ts_properties multi: options[:association] == :many, column_name: options.fetch(:value_from), ).tap do |property| - property.infer_type_from(model_columns, types_from) + property.infer_type_from(model_columns, model_enums, types_from) end end } @@ -190,9 +187,11 @@ def inspect # Internal: Infers the property's type by checking a corresponding SQL # column, or falling back to a TypeScript interface if provided. - def infer_type_from(columns_hash, ts_interface) + def infer_type_from(columns_hash, defined_enums, ts_interface) if type type + elsif (enum = defined_enums[column_name.to_s]) + self.type = enum.keys.map(&:inspect).join(" | ") elsif (column = columns_hash[column_name.to_s]) self.multi = true if column.try(:array) self.optional = true if column.null && !column.default