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

Add grape controller module with test application #16

Merged
merged 2 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Appraisals
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
appraise 'rails-app' do
gem 'rails', '6.0.3.1'
gem 'sqlite3', '~> 1.4'
gem "trailblazer-operation"
gem "representable"
gem "trailblazer-operation", '>= 0.6.5'
gem "trailblazer-cells"
gem "cells-rails"
gem "cells-erb"
gem "jwt"
end

appraise 'grape-app' do
gem 'grape', '~> 1.5'
gem "zeitwerk", "~> 2.4"
gem "representable"
gem "trailblazer-operation", '>= 0.6.5'

gem "minitest-line", "~> 0.6"
gem "rack-test", "1.1.0"
end
10 changes: 10 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ Rake::TestTask.new(:test) do |test|
test.verbose = true
end

# To run grape app's test, run below command
# $ appraisal rails-app rake test-rails-app
Rake::TestTask.new('test-rails-app') do |test|
test.libs << 'test'
test.test_files = FileList['test/rails-app/test/test_helper.rb', 'test/rails-app/test/**/*.rb']
test.verbose = true
end

# To run grape app's test, run below command
# $ appraisal grape-app rake test-grape-app
Rake::TestTask.new('test-grape-app') do |test|
test.libs << 'test'
test.test_files = FileList['test/grape-app/test/test_helper.rb', 'test/grape-app/test/**/*.rb']
test.verbose = true
end
14 changes: 14 additions & 0 deletions gemfiles/grape_app.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This file was generated by Appraisal

source "https://rubygems.org"

gem "multi_json"
gem "minitest-line", "~> 0.6"
gem "dry-validation"
gem "grape", "~> 1.5"
gem "zeitwerk", "~> 2.4"
gem "representable"
gem "trailblazer-operation", ">= 0.6.5"
gem "rack-test", "1.1.0"

gemspec path: "../"
17 changes: 17 additions & 0 deletions gemfiles/rails_app.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file was generated by Appraisal

source "https://rubygems.org"

gem "multi_json"
gem "minitest-line"
gem "dry-validation"
gem "rails", "6.0.3.1"
gem "sqlite3", "~> 1.4"
gem "representable"
gem "trailblazer-operation", ">= 0.6.5"
gem "trailblazer-cells"
gem "cells-rails"
gem "cells-erb"
gem "jwt"

gemspec path: "../"
11 changes: 7 additions & 4 deletions lib/trailblazer/endpoint/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ def build_endpoint(name, domain_activity: name, **options)
end

module InstanceMethods
# Returns object link between compile-time and run-time config
def config_source
self.class
end

