Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support Active Model Serializer #18

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_model_serializers (0.10.14)
actionpack (>= 4.1)
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activemodel (7.0.4.3)
activesupport (= 7.0.4.3)
activerecord (7.0.4.3)
Expand All @@ -36,6 +41,8 @@ GEM
awesome_print (1.9.2)
builder (3.2.4)
byebug (11.1.3)
case_transform (0.2)
activesupport
coderay (1.1.3)
concurrent-ruby (1.2.2)
crass (1.0.6)
Expand All @@ -56,6 +63,7 @@ GEM
js_from_routes (2.1.0)
railties (>= 5.1, < 8)
json (2.6.3)
jsonapi-renderer (0.2.2)
language_server-protocol (3.17.0.3)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
Expand Down Expand Up @@ -172,6 +180,7 @@ PLATFORMS
ruby

DEPENDENCIES
active_model_serializers (~> 0.10.0)
activerecord
bundler (~> 2)
debug
Expand Down
3 changes: 3 additions & 0 deletions playground/vanilla/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ gem "js_from_routes", "~> 2.0.6"
# A more efficient version of ActiveModelSerializers (https://github.com/ElMassimo/oj_serializers)
gem "oj_serializers"

# ActiveModel::Serializer implementation and Rails hooks
gem "active_model_serializers", "~> 0.10.0"

# Generate TypeScript interfaces from Ruby serializers.
gem "types_from_serializers", path: "../.."

Expand Down
11 changes: 10 additions & 1 deletion playground/vanilla/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -45,6 +45,11 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_model_serializers (0.10.14)
actionpack (>= 4.1)
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (6.0.6.1)
activesupport (= 6.0.6.1)
globalid (>= 0.3.6)
Expand All @@ -65,6 +70,8 @@ GEM
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
builder (3.2.4)
case_transform (0.2)
activesupport
concurrent-ruby (1.2.2)
crass (1.0.6)
date (3.3.3)
Expand All @@ -86,6 +93,7 @@ GEM
reline (>= 0.3.0)
js_from_routes (2.0.6)
railties (>= 5.1, < 8)
jsonapi-renderer (0.2.2)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
Expand Down Expand Up @@ -209,6 +217,7 @@ PLATFORMS
ruby

DEPENDENCIES
active_model_serializers (~> 0.10.0)
debug
inertia_rails (>= 1.2.2)
js_from_routes (~> 2.0.6)
Expand Down
3 changes: 3 additions & 0 deletions playground/vanilla/app/serializers/ams/base_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Ams::BaseSerializer < ActiveModel::Serializer
include TypesFromSerializers::Ams
end
5 changes: 5 additions & 0 deletions playground/vanilla/app/serializers/ams/composer_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Ams::ComposerSerializer < Ams::BaseSerializer
object_as :composer

attributes :id, :first_name, :last_name
end
10 changes: 10 additions & 0 deletions playground/vanilla/app/serializers/ams/song_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Ams::SongSerializer < Ams::BaseSerializer
object_as :song

attributes(
:id,
:title,
)

has_one :composer, serializer: Ams::ComposerSerializer
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "rails"
require "oj_serializers"
require "types_from_serializers"
require "active_model_serializers"
require "rspec/given"

begin
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// TypesFromSerializers CacheKey 6e82f9b05515cf2d2fd0ea698478f2de
// TypesFromSerializers CacheKey fda33f0b08d157334e88f37f595d5aa1
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
export type { default as Composer } from './Composer'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// TypesFromSerializers CacheKey 4cc2e79f18cd24f4999baf4b30ea3fef
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.

