diff --git a/.gitignore b/.gitignore index 2f761fe..ab69669 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ coverage config/settings.local.yml config/settings/*.local.yml config/environments/*.local.yml + +.idea +.byebug_history diff --git a/Gemfile b/Gemfile index fc76bca..1d32d3a 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,12 @@ gem 'activeadmin' gem 'activeadmin_addons' gem 'devise' +# Сериализация данных +gem 'alba' + +# Пагинация +gem 'kaminari' + group :development, :test do gem 'bundler-audit' gem 'capybara' diff --git a/Gemfile.lock b/Gemfile.lock index 63e5241..28c5656 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 bundler-audit capybara config @@ -460,6 +463,7 @@ DEPENDENCIES factory_bot_rails fasterer ffaker + kaminari parallel pg pry-byebug diff --git a/app/controllers/books_controller.rb b/app/controllers/books_controller.rb new file mode 100644 index 0000000..947e073 --- /dev/null +++ b/app/controllers/books_controller.rb @@ -0,0 +1,7 @@ +class BooksController < ApplicationController + def index + @books = Book.all.page(params[:page]) + + render json: BookSerializer.new(@books).serialize + end +end diff --git a/app/serializers/author_serializer.rb b/app/serializers/author_serializer.rb new file mode 100644 index 0000000..77561f2 --- /dev/null +++ b/app/serializers/author_serializer.rb @@ -0,0 +1,11 @@ +class AuthorSerializer + include Alba::Resource + + attributes :id, :full_name + + private + + def full_name(author) + author.slice(:first_name, :middle_name, :last_name).values.join(' ') + end +end diff --git a/app/serializers/book_serializer.rb b/app/serializers/book_serializer.rb new file mode 100644 index 0000000..5a74db9 --- /dev/null +++ b/app/serializers/book_serializer.rb @@ -0,0 +1,10 @@ +class BookSerializer + include Alba::Resource + + root_key :books + + attributes :id, :title, :insno, :libid + + many :authors + one :language +end diff --git a/app/serializers/language_serializer.rb b/app/serializers/language_serializer.rb new file mode 100644 index 0000000..1511be9 --- /dev/null +++ b/app/serializers/language_serializer.rb @@ -0,0 +1,5 @@ +class LanguageSerializer + include Alba::Resource + + attributes :id, :slug, :name +end diff --git a/config/database.yml b/config/database.yml index 48f30ee..a8e84e1 100644 --- a/config/database.yml +++ b/config/database.yml @@ -7,8 +7,8 @@ default: &default postgre: &postgre host: <%= ENV.fetch('POSTGRES_HOST', 'localhost') %> port: <%= ENV.fetch('POSTGRES_PORT', '5432') %> - username: <%= ENV.fetch('POSTGRES_USER') { 'igorsimdyanov' } %> - password: <%= ENV.fetch('POSTGRES_PASSWORD') { '' } %> + username: <%= ENV.fetch('POSTGRES_USER') { 'postgres' } %> + password: <%= ENV.fetch('POSTGRES_PASSWORD') { 'postgres' } %> development: <<: *default diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb new file mode 100644 index 0000000..ffd57da --- /dev/null +++ b/config/initializers/kaminari_config.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +Kaminari.configure do |config| + config.default_per_page = Settings.app.items_per_page + # config.max_per_page = nil + # config.window = 4 + # config.outer_window = 0 + # config.left = 0 + # config.right = 0 + # config.page_method_name = :page + # config.param_name = :page + # config.max_pages = nil + # config.params_on_first_page = false +end diff --git a/config/routes.rb b/config/routes.rb index ae608d2..e02068c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,6 +20,8 @@ # # end # end + get 'books(/:page)', to: 'books#index' + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get 'up' => 'rails/health#show', as: :rails_health_check diff --git a/docs/rails_layers.puml b/docs/rails_layers.puml new file mode 100644 index 0000000..32689a7 --- /dev/null +++ b/docs/rails_layers.puml @@ -0,0 +1,21 @@ +@startuml + +actor User + +participant "Router" as Routing +participant "Controller" as Controller +participant "Model" as Model +participant "Business Logic" as BusinessLogic +participant "Serializer" as Serializer + +User -> Routing: HTTP Request +Routing -> Controller: Routes to Controller +Controller -> BusinessLogic: Some Business Logic +BusinessLogic -> Model: Query +Model -> BusinessLogic: Records +BusinessLogic -> Controller: Processed Data +Controller -> Serializer: Serialize +Serializer -> Controller: Serialized Data +Controller -> User: HTTP Response + +@enduml diff --git a/spec/controllers/books_controller_spec.rb b/spec/controllers/books_controller_spec.rb new file mode 100644 index 0000000..2fd5b3b --- /dev/null +++ b/spec/controllers/books_controller_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +RSpec.describe BooksController, type: :controller do + describe 'GET #index' do + let(:language) { create(:language) } + let(:folder) { create(:folder) } + let!(:books) { create_list(:book, 21, folder:, language:) } + + before do + get :index + end + + it 'returns a successful response' do + expect(response).to have_http_status(:success) + end + + it 'returns JSON with all books' do + json_response = JSON.parse(response.body) + + expect(json_response.size).to eq(20) + + json_response.each_with_index do |book_json, index| + expect(book_json['id']).to eq(books[index].id) + expect(book_json['title']).to eq(books[index].title) + expect(book_json['insno']).to eq(books[index].insno) + expect(book_json['libid']).to eq(books[index].libid) + end + end + + context 'with pagination' do + before do + get :index, params: { page: 2 } + end + + it 'returns only the first page of books' do + json_response = JSON.parse(response.body) + + expect(json_response.size).to eq(books[20..].size) + end + end + end +end diff --git a/spec/factories/books.rb b/spec/factories/books.rb index f6717e5..709fc73 100644 --- a/spec/factories/books.rb +++ b/spec/factories/books.rb @@ -1,6 +1,7 @@ FactoryBot.define do factory :book do title { FFaker::Book.title } + insno { FFaker::Book.isbn } libid { 3064 } size { 9836 } filename { 3064 } diff --git a/spec/serializers/author_serializer_spec.rb b/spec/serializers/author_serializer_spec.rb new file mode 100644 index 0000000..7e25eeb --- /dev/null +++ b/spec/serializers/author_serializer_spec.rb @@ -0,0 +1,17 @@ +RSpec.describe AuthorSerializer, type: :serializer do + let(:author) { create(:full_author) } + + describe 'serialization' do + context 'when correct author is passed' do + subject { described_class.new(author) } + + it "return correct json response" do + serialized = JSON.parse(subject.to_json) + full_name = [ author.first_name, author.middle_name, author.last_name ].join(' ') + + expect(serialized['id']).to eq(author.id) + expect(serialized['full_name']).to eq(full_name) + end + end + end +end diff --git a/spec/serializers/book_serializer_spec.rb b/spec/serializers/book_serializer_spec.rb new file mode 100644 index 0000000..8044f61 --- /dev/null +++ b/spec/serializers/book_serializer_spec.rb @@ -0,0 +1,21 @@ +RSpec.describe BookSerializer, type: :serializer do + let(:book) { create(:book_with_authors) } + + describe 'serialization' do + context 'when correct book is passed' do + subject { described_class.new(book) } + + it "return correct json response" do + serialized = JSON.parse(subject.to_json) + + expect(serialized.dig('books', 'id')).to eq(book.id) + expect(serialized.dig('books', 'title')).to eq(book.title) + expect(serialized.dig('books', 'insno')).to eq(book.insno) + expect(serialized.dig('books', 'libid')).to eq(book.libid) + expect(serialized.dig('books', 'authors').size).to eq(book.authors.size) + expect(serialized.dig('books', 'language', 'id')).to eq(book.language.id) + expect(serialized.dig('books', 'language', 'name')).to eq(book.language.name) + end + end + end +end diff --git a/spec/serializers/language_serializer_spec.rb b/spec/serializers/language_serializer_spec.rb new file mode 100644 index 0000000..ba3e959 --- /dev/null +++ b/spec/serializers/language_serializer_spec.rb @@ -0,0 +1,17 @@ +RSpec.describe LanguageSerializer, type: :serializer do + let(:language) { create(:language) } + + describe 'serialization' do + context 'when correct language is passed' do + subject { described_class.new(language) } + + it "return correct json response" do + serialized = JSON.parse(subject.to_json) + + expect(serialized['id']).to eq(language.id) + expect(serialized['slug']).to eq(language.slug) + expect(serialized['name']).to eq(language.name) + end + end + end +end