Skip to content

Commit

Permalink
Merge pull request #16 from yogeshjain999/grape-integration
Browse files Browse the repository at this point in the history
Add grape controller module with test application
  • Loading branch information
yogeshjain999 authored Dec 21, 2022
2 parents fa8444d + fab8a7a commit e74d831
Show file tree
Hide file tree
Showing 23 changed files with 297 additions and 5 deletions.
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)
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

0 comments on commit e74d831

Please sign in to comment.