JSONAPI Resources Anchor

Quick Start

Background

jsonapi-resources-anchor enables a minimally invasive way to generate a type schema for your jsonapi-resources API.

Installation

Gemfile
gem 'jsonapi-resources-anchor'
bundle install

Generate a schema

Note that the usage of Types::String without the Anchor:: prefix is enabled via

module Types
  include Anchor::Types
end

Resources

app/resources/application_resource.rb
class ApplicationResource < JSONAPI::Resource
  abstract
  include Anchor::SchemaSerializable
end
app/resources/exhaustive_resource.rb
class ExhaustiveResource < ApplicationResource
  class AssertedObject < Types::Object
    property :a, Types::Literal.new("a")
    property "b-dash", Types::Literal.new(1)
    property :c, Types::Maybe.new(Types::String)
    property :d_optional, Types::Maybe.new(Types::String), optional: true
  end
 
  attribute :asserted_string, Types::String, description: "My asserted string."
  attribute :asserted_number, Types::Integer
  attribute :asserted_boolean, Types::Boolean
  attribute :asserted_null, Types::Null
  attribute :asserted_unknown, Types::Unknown
  attribute :asserted_object, AssertedObject
  attribute :asserted_maybe_object, Types::Maybe.new(AssertedObject)
  attribute :asserted_array_record, Types::Array.new(Types::Record.new(Types::Integer))
  attribute :asserted_union, Types::Union.new([Types::String, Types::Float])
  attribute :with_description, Types::String, description: "This is a provided description."
  attribute :inferred_unknown
 
  attribute :uuid
  attribute :string
  attribute :maybe_string
  attribute :text
  attribute :integer
  attribute :float
  attribute :decimal
  attribute :datetime
  attribute :timestamp
  attribute :time
  attribute :date
  attribute :boolean
  attribute :array_string
  attribute :maybe_array_string
  attribute :json
  attribute :jsonb
  attribute :daterange
  attribute :enum
  attribute :virtual_upcased_string
  attribute :loljk
  attribute :delegated_maybe_string, delegate: :maybe_string
  attribute :model_overridden
  attribute :resource_overridden
  attribute :with_comment
 
  class LinkSchema < Anchor::Types::Object
    property :self, Anchor::Types::String
    property :some_url, Anchor::Types::String
  end
  anchor_links_schema LinkSchema
 
  class MetaSchema < Anchor::Types::Object
    property :some_count, Anchor::Types::Integer
    property :extra_stuff, Anchor::Types::String
  end
  anchor_meta_schema MetaSchema
 
  def asserted_string = "asserted_string"
 
  def asserted_number = 1
 
  def asserted_boolean = true
 
  def asserted_null = nil
 
  def asserted_unknown = nil
 
  def asserted_object = { a: "a", "b-dash" => 1, c: nil }
 
  def asserted_maybe_object = nil
 
  def asserted_array_record = [{ key: 1 }]
 
  def asserted_union = 2
 
  def inferred_unknown = nil
 
  def resource_overridden = "resource_overridden"
 
  def with_description = "with_description"
end
config/initializers/anchor.rb
module Anchor
  configure do |c|
    c.field_case = :camel_without_inflection
    c.use_active_record_comment = true
    c.use_active_record_validations = true
    c.infer_nullable_relationships_as_optional = true
 
    c.ar_column_to_type = lambda { |column|
      return Types::Literal.new("never") if column.name == "loljk"
      Types::Inference::ActiveRecord::SQL.default_ar_column_to_type(column)
    }
 
    c.empty_relationship_type = -> { Anchor::Types::Object }
  end
end
 
JSONAPI.configure do |c|
  c.json_key_format = :camelized_key
end

Schemas

app/resources/schema.rb
class Schema < Anchor::Schema
  resource CommentResource
  resource UserResource
  resource PostResource
  resource ExhaustiveResource
 
  enum UserRoleEnum
end
lib/tasks/jsonapi.rake
namespace :jsonapi do
  desc "Generate JSONAPI::Resource Anchor schema"
  task generate: :environment do
    puts "Generating JSONAPI::Resource Anchor schema..."
 
    content = Anchor::TypeScript::SchemaGenerator(
      register: Schema.register,
      context: {},
      include_all_fields: true,
      exclude_fields: nil,
    ).call
 
    path = Rails.root.join("schema.ts")
    File.open(path, "w") { |f| f.write(content) }
    puts "✅ #{File.basename(path)}"
  end
end
rails jsonapi:generate
schema.ts
type Maybe<T> = T | null;
 
export enum UserRole {
  Admin = "admin",
  ContentCreator = "content_creator",
  External = "external",
  Guest = "guest",
  System = "system",
}
 
export type Comment = {
  id: number;
  type: "comments";
  text: string;
  createdAt: string;
  updatedAt: string;
  relationships: {
    /** Author of the comment. */
    user: User;
    deletedBy?: User;
    commentable?: Post;
  };
};
 
export type User = {
  id: number;
  type: "users";
  name: string;
  role: UserRole;
  relationships: {
    comments: Array<Comment>;
    posts: Array<Post>;
  };
};
 
export type Post = {
  id: number;
  type: "posts";
  description: string;
  relationships: {
    user: User;
    comments: Array<Comment>;
    participants: Array<User>;
  };
};
 
export type Exhaustive = {
  id: number;
  type: "exhaustives";
  /** My asserted string. */
  assertedString: string;
  assertedNumber: number;
  assertedBoolean: boolean;
  assertedNull: null;
  assertedUnknown: unknown;
  assertedObject: {
    a: "a";
    "b-dash": 1;
    c: Maybe<string>;
    d_optional?: Maybe<string>;
  };
  assertedMaybeObject: Maybe<{
    a: "a";
    "b-dash": 1;
    c: Maybe<string>;
    d_optional?: Maybe<string>;
  }>;
  assertedArrayRecord: Array<Record<string, number>>;
  assertedUnion: string | number;
  /** This is a provided description. */
  withDescription: string;
  inferredUnknown: unknown;
  uuid: string;
  string: string;
  maybeString: string;
  text: string;
  integer: number;
  float: number;
  decimal: string;
  datetime: string;
  timestamp: string;
  time: string;
  date: string;
  boolean: boolean;
  arrayString: Array<string>;
  maybeArrayString: Maybe<Array<string>>;
  json: Record<string, unknown>;
  jsonb: Record<string, unknown>;
  daterange: unknown;
  enum: unknown;
  virtualUpcasedString: Maybe<string>;
  loljk: "never";
  delegatedMaybeString: string;
  modelOverridden: unknown;
  resourceOverridden: unknown;
  /** This is a comment. */
  withComment: Maybe<string>;
  relationships: {};
  meta: {
    some_count: number;
    extra_stuff: string;
  };
  links: {
    self: string;
    some_url: string;
  };
};