Skip to content

Commit

Permalink
Make PageType an ObjectType, fix its export
Browse files Browse the repository at this point in the history
  • Loading branch information
jaynetics committed Nov 17, 2024
1 parent a719e1f commit e46b9dd
Show file tree
Hide file tree
Showing 11 changed files with 81 additions and 61 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Fixed console spam when inspecting declarations
- Fixed resolver method not being used when rendering a Hash
- Fixed the ErrorsType template
- Fixed OpenAPI export for PageType

## [1.1.0] - 2024-11-16

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ The purpose of this is to reduce unnecessary autoloading of the whole type depen
Yes.

```ruby
# Call define_derived_type after implementing ::derive_from.
# Implement ::derive_from in your custom type.
class PreviewType < Taro::Types::Scalar::StringType
singleton_class.attr_reader :type_to_preview

Expand All @@ -235,10 +235,11 @@ class PreviewType < Taro::Types::Scalar::StringType
def coerce_response
type_to_preview.new(object).coerce_response.to_s.truncate(100)
end

define_derived_type :preview
end

# Make it available in the DSL, e.g. in an initializer.
Taro::Types::BaseType.define_derived_type :preview, 'PreviewType'

# Usage:
class MyController < ApplicationController
returns code: :ok, preview_of: 'BikeType'
Expand Down
10 changes: 7 additions & 3 deletions lib/taro/types/coercion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ def keys
@keys ||= %i[type]
end

def derived_suffix
'_of'
end

private

def validate_hash(arg)
Expand All @@ -19,7 +23,7 @@ def validate_hash(arg)

types = arg.slice(*keys)
types.size == 1 || raise(Taro::ArgumentError, <<~MSG)
Exactly one of type, array_of, or page_of must be given, got: #{types}
Exactly one of #{keys.join(', ')} must be given, got: #{types}
MSG
end

Expand All @@ -32,10 +36,10 @@ def from_hash(hash)

# DerivedTypes
# e.g. `returns array_of: 'MyType'` -> MyType.array
return from_string(value).send(key.to_s.chomp('_of'))
return from_string(value).send(key.to_s.chomp(derived_suffix))
end

raise NotImplementedError, 'Unsupported type coercion'
raise NotImplementedError, "Unsupported type coercion #{hash}"
end

def from_string(arg)
Expand Down
2 changes: 1 addition & 1 deletion lib/taro/types/list_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ def coerce_response
object.map { |el| item_type.new(el).coerce_response }
end

define_derived_type :array
define_derived_type :array, 'Taro::Types::ListType'
end
35 changes: 13 additions & 22 deletions lib/taro/types/object_types/page_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,28 @@
#
# The gem rails_cursor_pagination must be installed to use this.
#
class Taro::Types::ObjectTypes::PageType < Taro::Types::BaseType
class Taro::Types::ObjectTypes::PageType < Taro::Types::ObjectType
extend Taro::Types::Shared::ItemType

def self.derive_from(from_type)
super
field(:page, array_of: from_type.name, null: false)
field(:page_info, type: 'Taro::Types::ObjectTypes::PageInfoType', null: false)
end

def coerce_input
input_error 'PageTypes cannot be used as input types'
end

def coerce_response(after:, items_key: nil, limit: 20, order_by: nil, order: nil)
list = RailsCursorPagination::Paginator.new(
object, limit:, order_by:, order:, after:
def self.render(relation, after:, limit: 20, order_by: nil, order: nil)
result = RailsCursorPagination::Paginator.new(
relation, limit:, order_by:, order:, after:
).fetch
coerce_paginated_list(list, items_key:)
end

def coerce_paginated_list(list, items_key:)
item_type = self.class.item_type
items = list[:page].map do |item|
item_type.new(item[:data]).coerce_response
end
items_key ||= self.class.items_key

{
items_key.to_sym => items,
page_info: Taro::Types::ObjectTypes::PageInfoType.new(list[:page_info]).coerce_response,
}
end
result[:page].map! { |el| el.fetch(:data) }

# support overrides, e.g. based on item_type
def self.items_key
:page
super(result)
end

define_derived_type :page
define_derived_type :page, 'Taro::Types::ObjectTypes::PageType'
end
28 changes: 17 additions & 11 deletions lib/taro/types/shared/derived_types.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
module Taro::Types::Shared::DerivedTypes
def derived_types
@derived_types ||= {}
end

def define_derived_type(name)
type = self
# Adds `name` as a method to all type classes and adds
# `name`_of as a supported key to the Coercion module.
# When `name` is called on a type class T, it returns a new subclass
# S inheriting from `type` and passes T to S::derive_from.
def define_derived_type(name, type)
root = Taro::Types::BaseType
raise ArgumentError, "#{name} is already in use" if root.respond_to?(name)

key = :"#{name}_of"
keys = Taro::Types::Coercion.keys
raise ArgumentError, "#{key} is already in use" if keys.include?(key)
ckey = :"#{name}#{Taro::Types::Coercion.derived_suffix}"
ckeys = Taro::Types::Coercion.keys
raise ArgumentError, "#{ckey} is already in use" if ckeys.include?(ckey)

root.define_singleton_method(name) do
derived_types[type] ||= Class.new(type).tap { |t| t.derive_from(self) }
derived_types[type] ||= begin
type_class = Taro::Types::Coercion.call(type:)
Class.new(type_class).tap { |t| t.derive_from(self) }
end
end

keys << key
ckeys << ckey
end

def derived_types
@derived_types ||= {}
end
end
10 changes: 5 additions & 5 deletions lib/taro/types/shared/openapi_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ def openapi_name=(arg)
end

def default_openapi_name # rubocop:disable Metrics
if self < Taro::Types::EnumType ||
self < Taro::Types::InputType ||
self < Taro::Types::ObjectType
if self < Taro::Types::ObjectTypes::PageType
"#{item_type.openapi_name}_Page"
elsif self < Taro::Types::EnumType ||
self < Taro::Types::InputType ||
self < Taro::Types::ObjectType
name && name.chomp('Type').gsub('::', '_') ||
raise(Taro::Error, 'openapi_name must be set for anonymous type classes')
elsif self < Taro::Types::ScalarType
openapi_type
elsif self < Taro::Types::ListType
"#{item_type.openapi_name}_List"
elsif self < Taro::Types::ObjectTypes::PageType
"#{item_type.openapi_name}_Page"
else
raise NotImplementedError, 'no default_openapi_name for this type'
end
Expand Down
8 changes: 4 additions & 4 deletions lib/taro/types/shared/rendering.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# The `::render` method is intended for use in controllers.
# Special types (e.g. PageType) may accept kwargs for `#coerce_response`.
module Taro::Types::Shared::Rendering
def render(object, opts = {})
result = new(object).coerce_response(**opts)
# The `::render` method is intended for use in controllers.
# Overrides of this method must call super.
def render(object)
result = new(object).coerce_response
self.last_render = [self, result.__id__]
result
end
Expand Down
24 changes: 23 additions & 1 deletion spec/taro/export/open_api_v3_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@
)
end

