diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 25fff31..cab09e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,12 +8,17 @@ on: jobs: build: runs-on: ubuntu-latest + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true name: Ruby ${{ matrix.ruby }} strategy: + fail-fast: false matrix: ruby: - "3.2" - "3.3" + - "3.4" + - "4.0" - "ruby-head" steps: diff --git a/.gitignore b/.gitignore index 867fc18..b82e7a1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,10 @@ Gemfile.lock # MacOS .DS_Store + +# Dummy app artifacts +spec/dummy/log/ +spec/dummy/tmp/ + +# Claude +.claude/settings.local.json diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ef4145c..e2516b0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,17 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-11-17 23:28:22 UTC using RuboCop version 1.81.7. +# on 2026-01-25 10:40:25 UTC using RuboCop version 1.81.7. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 2 +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 12 + +# Offense count: 6 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 14 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ea351c4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# Agents + +## Style + +- Always use Ruby keyword argument shorthand (e.g., `super(**kwargs, json:)` not `super(**kwargs, json: json)`). diff --git a/Gemfile b/Gemfile index 5fbbfc1..ab93cd5 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,10 @@ gemspec gem "rake", "~> 13.0" +gem "rails", ">= 7.2" + gem "rspec", "~> 3.0" +gem "rspec-rails", "~> 7.0" gem "rubocop", "~> 1.21" gem "rubocop-rspec", require: false @@ -18,5 +21,7 @@ gem "undercover" gem "pry" +gem "sqlite3" + gem "gem-release", require: false gem "solargraph", require: false diff --git a/lib/transmutation/serialization/rendering.rb b/lib/transmutation/serialization/rendering.rb index 1cfaeab..2f99712 100644 --- a/lib/transmutation/serialization/rendering.rb +++ b/lib/transmutation/serialization/rendering.rb @@ -4,11 +4,26 @@ module Transmutation module Serialization module Rendering # Serializes the value of the `json` parameter before calling the existing render method. - def render(json: nil, serialize: true, namespace: nil, serializer: nil, max_depth: 1, **args) - return super(**args) unless json - return super(**args, json:) unless serialize + # + # Handles both Rails-style hash arguments (e.g., from send_data) and keyword arguments. + def render(options = nil, **kwargs) + # Handle Rails-style positional hash argument (e.g., from send_data calling render(body: data)) + if options.is_a?(Hash) + return super(options) unless options.key?(:json) - super(**args, json: serialize(json, namespace:, serializer:, max_depth:)) + kwargs = options.merge(kwargs) + end + + json = kwargs.delete(:json) + should_serialize = kwargs.key?(:serialize) ? kwargs.delete(:serialize) : true + namespace = kwargs.delete(:namespace) + serializer = kwargs.delete(:serializer) + max_depth = kwargs.delete(:max_depth) || Transmutation.max_depth + + return super(**kwargs) unless json + return super(**kwargs, json:) unless should_serialize + + super(**kwargs, json: serialize(json, namespace:, serializer:, max_depth:)) end end end diff --git a/spec/system/dummy/controllers/api/application_controller.rb b/spec/dummy/app/controllers/api/application_controller.rb similarity index 62% rename from spec/system/dummy/controllers/api/application_controller.rb rename to spec/dummy/app/controllers/api/application_controller.rb index f633cff..0c2735a 100644 --- a/spec/system/dummy/controllers/api/application_controller.rb +++ b/spec/dummy/app/controllers/api/application_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Api - class ApplicationController < BaseController + class ApplicationController < ::ApplicationController include Transmutation::Serialization end end diff --git a/spec/system/dummy/controllers/api/v1/health_controller.rb b/spec/dummy/app/controllers/api/v1/health_controller.rb similarity index 60% rename from spec/system/dummy/controllers/api/v1/health_controller.rb rename to spec/dummy/app/controllers/api/v1/health_controller.rb index 8767c66..f792c9c 100644 --- a/spec/system/dummy/controllers/api/v1/health_controller.rb +++ b/spec/dummy/app/controllers/api/v1/health_controller.rb @@ -4,11 +4,11 @@ module Api module V1 class HealthController < Api::ApplicationController def index - render(json: { ok: true }) + render json: { ok: true } end def download - send_data("binary data content", filename: "report.txt", type: "text/plain") + send_data "binary data content", filename: "report.txt", type: "text/plain" end end end diff --git a/spec/system/dummy/controllers/api/v1/posts_controller.rb b/spec/dummy/app/controllers/api/v1/posts_controller.rb similarity index 83% rename from spec/system/dummy/controllers/api/v1/posts_controller.rb rename to spec/dummy/app/controllers/api/v1/posts_controller.rb index e7c6bd8..e658831 100644 --- a/spec/system/dummy/controllers/api/v1/posts_controller.rb +++ b/spec/dummy/app/controllers/api/v1/posts_controller.rb @@ -5,13 +5,11 @@ module V1 class PostsController < Api::ApplicationController def index posts = Post.all - render json: posts end - def show(id) - post = Post.find(id) - + def show + post = Post.find(params[:id]) render json: post, namespace: "Detailed" end end diff --git a/spec/system/dummy/controllers/api/v1/products_controller.rb b/spec/dummy/app/controllers/api/v1/products_controller.rb similarity index 61% rename from spec/system/dummy/controllers/api/v1/products_controller.rb rename to spec/dummy/app/controllers/api/v1/products_controller.rb index 4887e5d..0ded5d1 100644 --- a/spec/system/dummy/controllers/api/v1/products_controller.rb +++ b/spec/dummy/app/controllers/api/v1/products_controller.rb @@ -3,10 +3,9 @@ module Api module V1 class ProductsController < Api::ApplicationController - def show(id) - post = Product.find(id) - - render json: post + def show + product = Product.find(params[:id]) + render json: product end end end diff --git a/spec/system/dummy/controllers/api/v1/users_controller.rb b/spec/dummy/app/controllers/api/v1/users_controller.rb similarity index 84% rename from spec/system/dummy/controllers/api/v1/users_controller.rb rename to spec/dummy/app/controllers/api/v1/users_controller.rb index bf7a47e..eff3a96 100644 --- a/spec/system/dummy/controllers/api/v1/users_controller.rb +++ b/spec/dummy/app/controllers/api/v1/users_controller.rb @@ -5,13 +5,11 @@ module V1 class UsersController < Api::ApplicationController def index users = User.all - render json: users end - def show(id) - user = User.find(id) - + def show + user = User.find(params[:id]) render json: user, serializer: "Detailed::UserSerializer" end end diff --git a/spec/dummy/app/controllers/application_controller.rb b/spec/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000..13c271f --- /dev/null +++ b/spec/dummy/app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::API +end diff --git a/spec/dummy/app/models/comment.rb b/spec/dummy/app/models/comment.rb new file mode 100644 index 0000000..00b9bd6 --- /dev/null +++ b/spec/dummy/app/models/comment.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Comment < ActiveRecord::Base + belongs_to :user, optional: true +end diff --git a/spec/system/dummy/lib/money.rb b/spec/dummy/app/models/money.rb similarity index 100% rename from spec/system/dummy/lib/money.rb rename to spec/dummy/app/models/money.rb diff --git a/spec/dummy/app/models/post.rb b/spec/dummy/app/models/post.rb new file mode 100644 index 0000000..f9cb961 --- /dev/null +++ b/spec/dummy/app/models/post.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Post < ActiveRecord::Base + belongs_to :user, optional: true +end diff --git a/spec/dummy/app/models/product.rb b/spec/dummy/app/models/product.rb new file mode 100644 index 0000000..a38fd3e --- /dev/null +++ b/spec/dummy/app/models/product.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Product < ActiveRecord::Base + def price + Money.new(price_subunit, price_currency) + end +end diff --git a/spec/dummy/app/models/user.rb b/spec/dummy/app/models/user.rb new file mode 100644 index 0000000..1907662 --- /dev/null +++ b/spec/dummy/app/models/user.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class User < ActiveRecord::Base + has_many :posts + has_many :comments +end diff --git a/spec/system/dummy/serializers/api/v1/comment_serializer.rb b/spec/dummy/app/serializers/api/v1/comment_serializer.rb similarity index 100% rename from spec/system/dummy/serializers/api/v1/comment_serializer.rb rename to spec/dummy/app/serializers/api/v1/comment_serializer.rb diff --git a/spec/system/dummy/serializers/api/v1/detailed/post_serializer.rb b/spec/dummy/app/serializers/api/v1/detailed/post_serializer.rb similarity index 100% rename from spec/system/dummy/serializers/api/v1/detailed/post_serializer.rb rename to spec/dummy/app/serializers/api/v1/detailed/post_serializer.rb diff --git a/spec/system/dummy/serializers/api/v1/detailed/user_serializer.rb b/spec/dummy/app/serializers/api/v1/detailed/user_serializer.rb similarity index 100% rename from spec/system/dummy/serializers/api/v1/detailed/user_serializer.rb rename to spec/dummy/app/serializers/api/v1/detailed/user_serializer.rb diff --git a/spec/system/dummy/serializers/api/v1/post_serializer.rb b/spec/dummy/app/serializers/api/v1/post_serializer.rb similarity index 100% rename from spec/system/dummy/serializers/api/v1/post_serializer.rb rename to spec/dummy/app/serializers/api/v1/post_serializer.rb diff --git a/spec/system/dummy/serializers/api/v1/user_serializer.rb b/spec/dummy/app/serializers/api/v1/user_serializer.rb similarity index 100% rename from spec/system/dummy/serializers/api/v1/user_serializer.rb rename to spec/dummy/app/serializers/api/v1/user_serializer.rb diff --git a/spec/system/dummy/serializers/money_serializer.rb b/spec/dummy/app/serializers/money_serializer.rb similarity index 100% rename from spec/system/dummy/serializers/money_serializer.rb rename to spec/dummy/app/serializers/money_serializer.rb diff --git a/spec/system/dummy/serializers/product_serializer.rb b/spec/dummy/app/serializers/product_serializer.rb similarity index 100% rename from spec/system/dummy/serializers/product_serializer.rb rename to spec/dummy/app/serializers/product_serializer.rb diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb new file mode 100644 index 0000000..56131c2 --- /dev/null +++ b/spec/dummy/config/application.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "boot" + +require "rails" +require "active_record/railtie" +require "action_controller/railtie" + +Bundler.require(*Rails.groups) + +require "transmutation" + +module Dummy + class Application < Rails::Application + config.root = File.expand_path("..", __dir__) + + config.load_defaults Rails::VERSION::STRING.to_f + + config.autoload_paths << config.root.join("app/serializers") + + config.api_only = true + config.eager_load = false + end +end diff --git a/spec/dummy/config/boot.rb b/spec/dummy/config/boot.rb new file mode 100644 index 0000000..45535e7 --- /dev/null +++ b/spec/dummy/config/boot.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) + +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) +$LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) diff --git a/spec/dummy/config/database.yml b/spec/dummy/config/database.yml new file mode 100644 index 0000000..2cfeb7a --- /dev/null +++ b/spec/dummy/config/database.yml @@ -0,0 +1,3 @@ +test: + adapter: sqlite3 + database: ":memory:" diff --git a/spec/dummy/config/environment.rb b/spec/dummy/config/environment.rb new file mode 100644 index 0000000..e8173e0 --- /dev/null +++ b/spec/dummy/config/environment.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative "application" + +Rails.application.initialize! diff --git a/spec/dummy/config/environments/test.rb b/spec/dummy/config/environments/test.rb new file mode 100644 index 0000000..56d781c --- /dev/null +++ b/spec/dummy/config/environments/test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.eager_load = false + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.action_controller.allow_forgery_protection = false + config.active_support.deprecation = :stderr +end diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb new file mode 100644 index 0000000..adbfe03 --- /dev/null +++ b/spec/dummy/config/routes.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +Rails.application.routes.draw do + namespace :api do + namespace :v1 do + resources :users, only: %i[index show] + resources :posts, only: %i[index show] + resources :products, only: [:show] + resources :health, only: [:index] do + collection do + get :download + end + end + end + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb new file mode 100644 index 0000000..d4a49e4 --- /dev/null +++ b/spec/dummy/db/schema.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +ActiveRecord::Schema.define(version: 0) do + create_table :users, force: true do |t| + t.string :first_name, null: false + t.string :last_name, null: false + end + + create_table :posts, force: true do |t| + t.string :title, null: false + t.text :body, null: false + t.references :user, foreign_key: true + t.datetime :published_at + end + + create_table :comments, force: true do |t| + t.text :body, null: false + t.references :user, foreign_key: true + end + + create_table :products, force: true do |t| + t.string :name, null: false + t.string :description + t.integer :price_subunit, null: false + t.string :price_currency, null: false + end +end diff --git a/spec/dummy/test/fixtures/comments.yml b/spec/dummy/test/fixtures/comments.yml new file mode 100644 index 0000000..54ddaa9 --- /dev/null +++ b/spec/dummy/test/fixtures/comments.yml @@ -0,0 +1,14 @@ +first_comment: + id: 1 + body: "First!" + user_id: 1 + +second_comment: + id: 2 + body: "Second!" + user_id: 2 + +third_comment: + id: 3 + body: "Third!" + user_id: 1 diff --git a/spec/dummy/test/fixtures/posts.yml b/spec/dummy/test/fixtures/posts.yml new file mode 100644 index 0000000..44c51a2 --- /dev/null +++ b/spec/dummy/test/fixtures/posts.yml @@ -0,0 +1,20 @@ +first_post: + id: 1 + title: First post + body: "First!" + user_id: 1 + published_at: 2025-01-01 00:00:00 + +second_post: + id: 2 + title: How does this work? + body: body + user_id: 2 + published_at: 2025-01-01 00:00:00 + +third_post: + id: 3 + title: "Second post!?" + body: "Nope..." + user_id: 1 + published_at: null diff --git a/spec/dummy/test/fixtures/products.yml b/spec/dummy/test/fixtures/products.yml new file mode 100644 index 0000000..f1c2353 --- /dev/null +++ b/spec/dummy/test/fixtures/products.yml @@ -0,0 +1,6 @@ +shoes: + id: 1 + name: Shoes + description: A pair of shoes + price_subunit: 1000 + price_currency: GBP diff --git a/spec/dummy/test/fixtures/users.yml b/spec/dummy/test/fixtures/users.yml new file mode 100644 index 0000000..6f7747e --- /dev/null +++ b/spec/dummy/test/fixtures/users.yml @@ -0,0 +1,19 @@ +john: + id: 1 + first_name: John + last_name: Doe + +jane: + id: 2 + first_name: Jane + last_name: Doe + +adam: + id: 3 + first_name: Adam + last_name: Smith + +eve: + id: 4 + first_name: Eve + last_name: Smith diff --git a/spec/lib/transmutation/serialization_spec.rb b/spec/lib/transmutation/serialization_spec.rb index a8073c0..5300365 100644 --- a/spec/lib/transmutation/serialization_spec.rb +++ b/spec/lib/transmutation/serialization_spec.rb @@ -180,6 +180,10 @@ def initialize(id:) Transmutation.max_depth = 2 end + after do + Transmutation.max_depth = 1 + end + it "returns the maximum depth of the serializer" do expect(Transmutation.max_depth).to eq(2) end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..c667a96 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "spec_helper" + +ENV["RAILS_ENV"] ||= "test" +require_relative "dummy/config/environment" + +abort("The Rails environment is not running in test mode!") if Rails.env.production? + +require "rspec/rails" + +# Load the database schema into the in-memory SQLite database +ActiveRecord::Schema.verbose = false +load Rails.root.join("db/schema.rb").to_s + +RSpec.configure do |config| + config.include Rails.application.routes.url_helpers, type: :request + + config.fixture_paths = [Rails.root.join("test/fixtures").to_s] + config.use_transactional_fixtures = true + config.global_fixtures = :all + + config.infer_spec_type_from_file_location! + + config.filter_rails_from_backtrace! +end diff --git a/spec/requests/api/v1/health_spec.rb b/spec/requests/api/v1/health_spec.rb new file mode 100644 index 0000000..f453ac8 --- /dev/null +++ b/spec/requests/api/v1/health_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Api::V1::Health", type: :request do + describe "GET /api/v1/health" do + it "returns a JSON object without serialization", :aggregate_failures do + get api_v1_health_index_path + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq({ "ok" => true }) + end + end + + describe "GET /api/v1/health/download" do + it "returns binary data using send_data", :aggregate_failures do + get download_api_v1_health_index_path + + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("text/plain") + expect(response.body).to eq("binary data content") + end + + it "sets the correct Content-Disposition header" do + get download_api_v1_health_index_path + + expect(response.headers["Content-Disposition"]).to include("report.txt") + end + end +end diff --git a/spec/requests/api/v1/posts_spec.rb b/spec/requests/api/v1/posts_spec.rb new file mode 100644 index 0000000..4b1805e --- /dev/null +++ b/spec/requests/api/v1/posts_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Api::V1::Posts", type: :request do + describe "GET /api/v1/posts" do + it "returns a list of posts serialized with Api::V1::PostSerializer", :aggregate_failures do + get api_v1_posts_path + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq([ + { "id" => 1, "title" => "First post" }, + { "id" => 2, "title" => "How does this work?" }, + { "id" => 3, "title" => "Second post!?" } + ]) + end + end + + describe "GET /api/v1/posts/:id" do + it "returns a post serialized with Api::V1::Detailed::PostSerializer", :aggregate_failures do + get api_v1_post_path(1) + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq( + "id" => 1, + "title" => "First post", + "body" => "First!", + "user" => { + "id" => 1, + "first_name" => "John", + "last_name" => "Doe", + "full_name" => "John Doe" + } + ) + end + end +end diff --git a/spec/requests/api/v1/products_spec.rb b/spec/requests/api/v1/products_spec.rb new file mode 100644 index 0000000..8ab4ad5 --- /dev/null +++ b/spec/requests/api/v1/products_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Api::V1::Products", type: :request do + describe "GET /api/v1/products/:id" do + it "returns a product serialized with ProductSerializer", :aggregate_failures do + get api_v1_product_path(1) + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq( + "id" => 1, + "name" => "Shoes", + "price" => { + "subunit" => 1000, + "currency" => "GBP" + } + ) + end + end +end diff --git a/spec/requests/api/v1/users_spec.rb b/spec/requests/api/v1/users_spec.rb new file mode 100644 index 0000000..4b02421 --- /dev/null +++ b/spec/requests/api/v1/users_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Api::V1::Users", type: :request do + describe "GET /api/v1/users" do + it "returns a list of users serialized with Api::V1::UserSerializer", :aggregate_failures do + get api_v1_users_path + + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect(JSON.parse(response.body)).to eq([ + { "id" => 1, "full_name" => "John Doe" }, + { "id" => 2, "full_name" => "Jane Doe" }, + { "id" => 3, "full_name" => "Adam Smith" }, + { "id" => 4, "full_name" => "Eve Smith" } + ]) + end + end + + describe "GET /api/v1/users/:id" do + let(:expected_json) do + { + "id" => 1, + "first_name" => "John", + "last_name" => "Doe", + "full_name" => "John Doe", + "posts" => [ + { "id" => 1, "title" => "First post" }, + { "id" => 3, "title" => "Second post!?" } + ], + "comments" => [ + { "id" => 1, "body" => "First!" }, + { "id" => 3, "body" => "Third!" } + ], + "published_posts" => [ + { "id" => 1, "title" => "First post" } + ] + } + end + + it "returns a user serialized with Api::V1::Detailed::UserSerializer", :aggregate_failures do + get api_v1_user_path(1) + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq(expected_json) + end + end +end diff --git a/spec/system/dummy/app.rb b/spec/system/dummy/app.rb deleted file mode 100644 index 1e9e4ec..0000000 --- a/spec/system/dummy/app.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require "json" -require "zeitwerk" - -loader = Zeitwerk::Loader.new -loader.push_dir(__dir__) -loader.collapse("#{__dir__}/*") -loader.ignore(__FILE__) -loader.setup diff --git a/spec/system/dummy/controllers/base_controller.rb b/spec/system/dummy/controllers/base_controller.rb deleted file mode 100644 index 8e01103..0000000 --- a/spec/system/dummy/controllers/base_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class BaseController - def render(json: nil, body: nil, **options) - return body if body - - JSON.parse(JSON.generate(json)) - end - - # Simulates Rails' send_data method which internally calls render - # See: https://api.rubyonrails.org/classes/ActionController/DataStreaming.html - # - # Note: Rails calls render with a Hash (not keyword arguments): - # render options.slice(:status, :content_type).merge(body: data) - # - # We simulate this exactly to catch any issues with Hash vs kwargs handling - def send_data(data, options = {}) - render(**options.slice(:status, :content_type).merge(body: data)) - end -end diff --git a/spec/system/dummy/models/comment.rb b/spec/system/dummy/models/comment.rb deleted file mode 100644 index 9d85469..0000000 --- a/spec/system/dummy/models/comment.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class Comment - attr_accessor :id, :body, :user_id - - def initialize(id:, body:, user_id: nil) - self.id = id - self.body = body - self.user_id = user_id - end - - # ===== - # Finder methods - # ===== - - def self.all - [ - Comment.new(id: 1, body: "First!", user_id: 1), - Comment.new(id: 2, body: "Second!", user_id: 2), - Comment.new(id: 3, body: "Third!", user_id: 1) - ] - end -end diff --git a/spec/system/dummy/models/post.rb b/spec/system/dummy/models/post.rb deleted file mode 100644 index a5a0ac5..0000000 --- a/spec/system/dummy/models/post.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -class Post - attr_accessor :id, :title, :body, :user_id, :published_at - - def initialize(id:, title:, body:, user_id: nil, published_at: nil) - self.id = id - self.title = title - self.body = body - self.user_id = user_id - self.published_at = published_at - end - - def user - User.find(user_id) if user_id - end - - # ===== - # Finder methods - # ===== - - def self.all - [ - Post.new(id: 1, title: "First post", body: "First!", user_id: 1, published_at: Time.now), - Post.new(id: 2, title: "How does this work?", body: "body", user_id: 2, published_at: Time.now), - Post.new(id: 3, title: "Second post!?", body: "Nope...", user_id: 1, published_at: nil) - ] - end - - def self.find(id) - all.find { |post| post.id == id } - end - - # ===== - # JSON serialization - # ===== - - def to_json(options = {}) - as_json(options).to_json - end - - def as_json(_options = {}) - { - id:, - title: first_name, - body: last_name - } - end -end diff --git a/spec/system/dummy/models/product.rb b/spec/system/dummy/models/product.rb deleted file mode 100644 index 994d312..0000000 --- a/spec/system/dummy/models/product.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -class Product - attr_accessor :id, :name, :description, :price_subunit, :price_currency - - def initialize(id:, name:, description:, price_subunit:, price_currency:) - self.id = id - self.name = name - self.description = description - self.price_subunit = price_subunit - self.price_currency = price_currency - end - - def price - Money.new(price_subunit, price_currency) - end - - # ===== - # Finder methods - # ===== - - def self.all - [ - Product.new(id: 1, name: "Shoes", description: "A pair of shoes", price_subunit: 1000, price_currency: "GBP") - ] - end - - def self.find(id) - all.find { |user| user.id == id } - end - - # ===== - # JSON serialization - # ===== - - def to_json(options = {}) - as_json(options).to_json - end - - def as_json(_options = {}) - { - id:, - name:, - description:, - price_subunit:, - price_currency: - } - end -end diff --git a/spec/system/dummy/models/user.rb b/spec/system/dummy/models/user.rb deleted file mode 100644 index 859dcb5..0000000 --- a/spec/system/dummy/models/user.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -class User - attr_accessor :id, :first_name, :last_name - - def initialize(id:, first_name:, last_name:) - self.id = id - self.first_name = first_name - self.last_name = last_name - end - - def posts - Post.all.select { |post| post.user_id == id } - end - - def comments - Comment.all.select { |comment| comment.user_id == id } - end - - # ===== - # Finder methods - # ===== - - def self.all - [ - User.new(id: 1, first_name: "John", last_name: "Doe"), - User.new(id: 2, first_name: "Jane", last_name: "Doe"), - User.new(id: 3, first_name: "Adam", last_name: "Smith"), - User.new(id: 4, first_name: "Eve", last_name: "Smith") - ] - end - - def self.find(id) - all.find { |user| user.id == id } - end - - # ===== - # JSON serialization - # ===== - - def to_json(options = {}) - as_json(options).to_json - end - - def as_json(_options = {}) - { - id:, - first_name:, - last_name: - } - end -end diff --git a/spec/system/rendering_spec.rb b/spec/system/rendering_spec.rb deleted file mode 100644 index 02627d1..0000000 --- a/spec/system/rendering_spec.rb +++ /dev/null @@ -1,136 +0,0 @@ -# frozen_string_literal: true - -require_relative "dummy/app" - -RSpec.describe "Rendering" do - describe "Api::V1::UsersController" do - subject(:controller) { Api::V1::UsersController.new } - - describe "#index" do - let(:expected_json) do - [ - { "id" => 1, "full_name" => "John Doe" }, - { "id" => 2, "full_name" => "Jane Doe" }, - { "id" => 3, "full_name" => "Adam Smith" }, - { "id" => 4, "full_name" => "Eve Smith" } - ] - end - - it "returns a list of users serialized with the Api::V1::UserSerializer" do - expect(controller.index).to eq(expected_json) - end - end - - describe "#show" do - let(:expected_json) do - { - "id" => 1, - "first_name" => "John", - "last_name" => "Doe", - "full_name" => "John Doe", - "posts" => [ - { "id" => 1, "title" => "First post" }, - { "id" => 3, "title" => "Second post!?" } - ], - "comments" => [ - { "id" => 1, "body" => "First!" }, - { "id" => 3, "body" => "Third!" } - ], - "published_posts" => [ - { "id" => 1, "title" => "First post" } - ] - } - end - - it "returns a user serialized with the Api::V1::Detailed::UserSerializer" do - expect(controller.show(1)).to eq(expected_json) - end - end - end - - describe "Api::V1::PostsController" do - subject(:controller) { Api::V1::PostsController.new } - - describe "#index" do - let(:expected_json) do - [ - { "id" => 1, "title" => "First post" }, - { "id" => 2, "title" => "How does this work?" }, - { "id" => 3, "title" => "Second post!?" } - ] - end - - it "returns a list of posts serialized with the Api::V1::PostSerializer" do - expect(controller.index).to eq(expected_json) - end - end - - describe "#show" do - let(:expected_json) do - { - "id" => 1, - "title" => "First post", - "body" => "First!", - "user" => { "id" => 1, "first_name" => "John", "last_name" => "Doe", "full_name" => "John Doe" } - } - end - - it "returns a post serialized with the Api::V1::Detailed::PostSerializer" do - expect(controller.show(1)).to eq(expected_json) - end - end - end - - describe "Api::V1::ProductsController" do - subject(:controller) { Api::V1::ProductsController.new } - - describe "#show" do - let(:expected_json) do - { - "id" => 1, - "name" => "Shoes", - "price" => { - "subunit" => 1000, - "currency" => "GBP" - } - } - end - - it "returns a product serialized with the ProductSerializer" do - expect(controller.show(1)).to eq(expected_json) - end - end - end - - describe "Api::V1::HealthController" do - subject(:controller) { Api::V1::HealthController.new } - - describe "#index" do - let(:expected_json) do - { - "ok" => true - } - end - - it "returns a JSON object" do - expect(controller.index).to eq(expected_json) - end - end - - describe "#download" do - it "returns binary data without serialization" do - expect(controller.download).to eq("binary data content") - end - - it "does not call serialize" do - expect(controller).not_to receive(:serialize) - controller.download - end - - it "does not call lookup_serializer" do - expect(controller).not_to receive(:lookup_serializer) - controller.download - end - end - end -end