Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

v0.6.4 #168

Merged
merged 13 commits into from
Jul 1, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# v0.6.4

- JSI ~> v0.8.1
- OpenAPI::Operation#each_link_page

# v0.6.3

- JSI = v0.8.0
Expand Down
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,9 @@ pet_by_id = pet_store_doc.operations['getPetById'].run(petId: pet['id'])

# unlike ResourceBase instances above, JSI instances have stricter
# equality and the pets returned from different operations are not
# equal, though the underlying JSON instance is.
# equal, because they are in different JSON documents.
pet_by_id == pet
# => false
pet_by_id.jsi_instance == pet.jsi_instance
# => true

# let's name the pet after ourself
pet.name = ENV['USER']
Expand Down Expand Up @@ -210,7 +208,7 @@ A class which subclasses Scorpio::ResourceBase directly (such as PetStore::Resou

A model representing a resource needs to be configured, minimally, with:

- the OpenAPI document for the REST API
- the OpenAPI document describing the API
- the schemas that represent instances of the model, if any

If the resource has HTTP operations associated with it (most, but not all resources will):
Expand Down
2 changes: 1 addition & 1 deletion lib/scorpio/openapi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class SemanticError < Error
autoload :OperationsScope, 'scorpio/openapi/operations_scope'

module V3
openapi_document_schema = JSI::JSONSchemaOrgDraft04.new_schema(::YAML.load_file(Scorpio.root.join(
openapi_document_schema = JSI::JSONSchemaDraft04.new_schema(::YAML.load_file(Scorpio.root.join(
'documents/github.com/OAI/OpenAPI-Specification/blob/oas3-schema/schemas/v3.0/schema.yaml'
)))

Expand Down
51 changes: 45 additions & 6 deletions lib/scorpio/openapi/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,23 +165,62 @@ def request_accessor_module
# instantiates a {Scorpio::Request} for this operation.
# parameters are all passed to {Scorpio::Request#initialize}.
# @return [Scorpio::Request]
def build_request(**config, &b)
def build_request(configuration = {}, &b)
@request_class ||= Scorpio::Request.request_class_by_operation(self)
@request_class.new(**config, &b)
@request_class.new(configuration, &b)
end

# runs a {Scorpio::Request} for this operation, returning a {Scorpio::Ur}.
# parameters are all passed to {Scorpio::Request#initialize}.
# @return [Scorpio::Ur] response ur
def run_ur(**config, &b)
build_request(**config, &b).run_ur
def run_ur(configuration = {}, &b)
build_request(configuration, &b).run_ur
end

# runs a {Scorpio::Request} for this operation - see {Scorpio::Request#run}.
# parameters are all passed to {Scorpio::Request#initialize}.
# @return response body object
def run(**config, &b)
build_request(**config, &b).run
def run(configuration = {}, &b)
build_request(configuration, &b).run
end

# Runs this operation with the given request config, and yields the resulting {Scorpio::Ur}.
# If the response contains a `Link` header with a `next` link (and that link's URL
# corresponds to this operation), this operation is run again to that link's URL, that
# request's Ur yielded, and a `next` link in that response is followed.
# This repeats until a response does not contain a `Link` header with a `next` link.
#
# @param configuration (see Scorpio::Request#initialize)
# @yield [Scorpio::Ur]
# @return [Enumerator, nil]
def each_link_page(configuration = {}, &block)
init_request = build_request(configuration)
next_page = proc do |last_page_ur|
nextlinks = last_page_ur.response.links.select { |link| link.rel?('next') }
if nextlinks.size == 0
# no next link; we are at the end
nil
elsif nextlinks.size == 1
nextlink = nextlinks.first
# we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
# we use File.join just to deal with consecutive slashes.
template = Addressable::Template.new(File.join(init_request.base_url, path_template_str))
target_uri = nextlink.absolute_target_uri
path_params = template.extract(target_uri.merge(query: nil))
unless path_params
raise("the URI of the link to the next page did not match the URI of this operation")
end
query_params = target_uri.query_values
run_ur(
path_params: path_params,
query_params: query_params,
)
else
# TODO better error class / context / message
raise("response included multiple links with rel=next")
end
end
init_request.each_page_ur(next_page: next_page, &block)
end

private
Expand Down
11 changes: 8 additions & 3 deletions lib/scorpio/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def initialize(configuration = {}, &b)
# do the Configurables first
configuration.each do |name, value|
if Configurables.public_method_defined?("#{name}=")
Configurables.instance_method("#{name}=").bind_call(self, value)
Configurables.instance_method("#{name}=").bind(self).call(value)
params_set << name
end
end
Expand Down Expand Up @@ -390,12 +390,17 @@ def run
ur.response.body_object
end

# todo make a proper iterator interface
# Runs this request, passing the resulting Ur to the given block.
# The `next_page` callable is then called with that Ur and results in the next page's Ur, or nil.
# This repeats until the `next_page` call results in nil.
#
# See {OpenAPI::Operation#each_link_page} for integration with an OpenAPI Operation.
#
# @param next_page [#call] a callable which will take a parameter `page_ur`, which is a {Scorpio::Ur},
# and must result in an Ur representing the next page, which will be yielded to the block.
# @yield [Scorpio::Ur] yields the first page, and each subsequent result of calls to `next_page` until
# that results in nil
# @return [void]
# @return [Enumerator, nil]
def each_page_ur(next_page: , raise_on_http_error: true)
return to_enum(__method__, next_page: next_page, raise_on_http_error: raise_on_http_error) unless block_given?
page_ur = run_ur
Expand Down
2 changes: 1 addition & 1 deletion lib/scorpio/resource_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def operation_for_api_method_name(name)
end

# @private
# @param name [Scorpio::OpenAPI::Operation]
# @param operation [Scorpio::OpenAPI::Operation]
# @return [String, nil]
def api_method_name_by_operation(operation)
raise(ArgumentError, operation.pretty_inspect) unless operation.is_a?(Scorpio::OpenAPI::Operation)
Expand Down
2 changes: 1 addition & 1 deletion lib/scorpio/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Scorpio
VERSION = "0.6.3".freeze
VERSION = "0.6.4".freeze
end
4 changes: 2 additions & 2 deletions scorpio.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Gem::Specification.new do |spec|

spec.require_paths = ["lib"]

spec.add_dependency "jsi", "= 0.8.0"
spec.add_dependency "ur", "~> 0.2.4"
spec.add_dependency "jsi", "~> 0.8.1"
spec.add_dependency "ur", "~> 0.2.5"
spec.add_dependency "faraday", "< 3.0"
spec.add_dependency "addressable", '~> 2.3'
end
25 changes: 24 additions & 1 deletion test/blog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,30 @@ class Blog
check_accept

articles = Blog::Article.all
format_response(200, articles.map(&:serializable_hash))
headers = {}

if request.GET['per_page']
per_page = request.GET['per_page'].to_i
page = request.GET['page'] ? request.GET['page'].to_i : 1

if page * per_page < articles.size
link = Ur::Weblink.new(
Addressable::URI.new(
path: '/v1/articles',
query_values: {
per_page: per_page,
page: page + 1,
},
),
{'rel' => 'next'},
)
headers['link'] = link.to_s
end

articles = articles[((page - 1) * per_page)...(page * per_page)]
end

format_response(200, articles.map(&:serializable_hash), headers)
end
get '/v1/articles_with_root' do
check_accept
Expand Down
6 changes: 3 additions & 3 deletions test/blog_scorpio_models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ class BlogModel < Scorpio::ResourceBase

if ENV['SCORPIO_API_DESCRIPTION_FORMAT'] == 'rest_description'
self.openapi_document = Scorpio::Google::RestDescription.new_jsi(YAML.load_file('test/blog.rest_description.yml')).to_openapi_document
self.base_url = File.join("http://localhost:#{blog_port}/", openapi_document.basePath)
self.openapi_document.base_url = File.join("http://localhost:#{blog_port}/", openapi_document.basePath)
elsif ENV['SCORPIO_API_DESCRIPTION_FORMAT'] == 'openapi2'
self.openapi_document = YAML.load_file('test/blog.openapi2.yml')
self.base_url = File.join("http://localhost:#{blog_port}/", openapi_document.basePath)
self.openapi_document.base_url = File.join("http://localhost:#{blog_port}/", openapi_document.basePath)
elsif ENV['SCORPIO_API_DESCRIPTION_FORMAT'] == 'openapi3' || ENV['SCORPIO_API_DESCRIPTION_FORMAT'].nil?
self.openapi_document = YAML.load_file('test/blog.openapi3.yml')
self.server_variables = {
self.openapi_document.server_variables = {
'scheme' => 'http',
'host' => 'localhost',
'port' => blog_port,
Expand Down
16 changes: 16 additions & 0 deletions test/each_link_page_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true
require_relative 'test_helper'

describe("each_link_page") do
it("paginates articles index") do
5.times.map { |i| Article.post('title' => "#{i + 1}!") }

index_operation = BlogModel.openapi_document.operations['articles.index']
assert_titles = proc do |titles, **param|
assert_equal(titles, index_operation.each_link_page(query_params: param).map { |ur| ur.response.body_object.map(&:title) })
end
assert_titles.([['1!', '2!'], ['3!', '4!'], ['5!']], per_page: 2)
assert_titles.([['1!', '2!', '3!', '4!', '5!']], per_page: 5)
assert_titles.([['5!']], per_page: 4, page: 2)
end
end
Loading