From 5cf1cdd17fbe3fdc20b3f80665550312becf0733 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Wed, 31 Jul 2024 11:16:23 +0300 Subject: [PATCH] Initial implementation --- .github/workflows/main.yml | 52 ++++ .gitignore | 13 + .rspec | 3 + .standard.yml | 3 + CHANGELOG.md | 20 ++ Gemfile | 26 ++ README.md | 253 ++++++++++++++++++ Rakefile | 10 + bin/console | 11 + bin/setup | 8 + lib/tasks/generate.rake | 18 ++ lib/typelizer.rb | 51 ++++ lib/typelizer/config.rb | 75 ++++++ lib/typelizer/dsl.rb | 89 ++++++ lib/typelizer/generator.rb | 66 +++++ lib/typelizer/interface.rb | 101 +++++++ lib/typelizer/listen.rb | 72 +++++ lib/typelizer/model_plugins/active_record.rb | 34 +++ lib/typelizer/property.rb | 26 ++ lib/typelizer/railtie.rb | 35 +++ lib/typelizer/serializer_plugins/alba.rb | 120 +++++++++ lib/typelizer/serializer_plugins/ams.rb | 33 +++ lib/typelizer/serializer_plugins/auto.rb | 24 ++ lib/typelizer/serializer_plugins/base.rb | 34 +++ .../serializer_plugins/oj_serializers.rb | 33 +++ lib/typelizer/templates/fingerprint.ts.erb | 3 + lib/typelizer/templates/index.ts.erb | 3 + lib/typelizer/templates/interface.ts.erb | 26 ++ lib/typelizer/version.rb | 5 + lib/typelizer/writer.rb | 65 +++++ spec/__snapshots__/AlbaMeta.ts.snap | 16 ++ spec/__snapshots__/AlbaPost.ts.snap | 15 ++ spec/__snapshots__/AlbaUser.ts.snap | 14 + spec/__snapshots__/AlbaUserAuthor.ts.snap | 14 + .../AlbaUserSerializerFoo.ts.snap | 15 ++ spec/__snapshots__/AmsPost.ts.snap | 15 ++ spec/__snapshots__/AmsUser.ts.snap | 14 + spec/__snapshots__/AmsUserAuthor.ts.snap | 14 + .../AmsUserSerializerFoo.ts.snap | 15 ++ .../OjSerializersFlatUser.ts.snap | 14 + spec/__snapshots__/OjSerializersPost.ts.snap | 15 ++ spec/__snapshots__/OjSerializersUser.ts.snap | 14 + .../OjSerializersUserAuthor.ts.snap | 14 + .../OjSerializersUserSerializerFoo.ts.snap | 15 ++ spec/__snapshots__/index.ts.snap | 17 ++ spec/app/.dockerignore | 37 +++ spec/app/.gitattributes | 9 + spec/app/.gitignore | 35 +++ spec/app/Gemfile | 15 ++ spec/app/Gemfile.lock | 242 +++++++++++++++++ spec/app/Rakefile | 6 + .../app/controllers/application_controller.rb | 2 + spec/app/app/controllers/concerns/.keep | 0 spec/app/app/helpers/application_helper.rb | 2 + spec/app/app/models/application_record.rb | 3 + spec/app/app/models/concerns/.keep | 0 spec/app/app/models/post.rb | 5 + spec/app/app/models/user.rb | 3 + .../app/serializers/alba/base_serializer.rb | 8 + .../app/serializers/alba/meta_serializer.rb | 17 ++ .../app/serializers/alba/post_serializer.rb | 15 ++ .../alba/user/author_serializer.rb | 24 ++ .../app/serializers/alba/user_serializer.rb | 15 ++ .../app/serializers/ams/base_serializer.rb | 7 + .../app/serializers/ams/post_serializer.rb | 12 + .../serializers/ams/user/author_serializer.rb | 24 ++ .../app/serializers/ams/user_serializer.rb | 15 ++ .../oj_serializers/base_serializer.rb | 7 + .../oj_serializers/flat_user_serializer.rb | 5 + .../oj_serializers/post_serializer.rb | 12 + .../oj_serializers/user/author_serializer.rb | 24 ++ .../oj_serializers/user_serializer.rb | 15 ++ .../app/views/layouts/application.html.erb | 13 + spec/app/bin/bundle | 109 ++++++++ spec/app/bin/rails | 4 + spec/app/bin/rake | 4 + spec/app/bin/setup | 33 +++ spec/app/config.ru | 6 + spec/app/config/application.rb | 42 +++ spec/app/config/boot.rb | 3 + spec/app/config/credentials.yml.enc | 1 + spec/app/config/database.yml | 25 ++ spec/app/config/environment.rb | 5 + spec/app/config/environments/development.rb | 59 ++++ spec/app/config/environments/production.rb | 73 +++++ spec/app/config/environments/test.rb | 54 ++++ .../initializers/filter_parameter_logging.rb | 8 + spec/app/config/initializers/typelizer.rb | 7 + spec/app/config/locales/en.yml | 31 +++ spec/app/config/puma.rb | 35 +++ spec/app/config/routes.rb | 10 + .../db/migrate/20240707052900_create_users.rb | 11 + .../db/migrate/20240707052907_create_posts.rb | 13 + spec/app/db/schema.rb | 36 +++ spec/app/db/seeds.rb | 9 + spec/app/lib/assets/.keep | 0 spec/app/lib/tasks/.keep | 0 spec/app/log/.keep | 0 spec/app/public/404.html | 67 +++++ spec/app/public/422.html | 67 +++++ spec/app/public/500.html | 66 +++++ .../public/apple-touch-icon-precomposed.png | 0 spec/app/public/apple-touch-icon.png | 0 spec/app/public/favicon.ico | 0 spec/app/public/robots.txt | 1 + spec/app/storage/.keep | 0 spec/spec_helper.rb | 17 ++ spec/typelizer_spec.rb | 21 ++ typelizer.gemspec | 30 +++ 109 files changed, 2952 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 .standard.yml create mode 100644 CHANGELOG.md create mode 100644 Gemfile create mode 100644 README.md create mode 100644 Rakefile create mode 100755 bin/console create mode 100755 bin/setup create mode 100644 lib/tasks/generate.rake create mode 100644 lib/typelizer.rb create mode 100644 lib/typelizer/config.rb create mode 100644 lib/typelizer/dsl.rb create mode 100644 lib/typelizer/generator.rb create mode 100644 lib/typelizer/interface.rb create mode 100644 lib/typelizer/listen.rb create mode 100644 lib/typelizer/model_plugins/active_record.rb create mode 100644 lib/typelizer/property.rb create mode 100644 lib/typelizer/railtie.rb create mode 100644 lib/typelizer/serializer_plugins/alba.rb create mode 100644 lib/typelizer/serializer_plugins/ams.rb create mode 100644 lib/typelizer/serializer_plugins/auto.rb create mode 100644 lib/typelizer/serializer_plugins/base.rb create mode 100644 lib/typelizer/serializer_plugins/oj_serializers.rb create mode 100644 lib/typelizer/templates/fingerprint.ts.erb create mode 100644 lib/typelizer/templates/index.ts.erb create mode 100644 lib/typelizer/templates/interface.ts.erb create mode 100644 lib/typelizer/version.rb create mode 100644 lib/typelizer/writer.rb create mode 100644 spec/__snapshots__/AlbaMeta.ts.snap create mode 100644 spec/__snapshots__/AlbaPost.ts.snap create mode 100644 spec/__snapshots__/AlbaUser.ts.snap create mode 100644 spec/__snapshots__/AlbaUserAuthor.ts.snap create mode 100644 spec/__snapshots__/AlbaUserSerializerFoo.ts.snap create mode 100644 spec/__snapshots__/AmsPost.ts.snap create mode 100644 spec/__snapshots__/AmsUser.ts.snap create mode 100644 spec/__snapshots__/AmsUserAuthor.ts.snap create mode 100644 spec/__snapshots__/AmsUserSerializerFoo.ts.snap create mode 100644 spec/__snapshots__/OjSerializersFlatUser.ts.snap create mode 100644 spec/__snapshots__/OjSerializersPost.ts.snap create mode 100644 spec/__snapshots__/OjSerializersUser.ts.snap create mode 100644 spec/__snapshots__/OjSerializersUserAuthor.ts.snap create mode 100644 spec/__snapshots__/OjSerializersUserSerializerFoo.ts.snap create mode 100644 spec/__snapshots__/index.ts.snap create mode 100644 spec/app/.dockerignore create mode 100644 spec/app/.gitattributes create mode 100644 spec/app/.gitignore create mode 100644 spec/app/Gemfile create mode 100644 spec/app/Gemfile.lock create mode 100644 spec/app/Rakefile create mode 100644 spec/app/app/controllers/application_controller.rb create mode 100644 spec/app/app/controllers/concerns/.keep create mode 100644 spec/app/app/helpers/application_helper.rb create mode 100644 spec/app/app/models/application_record.rb create mode 100644 spec/app/app/models/concerns/.keep create mode 100644 spec/app/app/models/post.rb create mode 100644 spec/app/app/models/user.rb create mode 100644 spec/app/app/serializers/alba/base_serializer.rb create mode 100644 spec/app/app/serializers/alba/meta_serializer.rb create mode 100644 spec/app/app/serializers/alba/post_serializer.rb create mode 100644 spec/app/app/serializers/alba/user/author_serializer.rb create mode 100644 spec/app/app/serializers/alba/user_serializer.rb create mode 100644 spec/app/app/serializers/ams/base_serializer.rb create mode 100644 spec/app/app/serializers/ams/post_serializer.rb create mode 100644 spec/app/app/serializers/ams/user/author_serializer.rb create mode 100644 spec/app/app/serializers/ams/user_serializer.rb create mode 100644 spec/app/app/serializers/oj_serializers/base_serializer.rb create mode 100644 spec/app/app/serializers/oj_serializers/flat_user_serializer.rb create mode 100644 spec/app/app/serializers/oj_serializers/post_serializer.rb create mode 100644 spec/app/app/serializers/oj_serializers/user/author_serializer.rb create mode 100644 spec/app/app/serializers/oj_serializers/user_serializer.rb create mode 100644 spec/app/app/views/layouts/application.html.erb create mode 100755 spec/app/bin/bundle create mode 100755 spec/app/bin/rails create mode 100755 spec/app/bin/rake create mode 100755 spec/app/bin/setup create mode 100644 spec/app/config.ru create mode 100644 spec/app/config/application.rb create mode 100644 spec/app/config/boot.rb create mode 100644 spec/app/config/credentials.yml.enc create mode 100644 spec/app/config/database.yml create mode 100644 spec/app/config/environment.rb create mode 100644 spec/app/config/environments/development.rb create mode 100644 spec/app/config/environments/production.rb create mode 100644 spec/app/config/environments/test.rb create mode 100644 spec/app/config/initializers/filter_parameter_logging.rb create mode 100644 spec/app/config/initializers/typelizer.rb create mode 100644 spec/app/config/locales/en.yml create mode 100644 spec/app/config/puma.rb create mode 100644 spec/app/config/routes.rb create mode 100644 spec/app/db/migrate/20240707052900_create_users.rb create mode 100644 spec/app/db/migrate/20240707052907_create_posts.rb create mode 100644 spec/app/db/schema.rb create mode 100644 spec/app/db/seeds.rb create mode 100644 spec/app/lib/assets/.keep create mode 100644 spec/app/lib/tasks/.keep create mode 100644 spec/app/log/.keep create mode 100644 spec/app/public/404.html create mode 100644 spec/app/public/422.html create mode 100644 spec/app/public/500.html create mode 100644 spec/app/public/apple-touch-icon-precomposed.png create mode 100644 spec/app/public/apple-touch-icon.png create mode 100644 spec/app/public/favicon.ico create mode 100644 spec/app/public/robots.txt create mode 100644 spec/app/storage/.keep create mode 100644 spec/spec_helper.rb create mode 100644 spec/typelizer_spec.rb create mode 100644 typelizer.gemspec diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..2914828 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,52 @@ +name: Ruby + +on: + push: + branches: + - main + + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + name: Linter + env: + BUNDLE_JOBS: 4 + BUNDLE_RETRY: 3 + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + - name: Run StandardRB + run: bundle exec standardrb + + build: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + env: + BUNDLE_JOBS: 4 + BUNDLE_RETRY: 3 + strategy: + matrix: + ruby: + - "3.3" + - "3.2" + - "3.1" + - "3.0" + - "2.7" + - "jruby" + - "truffleruby" + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run tests + run: bundle exec rspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c249f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +# rspec failure tracking +.rspec_status + +Gemfile.lock diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.standard.yml b/.standard.yml new file mode 100644 index 0000000..2054e90 --- /dev/null +++ b/.standard.yml @@ -0,0 +1,3 @@ +# For available configuration options, see: +# https://github.com/standardrb/standard +ruby_version: 2.7 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..19ebfc4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog], +and this project adheres to [Semantic Versioning]. + +## [Unreleased] + +## [0.1.0] - 2024-08-02 + +- Initial release ([@skryukov]) + +[@skryukov]: https://github.com/skryukov + +[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/skryukov/typelizer/commits/v0.1.0 + +[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ +[Semantic Versioning]: https://semver.org/spec/v2.0.0.html diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..20a0dd6 --- /dev/null +++ b/Gemfile @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "rake", "~> 13.0" + +gem "rspec", "~> 3.0" + +gem "rspec-snapshot", "~> 2.0" + +gem "standard", "~> 1.3" + +gem "oj_serializers" + +gem "active_model_serializers" + +gem "alba" + +# Rails app +gem "rails", "~> 7.1.3" +gem "sqlite3", "~> 1.4" +gem "puma", ">= 5.0" +gem "tzinfo-data", platforms: %i[windows jruby] +gem "rspec-rails" diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d30100 --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +# Typelizer + +[![Gem Version](https://badge.fury.io/rb/typelizer.svg)](https://rubygems.org/gems/typelizer) + +Typelizer is a Ruby gem that automatically generates TypeScript interfaces from your Ruby serializers, bridging the gap between your Ruby backend and TypeScript frontend. It supports multiple serializer libraries and provides a flexible configuration system, making it easier to maintain type consistency across your full-stack application. + +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Usage](#usage) + - [Basic Setup](#basic-setup) + - [Manual Typing](#manual-typing) + - [TypeScript Integration](#typescript-integration) + - [Manual Generation](#manual-generation) + - [Automatic Generation in Development](#automatic-generation-in-development) +- [Configuration](#configuration) + - [Global Configuration](#global-configuration) + - [Config Options](#config-options) + - [Per-Serializer Configuration](#per-serializer-configuration) +- [Credits](#credits) +- [License](#license) + + +Built by Evil Martians + + +## Features + +- Automatic TypeScript interface generation +- Support for multiple serializer libraries (`Alba`, `ActiveModel::Serializer`, `Oj::Serializer`) +- File watching and automatic regeneration in development + +## Installation + +To install Typelizer, add the following line to your `Gemfile` and run `bundle install`: + +```ruby +gem "typelizer" +``` + +## Usage + +### Basic Setup + +Include the Typelizer DSL in your serializers: + +```ruby +class ApplicationResource + include Alba::Resource + include Typelizer::DSL +end + +class PostResource < ApplicationResource + attributes :id, :title, :body + + has_one :author, serializer: AuthorResource +end + +class AuthorResource < ApplicationResource + # specify the model to infer types from (optional) + typelize_from User + + attributes :id, :name +end +``` + +Typelizer will automatically generate TypeScript interfaces based on your serializer definitions using information from your models. + +### Manual Typing + +You can manually specify TypeScript types in your serializers: + +```ruby +class PostResource < ApplicationResource + attributes :id, :title, :body, :published_at + + typelize "string" + attribute :author_name do |post| + post.author.name + end +end +``` + +### TypeScript Integration + +Typelizer generates TypeScript interfaces in the specified output directory: + +```typescript +// app/javascript/types/serializers/Post.ts +export interface Post { + id: number; + title: string; + body: string; + published_at: string | null; + author_name: string; +} +``` + +All generated interfaces are automatically imported in a single file: + +```typescript +// app/javascript/types/serializers/index.ts +export * from "./post"; +export * from "./author"; +``` + +We recommend importing this file in a central location: + +```typescript +// app/javascript/types/index.ts +import "@/types/serializers"; +// Custom types can be added here +// ... +``` + +With such a setup, you can import all generated interfaces in your TypeScript files: + +```typescript +import { Post } from "@/types"; +``` + +This setup also allows you to use custom types in your serializers: + +```ruby +class PostWithMetaResource < ApplicationResource + attributes :id, :title + typelize "PostMeta" + attribute :meta do |post| + { likes: post.likes, comments: post.comments } + end +end +``` + +```typescript +// app/javascript/types/serializers/PostWithMeta.ts + +import { PostMeta } from "@/types"; + +export interface Post { + id: number; + title: string; + meta: PostMeta; +} +``` + +The `"@/types"` import path is configurable: + +```ruby +Typelizer.configure do |config| + config.types_import_path = "@/types"; +end +``` + +See the [Configuration](#configuration) section for more options. + +### Manual Generation + +To manually generate TypeScript interfaces: + +``` +$ rails typelizer:generate +``` + +### Automatic Generation in Development + +When [Listen](https://github.com/guard/listen) is installed, Typelizer automatically watches for changes and regenerates interfaces in development mode. You can disable this behavior: + +```ruby +Typelizer.listen = false +``` + +## Configuration + +### Global Configuration + +Typelizer provides several global configuration options: + +```ruby +# Directories to search for serializers: +Typelizer.dirs = [Rails.root.join("app", "resources"), Rails.root.join("app", "serializers")] +# Reject specific classes from being typelized: +Typelizer.reject_class = ->(serializer:) { false } +# Logger for debugging: +Typelizer.logger = Logger.new($stdout, level: :info) +# Force enable or disable file watching with Listen: +Typelizer.listen = nil +``` + +### Config Options + +`Typelizer::Config` offers fine-grained control over the gem's behavior. Here's a list of available options: + +```ruby +Typelizer.configure do |config| + # Determines how serializer names are mapped to TypeScript interface names + config.serializer_name_mapper = ->(serializer) { ... } + + # Maps serializers to their corresponding model classes + config.serializer_model_mapper = ->(serializer) { ... } + + # Custom transformation for generated properties + config.properties_transformer = ->(properties) { ... } + + # Plugin for model type inference (default: ModelPlugins::ActiveRecord) + config.model_plugin = Typelizer::ModelPlugins::ActiveRecord + + # Plugin for serializer parsing (default: SerializerPlugins::Auto) + config.serializer_plugin = Typelizer::SerializerPlugins::Auto + + # Additional configurations for specific plugins + config.plugin_configs = { alba: { ts_mapper: {...} } } + + # Custom DB to TypeScript type mapping + config.type_mapping = config.type_mapping.merge(jsonb: "Record", ... ) + + # Strategy for handling null values (:nullable, :optional, or :nullable_and_optional) + config.null_strategy = :nullable + + # Directory where TypeScript interfaces will be generated + config.output_dir = Rails.root.join("app/javascript/types/serializers") + + # Import path for generated types in TypeScript files + # (e.g., `import { MyType } from "@/types"`) + config.types_import_path = "@/types" + + # List of type names that should be considered global in TypeScript + # (i.e. not prefixed with the import path) + config.types_global << %w[Array Date Record File FileList] +end +``` + +### Per-Serializer Configuration + +You can also configure Typelizer on a per-serializer basis: + +```ruby +class PostResource < ApplicationResource + typelizer_config do |config| + config.type_mapping = config.type_mapping.merge(jsonb: "Record", ... ) + config.null_strategy = :nullable + # ... + end +end +``` + +## Credits + +Typelizer is inspired by [types_from_serializers](https://github.com/ElMassimo/types_from_serializers). + +## License + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..df40677 --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +require "standard/rake" + +task default: %i[spec standard] diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..385744e --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "typelizer" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/lib/tasks/generate.rake b/lib/tasks/generate.rake new file mode 100644 index 0000000..19a74f4 --- /dev/null +++ b/lib/tasks/generate.rake @@ -0,0 +1,18 @@ +namespace :typelizer do + desc "Generate TypeScript interfaces from serializers" + task generate: :environment do + require "benchmark" + + ENV["TYPELIZER"] = "true" + + puts "Generating TypeScript interfaces..." + serializers = [] + time = Benchmark.realtime do + serializers = Typelizer::Generator.call + end + + puts "Finished in #{time} seconds" + puts "Found #{serializers.size} serializers:" + puts serializers.map { |s| "\t#{s.name}" }.join("\n") + end +end diff --git a/lib/typelizer.rb b/lib/typelizer.rb new file mode 100644 index 0000000..46636cf --- /dev/null +++ b/lib/typelizer.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require_relative "typelizer/version" +require_relative "typelizer/config" +require_relative "typelizer/property" +require_relative "typelizer/interface" +require_relative "typelizer/writer" +require_relative "typelizer/generator" + +require_relative "typelizer/dsl" + +require_relative "typelizer/serializer_plugins/auto" +require_relative "typelizer/serializer_plugins/oj_serializers" +require_relative "typelizer/serializer_plugins/alba" +require_relative "typelizer/serializer_plugins/ams" + +require_relative "typelizer/model_plugins/active_record" + +require_relative "typelizer/railtie" if defined?(Rails) + +module Typelizer + class << self + def enabled? + ENV["RAILS_ENV"] == "development" || ENV["RACK_ENV"] == "development" || ENV["TYPELIZER"] == "true" + end + + attr_accessor :dirs + attr_accessor :reject_class + attr_accessor :logger + attr_accessor :listen + + # @private + attr_reader :base_classes + + def configure + yield Config + end + + private + + attr_writer :base_classes + end + + # Set in the Railtie + self.dirs = [] + self.reject_class = ->(serializer:) { false } + self.logger = Logger.new($stdout, level: :info) + self.listen = nil + + self.base_classes = Set.new +end diff --git a/lib/typelizer/config.rb b/lib/typelizer/config.rb new file mode 100644 index 0000000..4b5b3f7 --- /dev/null +++ b/lib/typelizer/config.rb @@ -0,0 +1,75 @@ +module Typelizer + TYPE_MAPPING = { + boolean: :boolean, + date: :string, + datetime: :string, + decimal: :number, + integer: :number, + string: :string, + text: :string, + citext: :string + }.tap do |types| + types.default = :unknown + end + + class Config < Struct.new( + :serializer_name_mapper, + :serializer_model_mapper, + :properties_transformer, + :model_plugin, + :serializer_plugin, + :plugin_configs, + :type_mapping, + :null_strategy, + :output_dir, + :types_import_path, + :types_global, + keyword_init: true + ) do + class << self + def instance + @instance ||= new( + serializer_name_mapper: ->(serializer) { serializer.name.ends_with?("Serializer") ? serializer.name.delete_suffix("Serializer") : serializer.name.delete_suffix("Resource") }, + serializer_model_mapper: ->(serializer) do + base_class = serializer_name_mapper.call(serializer) + Object.const_get(base_class) if Object.const_defined?(base_class) + end, + + model_plugin: ModelPlugins::ActiveRecord, + serializer_plugin: SerializerPlugins::Auto, + plugin_configs: {}, + + type_mapping: TYPE_MAPPING, + null_strategy: :nullable, + + output_dir: js_root.join("types/serializers"), + + types_import_path: "@/types", + types_global: %w[Array Date Record File FileList], + + properties_transformer: nil + ) + end + + private + + def js_root + root_path = defined?(Rails) ? Rails.root : Pathname.pwd + js_root = defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/javascript" + root_path.join(js_root) + end + + def respond_to_missing?(name, include_private = false) + Typelizer.respond_to?(name) || + instance.respond_to?(name, include_private) + end + + def method_missing(method, *args, &block) + return Typelizer.send(method, *args, &block) if Typelizer.respond_to?(method) + + instance.send(method, *args, &block) + end + end + end + end +end diff --git a/lib/typelizer/dsl.rb b/lib/typelizer/dsl.rb new file mode 100644 index 0000000..dda9742 --- /dev/null +++ b/lib/typelizer/dsl.rb @@ -0,0 +1,89 @@ +module Typelizer + module DSL + # typelize_from Model + # typelize attribute_name: ["string", "Date", optional: true, nullable: true, multi: true] + + def self.included(base) + Typelizer.base_classes << base.to_s + base.extend(ClassMethods) + end + + def self.extended(base) + Typelizer.base_classes << base.to_s + base.extend(ClassMethods) + end + + module ClassMethods + def typelizer_config + @typelizer_config ||= + begin + parent_config = superclass.respond_to?(:typelizer_config) ? superclass.typelizer_config : Config + Config.new(parent_config.to_h.transform_values(&:dup)) + end + yield @typelizer_config if block_given? + @typelizer_config + end + + def typelizer_interface + @typelizer_interface ||= Interface.new(serializer: self) + end + + # save association of serializer to model + def typelize_from(model) + return unless Typelizer.enabled? + + define_singleton_method(:_typelizer_model_name) { model } + end + + # save association of serializer attributes to type + # can be invoked multiple times + def typelize(type = nil, type_params = {}, **attributes) + if type + @keyless_type = [type, type_params.merge(attributes)] + else + assign_type_information(:_typelizer_attributes, attributes) + end + end + + attr_accessor :keyless_type + + def typelize_meta(**attributes) + assign_type_information(:_typelizer_meta_attributes, attributes) + end + + private + + def assign_type_information(attribute_name, attributes) + return unless Typelizer.enabled? + + instance_variable = "@#{attribute_name}" + + unless instance_variable_get(instance_variable) + instance_variable_set(instance_variable, {}) + end + + unless respond_to?(attribute_name) + define_singleton_method(attribute_name) do + result = instance_variable_get(instance_variable) + if superclass.respond_to?(attribute_name) + result.merge(superclass.send(attribute_name)) + else + result + end + end + end + + attributes.each do |name, attrs| + next unless name + + attrs = [attrs] if attrs && !attrs.is_a?(Array) + options = attrs.last.is_a?(Hash) ? attrs.pop : {} + + options[:type] = attrs.join(" | ") if attrs.any? + instance_variable_get(instance_variable)[name.to_sym] ||= {} + instance_variable_get(instance_variable)[name.to_sym].merge!(options) + end + end + end + end +end diff --git a/lib/typelizer/generator.rb b/lib/typelizer/generator.rb new file mode 100644 index 0000000..1b3fc5e --- /dev/null +++ b/lib/typelizer/generator.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Typelizer + class Generator + def self.call + new.call + end + + def initialize(config = Typelizer::Config) + @config = config + @writer = Writer.new + end + + attr_reader :config, :writer + + def call(force: false) + return unless Typelizer.enabled? + + read_serializers + + interfaces = target_serializers.map(&:typelizer_interface).reject(&:empty?) + writer.call(interfaces, force: force) + + interfaces + end + + private + + def target_serializers + base_classes = Typelizer.base_classes.filter_map do |base_class| + Object.const_get(base_class) if Object.const_defined?(base_class) + end.tap do |base_classes| + raise ArgumentError, "Please ensure all your serializers include Typelizer::DSL." if base_classes.none? + end + + (base_classes + base_classes.flat_map(&:descendants)).uniq.sort_by(&:name) + .reject { |serializer| Typelizer.reject_class.call(serializer: serializer) } + end + + def read_serializers(files = nil) + files ||= Typelizer.dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] } + + files.each do |file| + trace = TracePoint.new(:call) do |tp| + begin + next unless tp.self.respond_to?(:typelizer_interface) && !tp.self.send(:respond_to_missing?, :typelizer_interface, false) + rescue WeakRef::RefError + next + end + serializer_plugin = tp.self.typelizer_interface.serializer_plugin + + if tp.callee_id.in?(serializer_plugin.methods_to_typelize) + type, attrs = tp.self.keyless_type + name = tp.binding.local_variable_get(:name) if tp.binding.local_variable_defined?(:name) + tp.self.typelize(**serializer_plugin.typelize_method_transform(method: tp.callee_id, binding: tp.binding, name: name, type: type, attrs: attrs || {})) + tp.self.keyless_type = nil + end + end + + trace.enable + require file + trace.disable + end + end + end +end diff --git a/lib/typelizer/interface.rb b/lib/typelizer/interface.rb new file mode 100644 index 0000000..11d62a3 --- /dev/null +++ b/lib/typelizer/interface.rb @@ -0,0 +1,101 @@ +module Typelizer + class Interface + attr_reader :serializer, :serializer_plugin + + def config + serializer.typelizer_config + end + + def initialize(serializer:) + @serializer = serializer + @serializer_plugin = config.serializer_plugin.new(serializer: serializer, config: config) + end + + def name + config.serializer_name_mapper.call(serializer).tr_s(":", "") + end + + def filename + name.gsub("::", "/") + end + + def root_key + serializer_plugin.root_key + end + + def empty? + meta_fields.empty? && properties.empty? + end + + def meta_fields + @meta_fields ||= begin + props = serializer_plugin.meta_fields || [] + props = infer_types(props, :_typelizer_meta_attributes) + props = config.properties_transformer.call(props) if config.properties_transformer + props + end + end + + def properties + @properties ||= begin + props = serializer_plugin.properties + props = infer_types(props) + props = config.properties_transformer.call(props) if config.properties_transformer + props + end + end + + def imports + association_serializers, attribute_types = properties.filter_map(&:type) + .uniq + .partition { |type| type.is_a?(Interface) } + + serializer_types = association_serializers + .filter_map { |interface| interface.name if interface.name != name } + + custom_type_imports = attribute_types + .flat_map { |type| extract_typescript_types(type.to_s) } + .uniq + .reject { |type| global_type?(type) } + + custom_type_imports + serializer_types + end + + def inspect + "<#{self.class.name} #{name} properties=#{properties.inspect}>" + end + + private + + def extract_typescript_types(type) + type.split(/[<>\[\],\s|]+/) + end + + def global_type?(type) + type[0] == type[0].downcase || config.types_global.include?(type) + end + + def infer_types(props, hash_name = :_typelizer_attributes) + props.map do |prop| + if serializer.respond_to?(hash_name) + dsl_type = serializer.public_send(hash_name)[prop.name.to_sym] + next Property.new(prop.to_h.merge(dsl_type)) if dsl_type&.any? + end + + model_plugin.infer_types(prop) + end + end + + def model_class + return serializer._typelizer_model_name if serializer.respond_to?(:_typelizer_model_name) + + config.serializer_model_mapper.call(serializer) + rescue NameError + nil + end + + def model_plugin + @model_plugin ||= config.model_plugin.new(model_class: model_class, config: config) + end + end +end diff --git a/lib/typelizer/listen.rb b/lib/typelizer/listen.rb new file mode 100644 index 0000000..8201c93 --- /dev/null +++ b/lib/typelizer/listen.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Typelizer + module Listen + class << self + attr_accessor :started + + def call( + run_on_start: true, + options: {}, + &block + ) + return if started + return unless Typelizer.enabled? + + @block = block + @generator = Typelizer::Generator.new + + gem "listen" + require "listen" + + self.started = true + + locales_dirs = Typelizer.dirs.filter(&:exist?).map { |path| File.expand_path(path) } + + relative_paths = locales_dirs.map { |path| relative_path(path) } + + debug("Watching #{relative_paths.inspect}") + + listener(locales_dirs.map(&:to_s), options).start + @generator.call if run_on_start + end + + def relative_path(path) + root_path = defined?(Rails) ? Rails.root : Pathname.pwd + Pathname.new(path).relative_path_from(root_path).to_s + end + + def debug(message) + Typelizer.logger.debug(message) + end + + def listener(paths, options) + ::Listen.to(*paths, options) do |changed, added, removed| + changes = compute_changes(paths, changed, added, removed) + + next unless changes.any? + + debug(changes.map { |key, value| "#{key}=#{value.inspect}" }.join(", ")) + + @block.call + end + end + + def compute_changes(paths, changed, added, removed) + paths = paths.map { |path| relative_path(path) } + + { + changed: included_on_watched_paths(paths, changed), + added: included_on_watched_paths(paths, added), + removed: included_on_watched_paths(paths, removed) + }.select { |_k, v| v.any? } + end + + def included_on_watched_paths(paths, changes) + changes.map { |change| relative_path(change) }.select do |change| + paths.any? { |path| change.start_with?(path) } + end + end + end + end +end diff --git a/lib/typelizer/model_plugins/active_record.rb b/lib/typelizer/model_plugins/active_record.rb new file mode 100644 index 0000000..a653b6e --- /dev/null +++ b/lib/typelizer/model_plugins/active_record.rb @@ -0,0 +1,34 @@ +module Typelizer + module ModelPlugins + class ActiveRecord + def initialize(model_class:, config:) + @columns_hash = model_class&.columns_hash || {} + @config = config + end + + attr_reader :columns_hash, :config + + def infer_types(prop) + column = columns_hash[prop.column_name.to_s] + return prop unless column + + prop.multi = !!column.try(:array) + case config.null_strategy + when :nullable + prop.nullable = column.null + when :optional + prop.optional = column.null + when :nullable_and_optional + prop.nullable = column.null + prop.optional = column.null + else + raise "Unknown null strategy: #{config.null_strategy}" + end + + prop.type = @config.type_mapping[column.type] + + prop + end + end + end +end diff --git a/lib/typelizer/property.rb b/lib/typelizer/property.rb new file mode 100644 index 0000000..e647e96 --- /dev/null +++ b/lib/typelizer/property.rb @@ -0,0 +1,26 @@ +module Typelizer + Property = Struct.new( + :name, :type, :optional, :nullable, + :multi, :column_name, + keyword_init: true + ) do + def inspect + props = to_h.merge(type: type_name).map { |k, v| "#{k}=#{v.inspect}" }.join(" ") + "<#{self.class.name} #{props}>" + end + + def to_s + type_str = type_name + type_str = "Array<#{type_str}>" if multi + type_str = "#{type_str} | null" if nullable + + "#{name}#{"?" if optional}: #{type_str}" + end + + private + + def type_name + type.respond_to?(:name) ? type.name : type || "unknown" + end + end +end diff --git a/lib/typelizer/railtie.rb b/lib/typelizer/railtie.rb new file mode 100644 index 0000000..ff3c308 --- /dev/null +++ b/lib/typelizer/railtie.rb @@ -0,0 +1,35 @@ +module Typelizer + class Railtie < Rails::Railtie + rake_tasks do + load "tasks/generate.rake" + end + + initializer "typelizer.configure" do + Typelizer.configure do |c| + c.dirs = [ + Rails.root.join("app", "resources"), + Rails.root.join("app", "serializers") + ] + end + end + + initializer "typelizer.generate" do |app| + next unless Typelizer.enabled? + + generator = Typelizer::Generator.new + + if Typelizer.listen == true || Gem.loaded_specs["listen"] && Typelizer.listen != false + require_relative "listen" + app.config.after_initialize do + Typelizer::Listen.call do + Rails.application.reloader.reload! + end + end + end + + app.config.to_prepare do + generator.call + end + end + end +end diff --git a/lib/typelizer/serializer_plugins/alba.rb b/lib/typelizer/serializer_plugins/alba.rb new file mode 100644 index 0000000..c57d7f9 --- /dev/null +++ b/lib/typelizer/serializer_plugins/alba.rb @@ -0,0 +1,120 @@ +require_relative "base" + +module Typelizer + module SerializerPlugins + class Alba < Base + ALBA_TS_MAPPER = { + "String" => {type: :string}, + "Integer" => {type: :number}, + "Boolean" => {type: :boolean}, + "ArrayOfString" => {type: :string, multi: true}, + "ArrayOfInteger" => {type: :number, multi: true} + } + + def properties + serializer._attributes.map do |name, attr| + build_property(name, attr) + end + end + + def methods_to_typelize + [ + :association, :one, :has_one, + :many, :has_many, + :attributes, :attribute, + :nested_attribute, :nested, + :meta + ] + end + + def typelize_method_transform(method:, name:, binding:, type:, attrs:) + return {name => [type, attrs.merge(multi: true)]} if [:many, :has_many].include?(method) + + super + end + + def root_key + serializer.new({}).send(:_key) + end + + def meta_fields + return nil unless serializer._meta + + name = serializer._meta.first + [ + build_property(name, name) + ] + end + + private + + def build_property(name, attr, **options) + case attr + when Symbol + Property.new( + name: name, + type: nil, + optional: false, + nullable: false, + multi: false, + column_name: name, + **options + ) + when Proc + Property.new( + name: name, + type: nil, + optional: false, + nullable: false, + multi: false, + column_name: nil, + **options + ) + when ::Alba::Association + resource = attr.instance_variable_get(:@resource) + Property.new( + name: name, + type: Interface.new(serializer: resource), + optional: false, + nullable: false, + multi: false, # we override this in typelize_method_transform + column_name: name, + **options + ) + when ::Alba::TypedAttribute + alba_type = attr.instance_variable_get(:@type) + Property.new( + name: name, + optional: false, + # not sure if that's a good default tbh + nullable: !alba_type.instance_variable_get(:@auto_convert), + multi: false, + column_name: name, + **ts_mapper[alba_type.name.to_s], + **options + ) + when ::Alba::NestedAttribute + Property.new( + name: name, + type: nil, + optional: false, + nullable: false, + multi: false, + column_name: nil, + **options + ) + when ::Alba::ConditionalAttribute + build_property(name, attr.instance_variable_get(:@body), optional: true) + else + raise ArgumentError, "Unsupported attribute type: #{attr.class}" + end + end + + private + + def ts_mapper + config.plugin_configs.dig(:alba, :ts_mapper) || ALBA_TS_MAPPER + end + end + end +end diff --git a/lib/typelizer/serializer_plugins/ams.rb b/lib/typelizer/serializer_plugins/ams.rb new file mode 100644 index 0000000..4d58a47 --- /dev/null +++ b/lib/typelizer/serializer_plugins/ams.rb @@ -0,0 +1,33 @@ +require_relative "base" + +module Typelizer + module SerializerPlugins + class AMS < Base + def methods_to_typelize + [ + :has_many, :has_one, :belongs_to, + :attribute, :attributes + ] + end + + def typelize_method_transform(method:, name:, binding:, type:, attrs:) + return {binding.local_variable_get(:attr) => [type, attrs]} if method == :attribute + + super + end + + def properties + serializer._attributes_data.merge(serializer._reflections).flat_map do |key, association| + type = association.options[:serializer] ? Interface.new(serializer: association.options[:serializer]) : nil + Property.new( + name: key.to_s, + type: type, + optional: association.options.key?(:if) || association.options.key?(:unless), + multi: association.respond_to?(:collection?) && association.collection?, + column_name: association.name.to_s + ) + end + end + end + end +end diff --git a/lib/typelizer/serializer_plugins/auto.rb b/lib/typelizer/serializer_plugins/auto.rb new file mode 100644 index 0000000..01415de --- /dev/null +++ b/lib/typelizer/serializer_plugins/auto.rb @@ -0,0 +1,24 @@ +module Typelizer + module SerializerPlugins + module Auto + class << self + def new(serializer:, config:) + plugin(serializer).new(serializer: serializer, config: config) + end + + def plugin(serializer) + if defined?(::Oj::Serializer) && serializer.ancestors.include?(::Oj::Serializer) + OjSerializers + elsif defined?(::Alba) && serializer.ancestors.include?(::Alba::Resource) + Alba + elsif defined?(ActiveModel::Serializer) && serializer.ancestors.include?(ActiveModel::Serializer) + AMS + else + raise "Can't guess serializer plugin for #{serializer}. " \ + "Please specify it with `config.serializer_plugin`." + end + end + end + end + end +end diff --git a/lib/typelizer/serializer_plugins/base.rb b/lib/typelizer/serializer_plugins/base.rb new file mode 100644 index 0000000..0aae01a --- /dev/null +++ b/lib/typelizer/serializer_plugins/base.rb @@ -0,0 +1,34 @@ +module Typelizer + module SerializerPlugins + class Base + def initialize(serializer:, config:) + @serializer = serializer + @config = config + end + + def root_key + nil + end + + def meta_fields + nil + end + + def typelize_method_transform(method:, name:, binding:, type:, attrs:) + {name => [type, attrs]} + end + + def methods_to_typelize + [] + end + + def properties + [] + end + + private + + attr_reader :serializer, :config + end + end +end diff --git a/lib/typelizer/serializer_plugins/oj_serializers.rb b/lib/typelizer/serializer_plugins/oj_serializers.rb new file mode 100644 index 0000000..5ded466 --- /dev/null +++ b/lib/typelizer/serializer_plugins/oj_serializers.rb @@ -0,0 +1,33 @@ +require_relative "base" + +module Typelizer + module SerializerPlugins + class OjSerializers < Base + def methods_to_typelize + [ + :has_many, :has_one, :belongs_to, + :flat_one, :attribute, :attributes + ] + end + + def properties + serializer._attributes + .flat_map do |key, options| + if options[:association] == :flat + Interface.new(serializer: options.fetch(:serializer)).properties + else + type = options[:serializer] ? Interface.new(serializer: options[:serializer]) : options[:type] + Property.new( + name: key, + type: type, + optional: options[:optional] || options.key?(:if), + nullable: options[:nullable], + multi: options[:association] == :many, + column_name: options.fetch(:value_from) + ) + end + end + end + end + end +end diff --git a/lib/typelizer/templates/fingerprint.ts.erb b/lib/typelizer/templates/fingerprint.ts.erb new file mode 100644 index 0000000..d484e7e --- /dev/null +++ b/lib/typelizer/templates/fingerprint.ts.erb @@ -0,0 +1,3 @@ +// Typelizer digest <%= Digest::MD5.hexdigest(fingerprint) %> +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. diff --git a/lib/typelizer/templates/index.ts.erb b/lib/typelizer/templates/index.ts.erb new file mode 100644 index 0000000..6e368c2 --- /dev/null +++ b/lib/typelizer/templates/index.ts.erb @@ -0,0 +1,3 @@ +<%- interfaces.each do |interface| -%> +export type { default as <%= interface.name %> } from './<%= interface.filename %>' +<%- end -%> diff --git a/lib/typelizer/templates/interface.ts.erb b/lib/typelizer/templates/interface.ts.erb new file mode 100644 index 0000000..6546178 --- /dev/null +++ b/lib/typelizer/templates/interface.ts.erb @@ -0,0 +1,26 @@ +<%- if interface.imports.any? -%> +import type {<%= interface.imports.join(", ") %>} from '<%= interface.config.types_import_path %>' +<%- end -%> + +<%- if interface.root_key -%> +type <%= interface.name %>Data = { +<%- interface.properties.each do |property| -%> + <%= property %>; +<%- end -%> +} + +type <%= interface.name %> = { + <%= interface.root_key %>: <%= interface.name %>Data; +<%- interface.meta_fields&.each do |property| -%> + <%= property %>; +<%- end -%> +} +<%- else -%> +type <%= interface.name %> = { +<%- interface.properties.each do |property| -%> + <%= property %>; +<%- end -%> +} +<%- end -%> + +export default <%= interface.name %>; diff --git a/lib/typelizer/version.rb b/lib/typelizer/version.rb new file mode 100644 index 0000000..ea4a3ea --- /dev/null +++ b/lib/typelizer/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Typelizer + VERSION = "0.1.0" +end diff --git a/lib/typelizer/writer.rb b/lib/typelizer/writer.rb new file mode 100644 index 0000000..3fa43e5 --- /dev/null +++ b/lib/typelizer/writer.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "fileutils" +require "erb" + +module Typelizer + class Writer + def initialize + @template_cache = {} + @config = Config + end + + attr_reader :config, :template_cache + + def call(interfaces, force:) + cleanup_output_dir if force + + written_files = interfaces.map { |interface| write_interface(interface) } + written_files << write_index(interfaces) + + existing_files = Dir[File.join(config.output_dir, "**/*.ts")] + files_to_delete = existing_files - written_files + + File.delete(*files_to_delete) unless files_to_delete.empty? + end + + private + + def write_index(interfaces) + write_file("index.ts", interfaces.map(&:filename).join) do + render_template("index.ts.erb", interfaces: interfaces) + end + end + + def write_interface(interface) + write_file("#{interface.filename}.ts", interface.inspect) do + render_template("interface.ts.erb", interface: interface) + end + end + + def write_file(filename, fingerprint) + output_file = File.join(config.output_dir, filename) + existing_content = File.exist?(output_file) ? File.read(output_file) : nil + digest = render_template("fingerprint.ts.erb", fingerprint: fingerprint) + + return output_file if existing_content&.start_with?(digest) + + content = yield + + FileUtils.mkdir_p(File.dirname(output_file)) + + File.write(output_file, digest + content) + output_file + end + + def render_template(template, **context) + template_cache[template] ||= ERB.new(File.read(File.join(File.dirname(__FILE__), "templates/#{template}")), trim_mode: "-") + template_cache[template].result_with_hash(context) + end + + def cleanup_output_dir + FileUtils.rm_rf(config.output_dir) + end + end +end diff --git a/spec/__snapshots__/AlbaMeta.ts.snap b/spec/__snapshots__/AlbaMeta.ts.snap new file mode 100644 index 0000000..7d9ace3 --- /dev/null +++ b/spec/__snapshots__/AlbaMeta.ts.snap @@ -0,0 +1,16 @@ +// Typelizer digest 324c9ad635869485fb8231dfbed8cffe +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. + +type AlbaMetaData = { + username: string; + full_name: string | null; + address: {city: string, zipcode: string}; +} + +type AlbaMeta = { + meta: AlbaMetaData; + metadata: {foo: 'bar'}; +} + +export default AlbaMeta; diff --git a/spec/__snapshots__/AlbaPost.ts.snap b/spec/__snapshots__/AlbaPost.ts.snap new file mode 100644 index 0000000..bc55c7f --- /dev/null +++ b/spec/__snapshots__/AlbaPost.ts.snap @@ -0,0 +1,15 @@ +// Typelizer digest 37cf53e5c2445bf410d20f0dc76526d9 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {AlbaUser} from '@/types' + +type AlbaPost = { + id: number; + title?: string | null; + category?: string | null; + body?: string | null; + published_at?: string | null; + user: AlbaUser; +} + +export default AlbaPost; diff --git a/spec/__snapshots__/AlbaUser.ts.snap b/spec/__snapshots__/AlbaUser.ts.snap new file mode 100644 index 0000000..dbb8881 --- /dev/null +++ b/spec/__snapshots__/AlbaUser.ts.snap @@ -0,0 +1,14 @@ +// Typelizer digest 26603b722342e08a183535283afd6418 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {AlbaPost} from '@/types' + +type AlbaUser = { + id: number; + username: string; + active: boolean; + invitor: AlbaUser; + posts: Array; +} + +export default AlbaUser; diff --git a/spec/__snapshots__/AlbaUserAuthor.ts.snap b/spec/__snapshots__/AlbaUserAuthor.ts.snap new file mode 100644 index 0000000..6ec8281 --- /dev/null +++ b/spec/__snapshots__/AlbaUserAuthor.ts.snap @@ -0,0 +1,14 @@ +// Typelizer digest cea56a3dcb747f59b3998779acbe93f8 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {AlbaPost} from '@/types' + +type AlbaUserAuthor = { + id: number; + username: string | null; + posts?: Array; + avatar: unknown; + typed_avatar: string | null; +} + +export default AlbaUserAuthor; diff --git a/spec/__snapshots__/AlbaUserSerializerFoo.ts.snap b/spec/__snapshots__/AlbaUserSerializerFoo.ts.snap new file mode 100644 index 0000000..b93324a --- /dev/null +++ b/spec/__snapshots__/AlbaUserSerializerFoo.ts.snap @@ -0,0 +1,15 @@ +// Typelizer digest 859bc62cd130d27f4d44be0fdf719c90 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {AlbaUser, AlbaPost} from '@/types' + +type AlbaUserSerializerFoo = { + id: number; + username: string; + active: boolean; + invitor: AlbaUser; + posts: Array; + created_at: string; +} + +export default AlbaUserSerializerFoo; diff --git a/spec/__snapshots__/AmsPost.ts.snap b/spec/__snapshots__/AmsPost.ts.snap new file mode 100644 index 0000000..4fb07eb --- /dev/null +++ b/spec/__snapshots__/AmsPost.ts.snap @@ -0,0 +1,15 @@ +// Typelizer digest 38cf3a9940c0128748ccff969ac2d9ac +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {AmsUser} from '@/types' + +type AmsPost = { + id: number; + title?: string | null; + category?: string | null; + body?: string | null; + published_at?: string | null; + user: AmsUser; +} + +export default AmsPost; diff --git a/spec/__snapshots__/AmsUser.ts.snap b/spec/__snapshots__/AmsUser.ts.snap new file mode 100644 index 0000000..01a7b55 --- /dev/null +++ b/spec/__snapshots__/AmsUser.ts.snap @@ -0,0 +1,14 @@ +// Typelizer digest 83c10371506a5252b3c5eb44ff747d45 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {AmsPost} from '@/types' + +type AmsUser = { + id: number; + username: string; + active: boolean; + invitor: AmsUser; + posts: Array; +} + +export default AmsUser; diff --git a/spec/__snapshots__/AmsUserAuthor.ts.snap b/spec/__snapshots__/AmsUserAuthor.ts.snap new file mode 100644 index 0000000..73a9eb1 --- /dev/null +++ b/spec/__snapshots__/AmsUserAuthor.ts.snap @@ -0,0 +1,14 @@ +// Typelizer digest d00424b465021050da1b0cca9388df55 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {AmsPost} from '@/types' + +type AmsUserAuthor = { + id: number; + username: string | null; + avatar: unknown; + typed_avatar: string | null; + posts?: Array; +} + +export default AmsUserAuthor; diff --git a/spec/__snapshots__/AmsUserSerializerFoo.ts.snap b/spec/__snapshots__/AmsUserSerializerFoo.ts.snap new file mode 100644 index 0000000..2ada5b7 --- /dev/null +++ b/spec/__snapshots__/AmsUserSerializerFoo.ts.snap @@ -0,0 +1,15 @@ +// Typelizer digest c3e52d2a5d6865f2ee8a01c8b8d38dd2 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {AmsUser, AmsPost} from '@/types' + +type AmsUserSerializerFoo = { + id: number; + username: string; + active: boolean; + created_at: string; + invitor: AmsUser; + posts: Array; +} + +export default AmsUserSerializerFoo; diff --git a/spec/__snapshots__/OjSerializersFlatUser.ts.snap b/spec/__snapshots__/OjSerializersFlatUser.ts.snap new file mode 100644 index 0000000..ea0e8f6 --- /dev/null +++ b/spec/__snapshots__/OjSerializersFlatUser.ts.snap @@ -0,0 +1,14 @@ +// Typelizer digest d0613bfadf56720cfde6062373be13a6 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {OjSerializersUser, OjSerializersPost} from '@/types' + +type OjSerializersFlatUser = { + id: number; + username: string; + active: boolean; + invitor: OjSerializersUser; + posts: Array; +} + +export default OjSerializersFlatUser; diff --git a/spec/__snapshots__/OjSerializersPost.ts.snap b/spec/__snapshots__/OjSerializersPost.ts.snap new file mode 100644 index 0000000..1240d7f --- /dev/null +++ b/spec/__snapshots__/OjSerializersPost.ts.snap @@ -0,0 +1,15 @@ +// Typelizer digest e7ea4668b82a2d60552e8eaf7ada3d7a +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {OjSerializersUser} from '@/types' + +type OjSerializersPost = { + id: number; + title?: string | null; + category?: string | null; + body?: string | null; + published_at?: string | null; + user: OjSerializersUser; +} + +export default OjSerializersPost; diff --git a/spec/__snapshots__/OjSerializersUser.ts.snap b/spec/__snapshots__/OjSerializersUser.ts.snap new file mode 100644 index 0000000..7113343 --- /dev/null +++ b/spec/__snapshots__/OjSerializersUser.ts.snap @@ -0,0 +1,14 @@ +// Typelizer digest 63fa5a28b5147a978506b2c0e5f3f398 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {OjSerializersPost} from '@/types' + +type OjSerializersUser = { + id: number; + username: string; + active: boolean; + invitor: OjSerializersUser; + posts: Array; +} + +export default OjSerializersUser; diff --git a/spec/__snapshots__/OjSerializersUserAuthor.ts.snap b/spec/__snapshots__/OjSerializersUserAuthor.ts.snap new file mode 100644 index 0000000..2e24acd --- /dev/null +++ b/spec/__snapshots__/OjSerializersUserAuthor.ts.snap @@ -0,0 +1,14 @@ +// Typelizer digest 6c423801303540bc8b86331341f9a08c +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {OjSerializersPost} from '@/types' + +type OjSerializersUserAuthor = { + id: number; + username: string | null; + posts?: Array; + avatar: unknown; + typed_avatar: string | null; +} + +export default OjSerializersUserAuthor; diff --git a/spec/__snapshots__/OjSerializersUserSerializerFoo.ts.snap b/spec/__snapshots__/OjSerializersUserSerializerFoo.ts.snap new file mode 100644 index 0000000..1380206 --- /dev/null +++ b/spec/__snapshots__/OjSerializersUserSerializerFoo.ts.snap @@ -0,0 +1,15 @@ +// Typelizer digest 4b93ca3b7d3283f9147217b575696867 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +import type {OjSerializersUser, OjSerializersPost} from '@/types' + +type OjSerializersUserSerializerFoo = { + id: number; + username: string; + active: boolean; + invitor: OjSerializersUser; + posts: Array; + created_at: string; +} + +export default OjSerializersUserSerializerFoo; diff --git a/spec/__snapshots__/index.ts.snap b/spec/__snapshots__/index.ts.snap new file mode 100644 index 0000000..12ffd80 --- /dev/null +++ b/spec/__snapshots__/index.ts.snap @@ -0,0 +1,17 @@ +// Typelizer digest cc9502b1f9ad666caf72dabb190670a0 +// +// DO NOT MODIFY: This file was automatically generated by Typelizer. +export type { default as AlbaMeta } from './AlbaMeta' +export type { default as AlbaPost } from './AlbaPost' +export type { default as AlbaUserAuthor } from './AlbaUserAuthor' +export type { default as AlbaUser } from './AlbaUser' +export type { default as AlbaUserSerializerFoo } from './AlbaUserSerializerFoo' +export type { default as AmsPost } from './AmsPost' +export type { default as AmsUserAuthor } from './AmsUserAuthor' +export type { default as AmsUser } from './AmsUser' +export type { default as AmsUserSerializerFoo } from './AmsUserSerializerFoo' +export type { default as OjSerializersFlatUser } from './OjSerializersFlatUser' +export type { default as OjSerializersPost } from './OjSerializersPost' +export type { default as OjSerializersUserAuthor } from './OjSerializersUserAuthor' +export type { default as OjSerializersUser } from './OjSerializersUser' +export type { default as OjSerializersUserSerializerFoo } from './OjSerializersUserSerializerFoo' diff --git a/spec/app/.dockerignore b/spec/app/.dockerignore new file mode 100644 index 0000000..9612375 --- /dev/null +++ b/spec/app/.dockerignore @@ -0,0 +1,37 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets diff --git a/spec/app/.gitattributes b/spec/app/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/spec/app/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/spec/app/.gitignore b/spec/app/.gitignore new file mode 100644 index 0000000..5fb66c9 --- /dev/null +++ b/spec/app/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/spec/app/Gemfile b/spec/app/Gemfile new file mode 100644 index 0000000..8a44f89 --- /dev/null +++ b/spec/app/Gemfile @@ -0,0 +1,15 @@ +source "https://rubygems.org" + +ruby ">= 3.0" +gem "rails", "~> 7.1.3" +gem "sqlite3", "~> 1.4" +gem "puma", ">= 5.0" +gem "tzinfo-data", platforms: %i[windows jruby] + +gem "oj_serializers" +gem "alba" +gem "active_model_serializers" + +gem "typelizer", path: "../.." + +gem "rspec-rails" diff --git a/spec/app/Gemfile.lock b/spec/app/Gemfile.lock new file mode 100644 index 0000000..e509e92 --- /dev/null +++ b/spec/app/Gemfile.lock @@ -0,0 +1,242 @@ +PATH + remote: ../.. + specs: + typelizer (0.1.0) + railties (>= 6.0.0) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.3.4) + actionpack (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activesupport (= 7.1.3.4) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.3.4) + actionpack (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.3.4) + activesupport (= 7.1.3.4) + globalid (>= 0.3.6) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) + timeout (>= 0.4.0) + activestorage (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activesupport (= 7.1.3.4) + marcel (~> 1.0) + activesupport (7.1.3.4) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + base64 (0.2.0) + bigdecimal (3.1.8) + builder (3.3.0) + concurrent-ruby (1.3.3) + connection_pool (2.4.1) + crass (1.0.6) + date (3.3.4) + diff-lcs (1.5.1) + drb (2.2.1) + erubi (1.13.0) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + io-console (0.7.2) + irb (1.14.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + mini_mime (1.1.5) + minitest (5.24.1) + mutex_m (0.2.0) + net-imap (0.4.14) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.3) + nokogiri (1.16.6-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.6-arm-linux) + racc (~> 1.4) + nokogiri (1.16.6-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.6-x86-linux) + racc (~> 1.4) + nokogiri (1.16.6-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.6-x86_64-linux) + racc (~> 1.4) + oj (3.16.4) + bigdecimal (>= 3.0) + oj_serializers (2.0.3) + oj (>= 3.14.0) + psych (5.1.2) + stringio + puma (6.4.2) + nio4r (~> 2.0) + racc (1.8.0) + rack (3.1.6) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.1.3.4) + actioncable (= 7.1.3.4) + actionmailbox (= 7.1.3.4) + actionmailer (= 7.1.3.4) + actionpack (= 7.1.3.4) + actiontext (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activemodel (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + bundler (>= 1.15.0) + railties (= 7.1.3.4) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.2.1) + rdoc (6.7.0) + psych (>= 4.0.0) + reline (0.5.9) + io-console (~> 0.5) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) + sqlite3 (1.7.3-aarch64-linux) + sqlite3 (1.7.3-arm-linux) + sqlite3 (1.7.3-arm64-darwin) + sqlite3 (1.7.3-x86-linux) + sqlite3 (1.7.3-x86_64-darwin) + sqlite3 (1.7.3-x86_64-linux) + stringio (3.1.1) + thor (1.3.1) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + webrick (1.8.1) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.6.16) + +PLATFORMS + aarch64-linux + arm-linux + arm64-darwin + x86-linux + x86_64-darwin + x86_64-linux + +DEPENDENCIES + oj_serializers + puma (>= 5.0) + rails (~> 7.1.3) + rspec-rails + sqlite3 (~> 1.4) + typelizer! + tzinfo-data + +RUBY VERSION + ruby 3.2.3p157 + +BUNDLED WITH + 2.5.7 diff --git a/spec/app/Rakefile b/spec/app/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/spec/app/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/spec/app/app/controllers/application_controller.rb b/spec/app/app/controllers/application_controller.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/spec/app/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/spec/app/app/controllers/concerns/.keep b/spec/app/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/app/app/helpers/application_helper.rb b/spec/app/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/spec/app/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/spec/app/app/models/application_record.rb b/spec/app/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/spec/app/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/spec/app/app/models/concerns/.keep b/spec/app/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/app/app/models/post.rb b/spec/app/app/models/post.rb new file mode 100644 index 0000000..952858d --- /dev/null +++ b/spec/app/app/models/post.rb @@ -0,0 +1,5 @@ +class Post < ApplicationRecord + belongs_to :user + + enum category: [:news, :article, :blog].index_by(&:itself) +end diff --git a/spec/app/app/models/user.rb b/spec/app/app/models/user.rb new file mode 100644 index 0000000..4b14643 --- /dev/null +++ b/spec/app/app/models/user.rb @@ -0,0 +1,3 @@ +class User < ApplicationRecord + belongs_to :invitor, class_name: "User", optional: true +end diff --git a/spec/app/app/serializers/alba/base_serializer.rb b/spec/app/app/serializers/alba/base_serializer.rb new file mode 100644 index 0000000..2b18ff7 --- /dev/null +++ b/spec/app/app/serializers/alba/base_serializer.rb @@ -0,0 +1,8 @@ +module Alba + class BaseSerializer + include Alba::Resource + include Typelizer::DSL + + typelizer_config.null_strategy = :nullable_and_optional + end +end diff --git a/spec/app/app/serializers/alba/meta_serializer.rb b/spec/app/app/serializers/alba/meta_serializer.rb new file mode 100644 index 0000000..a7b499f --- /dev/null +++ b/spec/app/app/serializers/alba/meta_serializer.rb @@ -0,0 +1,17 @@ +module Alba + class MetaSerializer < BaseSerializer + root_key! + + attributes username: [String, true], full_name: String + + typelize address: "{city: string, zipcode: string}" + nested_attribute :address do + attributes :city, :zipcode + end + + typelize_meta metadata: "{foo: 'bar'}" + meta :metadata do + {foo: :bar} + end + end +end diff --git a/spec/app/app/serializers/alba/post_serializer.rb b/spec/app/app/serializers/alba/post_serializer.rb new file mode 100644 index 0000000..aebd497 --- /dev/null +++ b/spec/app/app/serializers/alba/post_serializer.rb @@ -0,0 +1,15 @@ +module Alba + class PostSerializer + include Alba::Resource + include Typelizer::DSL + + typelizer_config do |c| + c.null_strategy = :nullable_and_optional + c.serializer_model_mapper = ->(serializer) { Object.const_get(serializer.name.gsub("Serializer", "").gsub("Alba::", "")) } + end + + attributes :id, :title, :category, :body, :published_at + + has_one :user, serializer: UserSerializer + end +end diff --git a/spec/app/app/serializers/alba/user/author_serializer.rb b/spec/app/app/serializers/alba/user/author_serializer.rb new file mode 100644 index 0000000..1dcbdc0 --- /dev/null +++ b/spec/app/app/serializers/alba/user/author_serializer.rb @@ -0,0 +1,24 @@ +module Alba + module User + class AuthorSerializer < BaseSerializer + typelize_from ::User + + typelize username: [:string, nullable: true] + attributes :id, :username + + has_many :posts, resource: PostSerializer, if: ->(u) { u.posts.any? } + + attribute :avatar do + "https://example.com/avatar.png" if active? + end + + # typelize typed_avatar: [:string, nullable: true] + # typelize ["string", "null"] + # typelize "string | null" + typelize :string, nullable: true + attribute :typed_avatar do + "https://example.com/avatar.png" if active? + end + end + end +end diff --git a/spec/app/app/serializers/alba/user_serializer.rb b/spec/app/app/serializers/alba/user_serializer.rb new file mode 100644 index 0000000..e08234f --- /dev/null +++ b/spec/app/app/serializers/alba/user_serializer.rb @@ -0,0 +1,15 @@ +module Alba + class UserSerializer < BaseSerializer + typelize_from ::User + attributes :id, :username, :active + + has_one :invitor, resource: UserSerializer + + has_many :posts, resource: PostSerializer + + class FooSerializer < UserSerializer + typelize_from ::User + attributes :created_at + end + end +end diff --git a/spec/app/app/serializers/ams/base_serializer.rb b/spec/app/app/serializers/ams/base_serializer.rb new file mode 100644 index 0000000..7bed584 --- /dev/null +++ b/spec/app/app/serializers/ams/base_serializer.rb @@ -0,0 +1,7 @@ +module Ams + class BaseSerializer < ActiveModel::Serializer + include Typelizer::DSL + + typelizer_config.null_strategy = :nullable_and_optional + end +end diff --git a/spec/app/app/serializers/ams/post_serializer.rb b/spec/app/app/serializers/ams/post_serializer.rb new file mode 100644 index 0000000..c421ce9 --- /dev/null +++ b/spec/app/app/serializers/ams/post_serializer.rb @@ -0,0 +1,12 @@ +module Ams + class PostSerializer < ActiveModel::Serializer + include Typelizer::DSL + + typelize_from ::Post + typelizer_config.null_strategy = :nullable_and_optional + + attributes :id, :title, :category, :body, :published_at + + has_one :user, serializer: UserSerializer + end +end diff --git a/spec/app/app/serializers/ams/user/author_serializer.rb b/spec/app/app/serializers/ams/user/author_serializer.rb new file mode 100644 index 0000000..66f8b72 --- /dev/null +++ b/spec/app/app/serializers/ams/user/author_serializer.rb @@ -0,0 +1,24 @@ +module Ams + module User + class AuthorSerializer < BaseSerializer + typelize_from ::User + + typelize username: [:string, nullable: true] + attributes :id, :username + + has_many :posts, serializer: PostSerializer, if: ->(u) { u.posts.any? } + + attribute :avatar do + "https://example.com/avatar.png" if active? + end + + # typelize typed_avatar: [:string, nullable: true] + # typelize ["string", "null"] + # typelize "string | null" + typelize :string, nullable: true + attribute :typed_avatar do + "https://example.com/avatar.png" if active? + end + end + end +end diff --git a/spec/app/app/serializers/ams/user_serializer.rb b/spec/app/app/serializers/ams/user_serializer.rb new file mode 100644 index 0000000..68d78ab --- /dev/null +++ b/spec/app/app/serializers/ams/user_serializer.rb @@ -0,0 +1,15 @@ +module Ams + class UserSerializer < BaseSerializer + typelize_from ::User + attributes :id, :username, :active + + has_one :invitor, serializer: UserSerializer + + has_many :posts, serializer: PostSerializer + + class FooSerializer < UserSerializer + typelize_from ::User + attributes :created_at + end + end +end diff --git a/spec/app/app/serializers/oj_serializers/base_serializer.rb b/spec/app/app/serializers/oj_serializers/base_serializer.rb new file mode 100644 index 0000000..c783d82 --- /dev/null +++ b/spec/app/app/serializers/oj_serializers/base_serializer.rb @@ -0,0 +1,7 @@ +module OjSerializers + class BaseSerializer < Oj::Serializer + include Typelizer::DSL + + typelizer_config.null_strategy = :nullable_and_optional + end +end diff --git a/spec/app/app/serializers/oj_serializers/flat_user_serializer.rb b/spec/app/app/serializers/oj_serializers/flat_user_serializer.rb new file mode 100644 index 0000000..ca35e87 --- /dev/null +++ b/spec/app/app/serializers/oj_serializers/flat_user_serializer.rb @@ -0,0 +1,5 @@ +module OjSerializers + class FlatUserSerializer < BaseSerializer + flat_one :invitor, serializer: UserSerializer + end +end diff --git a/spec/app/app/serializers/oj_serializers/post_serializer.rb b/spec/app/app/serializers/oj_serializers/post_serializer.rb new file mode 100644 index 0000000..0111888 --- /dev/null +++ b/spec/app/app/serializers/oj_serializers/post_serializer.rb @@ -0,0 +1,12 @@ +module OjSerializers + class PostSerializer < Oj::Serializer + include Typelizer::DSL + + typelize_from ::Post + typelizer_config.null_strategy = :nullable_and_optional + + attributes :id, :title, :category, :body, :published_at + + has_one :user, serializer: UserSerializer + end +end diff --git a/spec/app/app/serializers/oj_serializers/user/author_serializer.rb b/spec/app/app/serializers/oj_serializers/user/author_serializer.rb new file mode 100644 index 0000000..581fb83 --- /dev/null +++ b/spec/app/app/serializers/oj_serializers/user/author_serializer.rb @@ -0,0 +1,24 @@ +module OjSerializers + module User + class AuthorSerializer < BaseSerializer + typelize_from ::User + + typelize username: [:string, nullable: true] + attributes :id, :username + + has_many :posts, serializer: PostSerializer, if: ->(u) { u.posts.any? } + + attribute :avatar do + "https://example.com/avatar.png" if active? + end + + # typelize typed_avatar: [:string, nullable: true] + # typelize ["string", "null"] + # typelize "string | null" + typelize :string, nullable: true + attribute :typed_avatar do + "https://example.com/avatar.png" if active? + end + end + end +end diff --git a/spec/app/app/serializers/oj_serializers/user_serializer.rb b/spec/app/app/serializers/oj_serializers/user_serializer.rb new file mode 100644 index 0000000..36d0359 --- /dev/null +++ b/spec/app/app/serializers/oj_serializers/user_serializer.rb @@ -0,0 +1,15 @@ +module OjSerializers + class UserSerializer < BaseSerializer + typelize_from ::User + attributes :id, :username, :active + + has_one :invitor, serializer: UserSerializer + + has_many :posts, serializer: PostSerializer + + class FooSerializer < UserSerializer + typelize_from ::User + attributes :created_at + end + end +end diff --git a/spec/app/app/views/layouts/application.html.erb b/spec/app/app/views/layouts/application.html.erb new file mode 100644 index 0000000..99d11f6 --- /dev/null +++ b/spec/app/app/views/layouts/application.html.erb @@ -0,0 +1,13 @@ + + + + App + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + + + <%= yield %> + + diff --git a/spec/app/bin/bundle b/spec/app/bin/bundle new file mode 100755 index 0000000..a0f692a --- /dev/null +++ b/spec/app/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/o + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/o + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/spec/app/bin/rails b/spec/app/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/spec/app/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/spec/app/bin/rake b/spec/app/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/spec/app/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/spec/app/bin/setup b/spec/app/bin/setup new file mode 100755 index 0000000..3cd5a9d --- /dev/null +++ b/spec/app/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" +end diff --git a/spec/app/config.ru b/spec/app/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/spec/app/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/spec/app/config/application.rb b/spec/app/config/application.rb new file mode 100644 index 0000000..06e9b3d --- /dev/null +++ b/spec/app/config/application.rb @@ -0,0 +1,42 @@ +require_relative "boot" + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +# require "active_job/railtie" +require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +# require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +# require "action_cable/engine" +require "rails/test_unit/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module App + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 7.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + # Don't generate system test files. + config.generators.system_tests = nil + end +end diff --git a/spec/app/config/boot.rb b/spec/app/config/boot.rb new file mode 100644 index 0000000..2820116 --- /dev/null +++ b/spec/app/config/boot.rb @@ -0,0 +1,3 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. diff --git a/spec/app/config/credentials.yml.enc b/spec/app/config/credentials.yml.enc new file mode 100644 index 0000000..b57fd9b --- /dev/null +++ b/spec/app/config/credentials.yml.enc @@ -0,0 +1 @@ +4W3soEgJJAVA8/VcfLaQwQKDH8kfz/hAYvFDhl20S6OTLiRCXTiPdFqFDCJ/fMin1w8uQy7PwlTCwyQhXmDWtzTrxZS/jH/3tVff0/najMBiA9WPEO3vop3J1XNHZM+3XgdqpMwQkg3guQC9OV+7saYsyTQ0AfHYTrOO499qXuMD/ylVcTSzlC4jKP86SGvD+rZdUywurtZNizGXAEPzft8zD8YptKF/mNnZb5UQJrHs8eC1pnJcOqoxWCu0ttBnM90kxrSa0aHTWavDOEl4we8IT4QUOAUxyHd+hJRE1SWVPuJLduurqHednwczwHBlVP5tE8xQh+tKKPFI+MnVfpXIaP1VDC7ATju5/K/jXaj4wiFcSwo7Y+ldTsZ5n0gYFP4WXNgtRVzXgX1PFCvCCqcMkQ/G--eZs4YzJX5V5tHt/U--OQ18/wST65IpSfmWBeaYvA== \ No newline at end of file diff --git a/spec/app/config/database.yml b/spec/app/config/database.yml new file mode 100644 index 0000000..796466b --- /dev/null +++ b/spec/app/config/database.yml @@ -0,0 +1,25 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + +production: + <<: *default + database: storage/production.sqlite3 diff --git a/spec/app/config/environment.rb b/spec/app/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/spec/app/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/spec/app/config/environments/development.rb b/spec/app/config/environments/development.rb new file mode 100644 index 0000000..44fb670 --- /dev/null +++ b/spec/app/config/environments/development.rb @@ -0,0 +1,59 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/spec/app/config/environments/production.rb b/spec/app/config/environments/production.rb new file mode 100644 index 0000000..918a051 --- /dev/null +++ b/spec/app/config/environments/production.rb @@ -0,0 +1,73 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new($stdout) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [:request_id] + + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/spec/app/config/environments/test.rb b/spec/app/config/environments/test.rb new file mode 100644 index 0000000..d349c35 --- /dev/null +++ b/spec/app/config/environments/test.rb @@ -0,0 +1,54 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/spec/app/config/initializers/filter_parameter_logging.rb b/spec/app/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c2d89e2 --- /dev/null +++ b/spec/app/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/spec/app/config/initializers/typelizer.rb b/spec/app/config/initializers/typelizer.rb new file mode 100644 index 0000000..ec55ab6 --- /dev/null +++ b/spec/app/config/initializers/typelizer.rb @@ -0,0 +1,7 @@ +Typelizer.configure do |c| + c.dirs = [ + Rails.root.join("app", "serializers") + ] + + c.types_global = %w[Array Date Record] +end diff --git a/spec/app/config/locales/en.yml b/spec/app/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/spec/app/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/spec/app/config/puma.rb b/spec/app/config/puma.rb new file mode 100644 index 0000000..afa809b --- /dev/null +++ b/spec/app/config/puma.rb @@ -0,0 +1,35 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies that the worker count should equal the number of processors in production. +if ENV["RAILS_ENV"] == "production" + require "concurrent-ruby" + worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) + workers worker_count if worker_count > 1 +end + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart diff --git a/spec/app/config/routes.rb b/spec/app/config/routes.rb new file mode 100644 index 0000000..9f768e5 --- /dev/null +++ b/spec/app/config/routes.rb @@ -0,0 +1,10 @@ +Rails.application.routes.draw do + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", :as => :rails_health_check + + # Defines the root path route ("/") + # root "posts#index" +end diff --git a/spec/app/db/migrate/20240707052900_create_users.rb b/spec/app/db/migrate/20240707052900_create_users.rb new file mode 100644 index 0000000..b7d05f4 --- /dev/null +++ b/spec/app/db/migrate/20240707052900_create_users.rb @@ -0,0 +1,11 @@ +class CreateUsers < ActiveRecord::Migration[7.1] + def change + create_table :users do |t| + t.string :username, null: false + t.boolean :active, null: false, default: false + t.references :invitor, foreign_key: {to_table: "users"} + + t.timestamps + end + end +end diff --git a/spec/app/db/migrate/20240707052907_create_posts.rb b/spec/app/db/migrate/20240707052907_create_posts.rb new file mode 100644 index 0000000..1617ecb --- /dev/null +++ b/spec/app/db/migrate/20240707052907_create_posts.rb @@ -0,0 +1,13 @@ +class CreatePosts < ActiveRecord::Migration[7.1] + def change + create_table :posts do |t| + t.string :title + t.string :category + t.text :body + t.datetime :published_at + t.references :user, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/spec/app/db/schema.rb b/spec/app/db/schema.rb new file mode 100644 index 0000000..4464a20 --- /dev/null +++ b/spec/app/db/schema.rb @@ -0,0 +1,36 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.1].define(version: 2024_07_07_052907) do + create_table "posts", force: :cascade do |t| + t.string "title" + t.string "category" + t.text "body" + t.datetime "published_at" + t.integer "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_posts_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.string "username", null: false + t.boolean "active", default: false, null: false + t.integer "invitor_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["invitor_id"], name: "index_users_on_invitor_id" + end + + add_foreign_key "posts", "users" + add_foreign_key "users", "users", column: "invitor_id" +end diff --git a/spec/app/db/seeds.rb b/spec/app/db/seeds.rb new file mode 100644 index 0000000..4fbd6ed --- /dev/null +++ b/spec/app/db/seeds.rb @@ -0,0 +1,9 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/spec/app/lib/assets/.keep b/spec/app/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/app/lib/tasks/.keep b/spec/app/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/app/log/.keep b/spec/app/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/app/public/404.html b/spec/app/public/404.html new file mode 100644 index 0000000..2be3af2 --- /dev/null +++ b/spec/app/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/spec/app/public/422.html b/spec/app/public/422.html new file mode 100644 index 0000000..c08eac0 --- /dev/null +++ b/spec/app/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/spec/app/public/500.html b/spec/app/public/500.html new file mode 100644 index 0000000..78a030a --- /dev/null +++ b/spec/app/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/spec/app/public/apple-touch-icon-precomposed.png b/spec/app/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..e69de29 diff --git a/spec/app/public/apple-touch-icon.png b/spec/app/public/apple-touch-icon.png new file mode 100644 index 0000000..e69de29 diff --git a/spec/app/public/favicon.ico b/spec/app/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/spec/app/public/robots.txt b/spec/app/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/spec/app/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/spec/app/storage/.keep b/spec/app/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..3bd4c3a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +ENV["TYPELIZER"] = "true" +ENV["RAILS_ENV"] = "test" +require File.expand_path("app/config/environment", __dir__) + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/spec/typelizer_spec.rb b/spec/typelizer_spec.rb new file mode 100644 index 0000000..c9fd435 --- /dev/null +++ b/spec/typelizer_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.describe Typelizer do + let(:config) { Typelizer::Config } + + around(:each) do |example| + FileUtils.rmtree(config.output_dir) + example.run + FileUtils.rmtree(config.output_dir) + end + + it "has a rake task available", aggregate_failures: true do + Rails.application.load_tasks + expect { Rake::Task["typelizer:generate"].invoke }.not_to raise_error + + # check all generated files are equal to the snapshots + config.output_dir.glob("**/*.ts").each do |file| + expect(file.read).to match_snapshot(file.basename) + end + end +end diff --git a/typelizer.gemspec b/typelizer.gemspec new file mode 100644 index 0000000..581ecb8 --- /dev/null +++ b/typelizer.gemspec @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative "lib/typelizer/version" + +Gem::Specification.new do |spec| + spec.name = "typelizer" + spec.version = Typelizer::VERSION + spec.authors = ["Svyatoslav Kryukov"] + spec.email = ["me@skryukov.dev"] + + spec.summary = "A TypeScript type generator for Ruby serializers." + spec.description = "A TypeScript type generator for Ruby serializers." + spec.homepage = "https://github.com/skryukov/typelizer" + spec.license = "MIT" + spec.required_ruby_version = ">= 2.7.0" + + spec.metadata = { + "bug_tracker_uri" => "#{spec.homepage}/issues", + "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md", + "documentation_uri" => "#{spec.homepage}/blob/main/README.md", + "homepage_uri" => spec.homepage, + "source_code_uri" => spec.homepage, + "rubygems_mfa_required" => "true" + } + + spec.files = Dir["{app,lib}/**/*", "CHANGELOG.md", "LICENSE.txt", "README.md"] + spec.require_paths = ["lib"] + + spec.add_dependency "railties", ">= 6.0.0" +end