export default interface AmsComposer {
id: number
first_name?: string
last_name?: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// TypesFromSerializers CacheKey 7c65405710839d67b7e8f6a8578bfa2b
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
import type AmsComposer from './Composer'

export default interface AmsSong {
id: number
title?: string
composer: AmsComposer
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// TypesFromSerializers CacheKey fda33f0b08d157334e88f37f595d5aa1
//
// DO NOT MODIFY: This file was automatically generated by TypesFromSerializers.
export type { default as AmsComposer } from './Ams/Composer'
export type { default as AmsSong } from './Ams/Song'
85 changes: 85 additions & 0 deletions spec/types_from_serializers/ams/generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require "vanilla/config/boot"
require "vanilla/config/environment"

describe "Generator" do
let(:output_dir) { Pathname.new File.expand_path("../support/generated", __dir__) }
let(:sample_dir) { Rails.root.join("app/frontend/types/serializers") }
let(:serializers) {
%w[
Ams::ComposerSerializer
Ams::SongSerializer
]
}

def file_for(dir, name, ext)
dir.join("#{name.chomp("Serializer").gsub("::", "/")}.#{ext}")
end

def app_file_for(name, ext = "ts")
file_for(sample_dir, name, ext)
end

def output_file_for(name, ext = "ts")
file_for(output_dir, name, ext)
end

def expect_generator
expect(TypesFromSerializers)
end

def generate_serializers
receive(:serializer_interface_content).and_call_original
end

original_config = TypesFromSerializers::Config.new TypesFromSerializers.config.clone.to_h.transform_values(&:clone)

before do
TypesFromSerializers.instance_variable_set(:@config, original_config)

# Change the configuration to use a different directory.
TypesFromSerializers.config do |config|
config.base_serializers = ["Ams::BaseSerializer"]
config.output_dir = output_dir
end

output_dir.rmtree if output_dir.exist?
end

it "generates the files as expected" do
expect_generator.to generate_serializers.exactly(serializers.size).times
TypesFromSerializers.generate

# It does not generate routes that don't have `export: true`.
expect(output_file_for("AmsSerializer").exist?).to be false

# It generates one file per serializer.
serializers.each do |name|
output_file = output_file_for(name)
expect(output_file.read).to match_snapshot("interfaces_#{name.gsub("::", "__")}") # UPDATE_SNAPSHOTS="1" bin/rspec
end

# It generates an file that exports all interfaces.
index_file = output_dir.join("index.ts")
expect(index_file.exist?).to be true
expect(index_file.read).to match_snapshot("interfaces_index") # UPDATE_SNAPSHOTS="1" bin/rspec

# It does not render if generating again.
TypesFromSerializers.generate
end

it "has a rake task available" do
Rails.application.load_tasks
expect_generator.to generate_serializers.exactly(serializers.size).times
expect { Rake::Task["types_from_serializers:generate"].invoke }.not_to raise_error
end

describe "types mapping" do
it "maps citext type from SQL to string type in TypeScript" do
db_type = :citext

ts_type = TypesFromSerializers.config.sql_to_typescript_type_mapping[db_type]

expect(ts_type).to eq(:string)
end
end
end
1 change: 1 addition & 0 deletions types_from_serializers/lib/types_from_serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
require_relative "types_from_serializers/version"
require_relative "types_from_serializers/dsl"
require_relative "types_from_serializers/railtie"
require_relative "types_from_serializers/ams"
73 changes: 73 additions & 0 deletions types_from_serializers/lib/types_from_serializers/ams.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

require "active_support/concern"

# Internal: A DSL to specify types for serializer attributes.
module TypesFromSerializers
module Ams
extend ActiveSupport::Concern

module ClassMethods
# Override: Capture the name of the model related to the serializer.
#
# name - An alias for the internal object in the serializer.
# model - The name of an ActiveRecord model to infer types from the schema.
# types_from - The name of a TypeScript interface to infer types from.
def object_as(name, model: nil, types_from: nil)
# NOTE: Avoid taking memory for type information that won't be used.
if Rails.env.development?
model ||= name.is_a?(Symbol) ? name : try(:_serializer_model_name) || name
define_singleton_method(:_serializer_model_name) { model }
define_singleton_method(:_serializer_types_from) { types_from } if types_from
end
end

# Public: Shortcut for typing a serializer attribute.
#
# It specifies the type for a serializer method that will be defined
# immediately after calling this method.
def type(type, **options)
attribute type: type, **options
end

def prepare_attributes(transform_keys: nil, sort_by: nil)
attributes = _attributes_data.transform_values do |attribute|
{
value_from: attribute.name.to_s,
attribute: :method,
identifier: attribute.name == :id,
}
end

association_attributes = _reflections.transform_values do |association|
{
value_from: association.name.to_s,
association: one_association?(association) ? :one : :many,
serializer: association.options[:serializer],
identifier: association.name == :id,
}
end

attributes.merge!(association_attributes)
end

private

def one_association?(association)
# rubocop:disable Style/ClassEqualityComparison
return false if association.class.name == "ActiveModel::Serializer::HasManyReflection"
# rubocop:enable Style/ClassEqualityComparison

true
end

# Override: Remove unnecessary options in production, types are only
# used when generating code in development.
unless Rails.env.development?
def add_attribute(name, type: nil, optional: nil, **options)
super(name, **options)
end
end
end
end
end
1 change: 1 addition & 0 deletions types_from_serializers/types_from_serializers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Gem::Specification.new do |s|
s.add_development_dependency "bundler", "~> 2"
s.add_development_dependency "rake", "~> 13"
s.add_development_dependency "rspec-given", "~> 3.8"
s.add_development_dependency "active_model_serializers", "~> 0.10.0"
s.add_development_dependency "rspec-snapshot"
s.add_development_dependency "simplecov", "< 0.18"
s.add_development_dependency "standard", "~> 1.0"
Expand Down