it 'handles FooFormType' do
it 'handles FreeFormType' do
stub_const('FreeFormType', Class.new(T::ObjectTypes::FreeFormType))

expect(subject.extract_component_ref(FreeFormType))
Expand All @@ -264,6 +264,28 @@
)
end

it 'handles PageType' do
expect(subject.extract_component_ref(S::StringType.page))
.to eq(:$ref => "#/components/schemas/string_Page")

expect(subject.schemas).to match(
string_Page: {
type: :object,
properties:
{
page: { :$ref => "#/components/schemas/string_List" },
page_info: { :$ref => "#/components/schemas/Taro_Types_ObjectTypes_PageInfo" },
},
required: [:page, :page_info],
},
string_List: {
type: 'array',
items: { type: :string },
},
Taro_Types_ObjectTypes_PageInfo: instance_of(Hash),
)
end

it 'always marks path params as required' do
declaration = Taro::Rails::Declaration.new
declaration.add_param :id, type: 'Integer', null: true # incorrect null
Expand Down
13 changes: 4 additions & 9 deletions spec/taro/types/object_types/page_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@
end

it 'coerces response data' do
expect(example.new(relation).coerce_response(after: 'cursor'))
expect(example.render(relation, after: 'cursor'))
.to eq(page: [], page_info:)

page << { data: 'x' }
expect(example.new(relation).coerce_response(after: 'cursor'))
expect(example.render(relation, after: 'cursor'))
.to eq(page: %w[x], page_info:)
end

it 'raises for items that do not match the item type' do
page << { data: 42 }
expect do
example.new(relation).coerce_response(after: 'cursor')
example.render(relation, after: 'cursor')
end.to raise_error(
Taro::ResponseError,
'Integer is not valid as String: must be a String or Symbol'
/Integer is not valid as String: must be a String or Symbol/
)
end

Expand All @@ -49,11 +49,6 @@
expect(result).to eq(page: [], page_info:)
end

it 'can use a custom key items_key for nesting the items' do
expect(example.render(relation, after: 'cursor', items_key: 'foos'))
.to eq(foos: [], page_info:)
end

it 'raises if after kwarg is missing for ::render' do
expect { example.render(relation) }.to raise_error(/after/)
end
Expand Down
4 changes: 2 additions & 2 deletions spec/taro/types/shared/derived_types_spec.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
describe Taro::Types::Shared::DerivedTypes do
it 'raises an error if the deriving method is already in use' do
expect do
Taro::Types::BaseType.define_derived_type(:inspect)
Taro::Types::BaseType.define_derived_type(:inspect, 'String')
end.to raise_error(ArgumentError, 'inspect is already in use')
end

it 'raises an error if the coercion key is already in use' do
allow(Taro::Types::Coercion).to receive(:keys).and_return([:blob_of])
expect do
Taro::Types::BaseType.define_derived_type(:blob)
Taro::Types::BaseType.define_derived_type(:blob, 'String')
end.to raise_error(ArgumentError, 'blob_of is already in use')
end
end

0 comments on commit e46b9dd

Please sign in to comment.