Jsonapi-resources: Issue with namespaced resource

Created on 6 Jun 2017  路  5Comments  路  Source: cerebris/jsonapi-resources

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.

Bug

Most helpful comment

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.

All 5 comments

@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.

Was this page helpful?
0 / 5 - 0 ratings