I'm running into difficulty with STI models and their corresponding resources.
I have a base model Event, with subclasses Events::Applied, Events::Interview (amongst others) and I'm not sure how to set up the resources. Currently I have an Api::V1::EventResource and Api::V1::Events::AppliedResource and Api::V1::Events::InterviewResource (both of which subclass the EventResource, however I'm getting an error NameError (JSONAPI: Could not find resource 'events/applied'. (Class Events::AppliedResource not found)) - it appears that my base namespace Api::V1 isn't being taken into account?
I'm also not sure how to handle model inheritance as a whole when it comes to JSONAPI::Resources. I'd like to have the base resource for index actions (immutable) and the subclass resources for create/updates (as well as index/show). Does this fit the pattern correctly? I'm certainly open to suggestions.
@adambedford I tried to do this and it works)
here is an example
begin
require 'bundler/inline'
require 'bundler'
rescue LoadError => e
STDERR.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
raise e
end
gemfile(true, ui: ENV['SILENT'] ? Bundler::UI::Silent.new : Bundler::UI::Shell.new) do
source 'https://rubygems.org'
gem 'rails', require: false
if ENV['JSONAPI_RESOURCES_PG']
gem 'pg'
else
gem 'sqlite3', platform: :mri
gem 'activerecord-jdbcsqlite3-adapter',
git: 'https://github.com/jruby/activerecord-jdbc-adapter',
branch: 'rails-5',
platform: :jruby
end
if ENV['JSONAPI_RESOURCES_PATH']
gem 'jsonapi-resources', path: ENV['JSONAPI_RESOURCES_PATH'], require: false
else
gem 'jsonapi-resources', git: 'https://github.com/cerebris/jsonapi-resources', require: false
end
end
# prepare active_record database
require 'active_record'
class NullLogger < Logger
def initialize(*_args)
end
def add(*_args, &_block)
end
end
if ENV['JSONAPI_RESOURCES_PG']
db_name = ENV['JSONAPI_RESOURCES_PG']
system("psql -c 'DROP DATABASE IF EXISTS #{db_name};'")
system("psql -c 'CREATE DATABASE #{db_name};'")
ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: db_name)
else
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
end
ActiveRecord::Base.logger = ENV['SILENT'] ? NullLogger.new : Logger.new(STDOUT)
ActiveRecord::Migration.verbose = !ENV['SILENT']
ActiveRecord::Schema.define do
# Add your schema here
create_table :reviews, force: true do |t|
t.string :type
t.string :content
end
end
# create models
class Review < ActiveRecord::Base
validates :type, inclusion: { in: ['Review::Author', 'Review::Post'] }
end
class Review::Author < Review
end
class Review::Post < Review
end
# prepare rails app
require 'action_controller/railtie'
# require 'action_view/railtie'
require 'jsonapi-resources'
class ApplicationController < ActionController::Base
end
# prepare jsonapi resources and controllers
class ReviewsController < ApplicationController
include JSONAPI::ActsAsResourceController
end
class ReviewAuthorsController < ApplicationController
include JSONAPI::ActsAsResourceController
end
class ReviewPostsController < ApplicationController
include JSONAPI::ActsAsResourceController
end
class ReviewResource < JSONAPI::Resource
model_name 'Review'
model_hint model: Review::Author, resource: self
model_hint model: Review::Post, resource: self
attributes :content, :type
end
class ReviewAuthorResource < JSONAPI::Resource
model_name 'Review::Author'
attribute :content
end
class ReviewPostResource < JSONAPI::Resource
model_name 'Review::Post'
attribute :content
end
class TestApp < Rails::Application
config.root = File.dirname(__FILE__)
config.logger = ENV['SILENT'] ? NullLogger.new : Logger.new(STDOUT)
Rails.logger = config.logger
secrets.secret_token = 'secret_token'
secrets.secret_key_base = 'secret_key_base'
config.eager_load = false
end
# initialize app
Rails.application.initialize!
JSONAPI.configure do |config|
config.json_key_format = :underscored_key
config.route_format = :underscored_key
end
# draw routes
Rails.application.routes.draw do
jsonapi_resources :review_authors, except: :index
jsonapi_resources :review_posts, except: :index
jsonapi_resources :reviews, only: :index
end
# prepare tests
require 'minitest/autorun'
require 'rack/test'
# Replace this with the code necessary to make your test fail.
class BugTest < Minitest::Test
include Rack::Test::Methods
def setup
Review.delete_all
@author = Review::Author.create! content: 'author 1'
@post = Review::Post.create! content: 'post 1'
end
def teardown
puts last_response_json if last_response.body.present?
rescue StandardError => e
puts "Exception caught in teardown\n<#{e.class}>: #{e.message}"
end
def test_fetch_reviews
get '/reviews', nil, json_api_headers
assert last_response.ok?
assert_equal 2, last_response_json[:data].size
author_data = last_response_json[:data].detect { |r| r[:id] == @author.id.to_s }
assert_equal 'reviews', author_data[:type]
expected_author_attributes = { content: @author.content, type: @author.class.to_s }
assert_equal expected_author_attributes, author_data[:attributes]
post_data = last_response_json[:data].detect { |r| r[:id] == @post.id.to_s }
assert_equal 'reviews', post_data[:type]
expected_post_attributes = { content: @post.content, type: @post.class.to_s }
assert_equal expected_post_attributes, post_data[:attributes]
end
def test_fetch_review_author
get "/review_authors/#{@author.id}", nil, json_api_headers
assert last_response.ok?
assert_equal @author.id.to_s, last_response_json[:data][:id]
assert_equal 'review_authors', last_response_json[:data][:type]
expected_attributes = { content: @author.content }
assert_equal expected_attributes, last_response_json[:data][:attributes]
end
def test_create_review_author
request_body = {
data: {
type: 'review_authors',
attributes: {
content: 'author 2'
}
}
}
post '/review_authors', request_body.to_json, json_api_headers
assert last_response.created?
assert last_response_json[:data][:id].present?
assert_equal 'review_authors', last_response_json[:data][:type]
assert_equal request_body[:data][:attributes], last_response_json[:data][:attributes]
end
def test_update_review_author
request_body = {
data: {
id: @author.id.to_s,
type: 'review_authors',
attributes: {
content: 'changed'
}
}
}
patch "/review_authors/#{@author.id}", request_body.to_json, json_api_headers
assert last_response.ok?
assert_equal @author.id.to_s, last_response_json[:data][:id]
assert_equal 'review_authors', last_response_json[:data][:type]
assert_equal request_body[:data][:attributes], last_response_json[:data][:attributes]
end
def test_delete_review_author
delete "/review_authors/#{@author.id}", nil, json_api_headers
assert last_response.no_content?
assert_equal 0, Review.where(id: @author.id).count
assert_equal 1, Review.where(id: @post.id).count
end
def test_fetch_review_post
get "/review_posts/#{@post.id}", nil, json_api_headers
assert last_response.ok?
assert_equal @post.id.to_s, last_response_json[:data][:id]
assert_equal 'review_posts', last_response_json[:data][:type]
expected_attributes = { content: @post.content }
assert_equal expected_attributes, last_response_json[:data][:attributes]
end
def test_create_review_post
request_body = {
data: {
type: 'review_posts',
attributes: {
content: 'author 2'
}
}
}
post '/review_posts', request_body.to_json, json_api_headers
assert last_response.created?
assert last_response_json[:data][:id].present?
assert_equal 'review_posts', last_response_json[:data][:type]
assert_equal request_body[:data][:attributes], last_response_json[:data][:attributes]
end
def test_update_review_post
request_body = {
data: {
id: @post.id.to_s,
type: 'review_posts',
attributes: {
content: 'changed'
}
}
}
patch "/review_posts/#{@post.id}", request_body.to_json, json_api_headers
assert last_response.ok?
assert_equal @post.id.to_s, last_response_json[:data][:id]
assert_equal 'review_posts', last_response_json[:data][:type]
assert_equal request_body[:data][:attributes], last_response_json[:data][:attributes]
end
def test_delete_review_post
delete "/review_posts/#{@post.id}", nil, json_api_headers
assert last_response.no_content?
assert_equal 0, Review.where(id: @post.id).count
assert_equal 1, Review.where(id: @author.id).count
end
private
def json_api_headers
{'Accept' => JSONAPI::MEDIA_TYPE, 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE}
end
def last_response_json
JSON.parse(last_response.body).deep_symbolize_keys
end
def app
Rails.application
end
end
Was there ever a fix for the namespacing issues?
@jamesconant if you're experiencing an issue could you please add some details to help reproduce it? Ideally something like the code above.
The gist of it is that if I have a nested model and a nested resource, it doesn't appear that the resource is able to understand what model it corresponds to (without additional declarations) if they are name-spaced. Maybe that is expected behavior, so my apologies in advance if that's the case. Below is some code. Probably also worth mentioning that I'm working with v0.9 and not v0.10.
Given this model:
class Api::V1::Task < ApplicationRecord
end
Works:
class Api::V1::Task < ApplicationResource
model_name "Api::V1::Task"
end
Doesn't work:
class Api::V1::Task < ApplicationResource
end
Here's another issue:
class Api::V2::CarePlan::AgreementResource < Api::V2::ApiResource
model_name 'CarePlan::Agreement'
has_one :user
end
It will return an error: JSONAPI: Could not find resource 'api/v2/care_plan/users'. (Class Api::V2::CarePlan::UserResource not found)
It's because UserResource is outside of CarePlan namespace.
Most helpful comment
Here's another issue:
class Api::V2::CarePlan::AgreementResource < Api::V2::ApiResourcemodel_name 'CarePlan::Agreement'has_one :userendIt will return an error:
JSONAPI: Could not find resource 'api/v2/care_plan/users'. (Class Api::V2::CarePlan::UserResource not found)It's because UserResource is outside of CarePlan namespace.