From 37ac70342cfd6f4cde2982b479b51ff7462c022b Mon Sep 17 00:00:00 2001 From: Vyacheslav Yurchenkov Date: Wed, 12 Mar 2025 23:19:22 +0300 Subject: [PATCH 1/4] small fixes --- config/database.yml | 1 + db/schema.rb | 1 + docker-compose.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/config/database.yml b/config/database.yml index 48f30ee..4277af8 100644 --- a/config/database.yml +++ b/config/database.yml @@ -17,6 +17,7 @@ development: test: <<: *default + <<: *postgre database: library_test production: diff --git a/db/schema.rb b/db/schema.rb index 7733389..ed479dd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -49,6 +49,7 @@ t.string "insno", comment: "ISBN" t.uuid "folder_id", null: false t.uuid "language_id", null: false + t.index ["folder_id", "libid"], name: "idx_on_folder_id_libid", unique: true t.index ["folder_id"], name: "index_books_on_folder_id" t.index ["language_id"], name: "index_books_on_language_id" end diff --git a/docker-compose.yml b/docker-compose.yml index 9c195af..9c87062 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,5 +33,6 @@ services: POSTGRES_HOST: 'db' POSTGRES_USER: 'postgres' POSTGRES_PASSWORD: 'postgres' + RAILS_ENV: development volumes: db: From ec8d77dd7206b471a03937617c7f2b34c27fbf3e Mon Sep 17 00:00:00 2001 From: Vyacheslav Yurchenkov Date: Thu, 13 Mar 2025 00:45:15 +0300 Subject: [PATCH 2/4] GET /api/v1/books --- Gemfile | 2 ++ Gemfile.lock | 5 +++- app/controllers/api/v1/books_controller.rb | 16 ++++++++++++ .../api/v1/books/index_interactor.rb | 25 +++++++++++++++++++ app/interactors/application_interactor.rb | 5 ++++ .../api/v1/books/index/author_resource.rb | 13 ++++++++++ .../api/v1/books/index/book_resource.rb | 15 +++++++++++ config/initializers/alba.rb | 1 + config/routes.rb | 6 +++++ 9 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/books_controller.rb create mode 100644 app/interactors/api/v1/books/index_interactor.rb create mode 100644 app/interactors/application_interactor.rb create mode 100644 app/serializers/api/v1/books/index/author_resource.rb create mode 100644 app/serializers/api/v1/books/index/book_resource.rb create mode 100644 config/initializers/alba.rb diff --git a/Gemfile b/Gemfile index fc76bca..7823088 100644 --- a/Gemfile +++ b/Gemfile @@ -52,3 +52,5 @@ end group :development do gem 'web-console' end + +gem "alba", "~> 3.5" diff --git a/Gemfile.lock b/Gemfile.lock index d66b198..bce9fa1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,6 +95,8 @@ GEM uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + alba (3.5.0) + ostruct (~> 0.6) arbre (1.7.0) activesupport (>= 3.0.0) ruby2_keywords (>= 0.0.2) @@ -448,6 +450,7 @@ DEPENDENCIES activeadmin activeadmin_addons activerecord-import + alba (~> 3.5) bundler-audit capybara config @@ -479,4 +482,4 @@ DEPENDENCIES web-console BUNDLED WITH - 2.5.18 + 2.5.3 diff --git a/app/controllers/api/v1/books_controller.rb b/app/controllers/api/v1/books_controller.rb new file mode 100644 index 0000000..0b9bcc2 --- /dev/null +++ b/app/controllers/api/v1/books_controller.rb @@ -0,0 +1,16 @@ +module Api + module V1 + class BooksController < ApplicationController + def index + permitted_params = index_params.to_h + books = Books::IndexInteractor.call(page: permitted_params[:page]) + render json: { data: Books::Index::BookResource.new(books).to_h } + end + + private + def index_params + params.permit(:page) + end + end + end +end diff --git a/app/interactors/api/v1/books/index_interactor.rb b/app/interactors/api/v1/books/index_interactor.rb new file mode 100644 index 0000000..b349df0 --- /dev/null +++ b/app/interactors/api/v1/books/index_interactor.rb @@ -0,0 +1,25 @@ +module Api + module V1 + module Books + class IndexInteractor < ApplicationInteractor + def initialize(page:) + @page = Integer(page, exception: false) || 1 + end + + def call + Book.includes(:authors).order(created_at: :asc).offset(offset).limit(per_page) + end + + private + + def offset + per_page * (@page - 1) + end + + def per_page + Settings.app.items_per_page + end + end + end + end +end diff --git a/app/interactors/application_interactor.rb b/app/interactors/application_interactor.rb new file mode 100644 index 0000000..94c2773 --- /dev/null +++ b/app/interactors/application_interactor.rb @@ -0,0 +1,5 @@ +class ApplicationInteractor + def self.call(*, **) + new(*, **).call + end +end diff --git a/app/serializers/api/v1/books/index/author_resource.rb b/app/serializers/api/v1/books/index/author_resource.rb new file mode 100644 index 0000000..f87f83b --- /dev/null +++ b/app/serializers/api/v1/books/index/author_resource.rb @@ -0,0 +1,13 @@ +module Api + module V1 + module Books + module Index + class AuthorResource + include Alba::Resource + + attributes :id, :first_name, :middle_name, :last_name + end + end + end + end +end diff --git a/app/serializers/api/v1/books/index/book_resource.rb b/app/serializers/api/v1/books/index/book_resource.rb new file mode 100644 index 0000000..23c83a3 --- /dev/null +++ b/app/serializers/api/v1/books/index/book_resource.rb @@ -0,0 +1,15 @@ +module Api + module V1 + module Books + module Index + class BookResource + include Alba::Resource + + attributes :id, :title, :series, :serno + + many :authors, resource: AuthorResource + end + end + end + end +end diff --git a/config/initializers/alba.rb b/config/initializers/alba.rb new file mode 100644 index 0000000..6063789 --- /dev/null +++ b/config/initializers/alba.rb @@ -0,0 +1 @@ +Alba.inflector = nil diff --git a/config/routes.rb b/config/routes.rb index ae608d2..0598a14 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,4 +27,10 @@ # Render dynamic PWA files from app/views/pwa/* get 'service-worker' => 'rails/pwa#service_worker', as: :pwa_service_worker get 'manifest' => 'rails/pwa#manifest', as: :pwa_manifest + + namespace :api do + namespace :v1 do + resources :books, only: [:index] + end + end end From 5ba2cdf4951bcdf5aaabfbb5d650d05a70fa06c4 Mon Sep 17 00:00:00 2001 From: Vyacheslav Yurchenkov Date: Thu, 13 Mar 2025 01:04:13 +0300 Subject: [PATCH 3/4] sequence diagram --- docs/arch.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/arch.md diff --git a/docs/arch.md b/docs/arch.md new file mode 100644 index 0000000..fbab6b3 --- /dev/null +++ b/docs/arch.md @@ -0,0 +1,55 @@ +```plantuml +@startuml +actor Client as C +participant Routing as R +participant Controller as Cntr +participant ServiceObject as Inter + +entity Model as M + + +participant Serializer as S +database DB as DB + + +C -> R +activate R + + +R -> Cntr +activate Cntr +deactivate R + + +Cntr -> Inter +activate Inter + + +Inter -> M +activate M + +M -> DB +activate DB + +return +return +return + +... + + +Cntr -> S +activate S + +return +Cntr -> C +deactivate Cntr + + + + + + +@enduml + +``` From 35dc7f9d13fd44f718eec1e937d1cd3840a99c21 Mon Sep 17 00:00:00 2001 From: Vyacheslav Yurchenkov Date: Thu, 13 Mar 2025 13:22:04 +0300 Subject: [PATCH 4/4] spec --- .gitignore | 2 + bin/rspec | 28 +++++++ db/schema.rb | 1 - docker-compose.yml | 1 - .../api/v1/books_controller_spec.rb | 73 +++++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100755 bin/rspec create mode 100644 spec/controllers/api/v1/books_controller_spec.rb diff --git a/.gitignore b/.gitignore index 2f761fe..ae32e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ coverage config/settings.local.yml config/settings/*.local.yml config/environments/*.local.yml + +.byebug_history diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000..e59dcd3 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) +ENV["RAILS_ENV"] = "test" + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/db/schema.rb b/db/schema.rb index ed479dd..7733389 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -49,7 +49,6 @@ t.string "insno", comment: "ISBN" t.uuid "folder_id", null: false t.uuid "language_id", null: false - t.index ["folder_id", "libid"], name: "idx_on_folder_id_libid", unique: true t.index ["folder_id"], name: "index_books_on_folder_id" t.index ["language_id"], name: "index_books_on_language_id" end diff --git a/docker-compose.yml b/docker-compose.yml index 9c87062..9c195af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,5 @@ services: POSTGRES_HOST: 'db' POSTGRES_USER: 'postgres' POSTGRES_PASSWORD: 'postgres' - RAILS_ENV: development volumes: db: diff --git a/spec/controllers/api/v1/books_controller_spec.rb b/spec/controllers/api/v1/books_controller_spec.rb new file mode 100644 index 0000000..81597a8 --- /dev/null +++ b/spec/controllers/api/v1/books_controller_spec.rb @@ -0,0 +1,73 @@ +describe Api::V1::BooksController do + describe "GET #index" do + subject(:call) { get :index } + + before do + Settings.app.items_per_page = 2 + end + + context "when there are no books" do + it "returns empty array" do + subject + + expect(response).to have_http_status 200 + expect(JSON.parse(response.body, symbolize_names: true)).to eq(data: []) + end + end + + context "when there are 6 books" do + let!(:books) { create_list(:book, 6) } + + it "returns first 2 books" do + subject + + expect(response).to have_http_status 200 + expect(JSON.parse(response.body, symbolize_names: true)).to eq( + data: [ + { + authors: [], + id: books[0].id, + title: books[0].title, + series: nil, + serno: nil + }, + { + authors: [], + id: books[1].id, + title: books[1].title, + series: nil, + serno: nil + } + ]) + end + + context "when requested page is 2" do + subject(:call) { get :index, params: { page: 2 } } + + + it "returns third and fourth books" do + subject + + expect(response).to have_http_status 200 + expect(JSON.parse(response.body, symbolize_names: true)).to eq( + data: [ + { + authors: [], + id: books[2].id, + title: books[2].title, + series: nil, + serno: nil + }, + { + authors: [], + id: books[3].id, + title: books[3].title, + series: nil, + serno: nil + } + ]) + end + end + end + end +end