def endpoint_for(name, config_source: self.class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason for removing this config_source arg? 🌷

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, self.class needs to be different in case grape instance. So I defined a method for config_source .

See default method for rails and grape

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't you override the entire method instead, for grape.rb and change the defaulting there?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do you mean override endpoint_for itself ?

We actually need config_source in other methods too (API.endpoint and compile_options_for_controller).

def endpoint_for(name)
config_source.options_for(:endpoints, {}).fetch(name.to_s) # TODO: test non-existant endpoint
end

Expand All @@ -144,9 +148,8 @@ def invoke_endpoint_with_dsl(options, &block)
end

module API
def endpoint(name, config_source: self.class, **action_options)
endpoint = endpoint_for(name, config_source: config_source)

def endpoint(name, **action_options)
endpoint = endpoint_for(name)
action_options = {controller: self}.merge(action_options) # FIXME: redundant with {InstanceMethods#endpoint}

block_options = config_source.options_for(:options_for_block_options, **action_options)
Expand Down
28 changes: 28 additions & 0 deletions lib/trailblazer/endpoint/grape/controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Trailblazer
class Endpoint
# Grape Integration
#
module Grape
module Controller
# Make endpoint's compile time methods available in `base` and
# instance methods available in it's routes.
def self.included(base)
base.extend(Trailblazer::Endpoint::Controller)

base.helpers(
Trailblazer::Endpoint::Controller::InstanceMethods,
Trailblazer::Endpoint::Controller::InstanceMethods::API
)

base.helpers do
# Override `Controller::InstanceMethods#config_source` to return `base`
# as the link between compile-time and run-time config.
#
# @api public
define_method(:config_source, ->{ base })
end
end
end
end
end
end
5 changes: 5 additions & 0 deletions test/grape-app/app/api/app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module App
class API < Grape::API
mount V1::API => "/v1"
end
end
6 changes: 6 additions & 0 deletions test/grape-app/app/api/v1.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module V1
class API < Grape::API
format :json
mount V1::Album => "/albums"
end
end
67 changes: 67 additions & 0 deletions test/grape-app/app/api/v1/album.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module V1
class Album < V1::API
include Trailblazer::Endpoint::Grape::Controller

def self.options_for_endpoint(ctx, controller:, **)
{
request: controller.request,
errors: Trailblazer::Endpoint::Adapter::API::Errors.new,
}
end

def self.options_for_domain_ctx(ctx, controller:, **)
{
params: controller.params,
# current_user: current_user, # TODO: Access current_user
}
end

def self.options_for_block_options(ctx, controller:, **)
response_block = ->(ctx, endpoint_ctx:, **) do
controller.body json: ctx[:model]
controller.status endpoint_ctx[:status]
end

failure_block = ->(ctx, endpoint_ctx:, **) do
controller.body json: ctx[:errors].message
controller.status endpoint_ctx[:status]
end

{
success_block: response_block,
failure_block: failure_block,
protocol_failure_block: failure_block
}
end

directive :options_for_endpoint, method(:options_for_endpoint)
directive :options_for_domain_ctx, method(:options_for_domain_ctx)
directive :options_for_block_options, method(:options_for_block_options)

endpoint ::Album::Operation::Index, protocol: Application::Endpoint::Protocol, adapter: Application::Endpoint::Adapter
desc "Get list of albums"
get { endpoint ::Album::Operation::Index, representer_class: ::Album::Representer }

endpoint ::Song::Operation::Index, protocol: Application::Endpoint::Protocol, adapter: Application::Endpoint::Adapter
endpoint ::Song::Operation::Create, protocol: Application::Endpoint::Protocol, adapter: Application::Endpoint::Adapter

# FIXME: Use inheritance same as Rails's ApplicationController for maintaining global config
# Grape has anonymous class scope within resource block which doesn't copy inheritance settings
# mount ::V1::Song => ':album_id/songs'

resource ':album_id/songs' do
desc "Get list of songs"
get { endpoint ::Song::Operation::Index, representer_class: ::Song::Representer }

desc "Create a song"
post do
on_create = ->(ctx, model:, endpoint_ctx:, **) do
status 201
body json: endpoint_ctx[:representer_class].new(model).to_json
end

endpoint ::Song::Operation::Create, success_block: on_create, representer_class: ::Song::Representer
end
end
end
end
7 changes: 7 additions & 0 deletions test/grape-app/app/concepts/album/operation/index.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Album::Operation::Index < Trailblazer::Operation
step :model

def model(ctx, **)
ctx[:model] = 3.times.collect{ |i| Album.new(i) }
end
end
7 changes: 7 additions & 0 deletions test/grape-app/app/concepts/album/representer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require 'representable'

class Album::Representer < Representable::Decorator
include Representable::JSON

property :id
end
6 changes: 6 additions & 0 deletions test/grape-app/app/concepts/application/endpoint/adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Application::Endpoint
class Adapter < Trailblazer::Endpoint::Adapter::API
include Errors::Handlers
insert_error_handler_steps!(self)
end
end
14 changes: 14 additions & 0 deletions test/grape-app/app/concepts/application/endpoint/protocol.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Application::Endpoint
class Protocol < Trailblazer::Endpoint::Protocol
def authenticate(ctx, controller:, **)
username, password = Rack::Auth::Basic::Request.new(controller.env).credentials
return false if username != password

ctx[:current_user] = User.new(username)
end

def policy(ctx, current_user:, **)
current_user.username == 'admin'
end
end
end
7 changes: 7 additions & 0 deletions test/grape-app/app/concepts/song/operation/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Song::Operation::Create < Trailblazer::Operation
step :model

def model(ctx, params:, **)
ctx[:model] = Song.new(1, params.fetch(:album_id), "current_user.username") # TODO: Access current_user
end
end
7 changes: 7 additions & 0 deletions test/grape-app/app/concepts/song/operation/index.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Song::Operation::Index < Trailblazer::Operation
step :model

def model(ctx, **)
ctx[:model] = 3.times.collect{ |i| Song.new(i) }
end
end
9 changes: 9 additions & 0 deletions test/grape-app/app/concepts/song/representer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require 'representable'

class Song::Representer < Representable::Decorator
include Representable::JSON

property :id
property :album_id
property :created_by
end
2 changes: 2 additions & 0 deletions test/grape-app/app/models/album.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Album < Struct.new(:id)
end
2 changes: 2 additions & 0 deletions test/grape-app/app/models/song.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Song < Struct.new(:id, :album_id, :created_by)
end
2 changes: 2 additions & 0 deletions test/grape-app/app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class User < Struct.new(:username)
end
15 changes: 15 additions & 0 deletions test/grape-app/config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require "grape"
require "zeitwerk"

require "trailblazer/operation"
require "trailblazer/endpoint"
require "trailblazer/endpoint/grape/controller"

loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/app/api")
loader.push_dir("#{__dir__}/app/models")
loader.push_dir("#{__dir__}/app/concepts")
loader.setup

App::API.compile!
run App::API
37 changes: 37 additions & 0 deletions test/grape-app/test/album_api_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require "test_helper"

class AlbumApiTest < Minitest::Spec
include Rack::Test::Methods

def app
APP_API
end

it "not_authenticated" do
get "/v1/albums", {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong')

assert_equal last_response.status, 401
assert_equal last_response.body, "{\"json\":\"Authentication credentials were not provided or are invalid.\"}"
end

it "not_authorized" do
get "/v1/albums", {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('not_admin', 'not_admin')

assert_equal last_response.status, 403
assert_equal last_response.body, "{\"json\":\"Action not allowed due to a policy setting.\"}"
end

it "success" do
get "/v1/albums", {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin')

assert_equal last_response.status, 200
# assert_equal last_response.body, "" # TODO: Use representer
end

it "created" do
post "/v1/albums/1/songs", {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin')

assert_equal last_response.status, 201
assert_equal JSON.parse(last_response.body), {"json"=>"{\"id\":1,\"album_id\":\"1\",\"created_by\":\"current_user.username\"}"}
end
end
11 changes: 11 additions & 0 deletions test/grape-app/test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require "minitest/autorun"
require "rack/test"

config_path = File.expand_path(File.join(__FILE__, '../../config.ru'))
APP_API = Rack::Builder.parse_file(config_path).first

Minitest::Spec.class_eval do
def encode_basic_auth(username, password)
'Basic ' + Base64.encode64("#{username}:#{password}")
end
end
1 change: 1 addition & 0 deletions trailblazer-endpoint.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "rake"
spec.add_development_dependency "minitest"
spec.add_development_dependency "trailblazer-developer"
spec.add_development_dependency "appraisal"
end