diff --git a/Gemfile b/Gemfile index b183c97..2db5717 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,8 @@ gem "jquery-rails" gem "sass-rails" gem "sqlite3" gem "uglifier" - +gem "jbuilder" +gem 'will_paginate' # Reduces boot times through caching; required in config/boot.rb gem "bootsnap", ">= 1.1.0", require: false @@ -34,6 +35,7 @@ group :development, :test do gem "rspec-rails" gem "rubocop" gem "simplecov" + gem "rails-controller-testing" end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 66f47ff..7121cca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,6 +126,8 @@ GEM ruby_parser (~> 3.5) i18n (1.0.0) concurrent-ruby (~> 1.0) + jbuilder (2.9.1) + activesupport (>= 4.2.0) jquery-rails (4.3.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -182,6 +184,10 @@ GEM bundler (>= 1.3.0) railties (= 5.2.0) sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.4) + actionpack (>= 5.0.1.x) + actionview (>= 5.0.1.x) + activesupport (>= 5.0.1.x) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -283,6 +289,7 @@ GEM websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) + will_paginate (3.1.7) xpath (3.0.0) nokogiri (~> 1.8) @@ -302,11 +309,13 @@ DEPENDENCIES factory_bot_rails faker haml-rails + jbuilder jquery-rails letter_opener listen pry-rails rails (= 5.2) + rails-controller-testing rails-erd rspec-rails rubocop @@ -317,6 +326,7 @@ DEPENDENCIES spring-commands-rspec sqlite3 uglifier + will_paginate BUNDLED WITH - 1.16.1 + 1.16.2 diff --git a/app/controllers/api/movies_controller.rb b/app/controllers/api/movies_controller.rb new file mode 100644 index 0000000..bdbeca5 --- /dev/null +++ b/app/controllers/api/movies_controller.rb @@ -0,0 +1,6 @@ +class Api::MoviesController < ApplicationController + def index + @with_details = params[:details] and params[:details] == 'true'; + @movies = Movie.all + end +end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100644 index 0000000..e2ba2dc --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,28 @@ +class CommentsController < ApplicationController + def create + @movie = Movie.find(params[:movie_id]) + @comment = @movie.comments.new(comment_params) do |c| + c.user = current_user + end + + if @comment.save() + redirect_back(fallback_location: root_path, notice: "Comment added") + else + redirect_back(fallback_location: root_path, alert: @comment.errors) + end + end + + def destroy + @comment = Comment.find(params[:id]) + if @comment.destroy + redirect_back(fallback_location: root_path, notice: "Comment deleted") + else + redirect_back(fallback_location: root_path, alert: @comment.errors) + end + end + + private + def comment_params + params.require(:comment).permit(:body) + end +end diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb index fe282aa..442bb5e 100644 --- a/app/controllers/movies_controller.rb +++ b/app/controllers/movies_controller.rb @@ -2,22 +2,26 @@ class MoviesController < ApplicationController before_action :authenticate_user!, only: [:send_info] def index - @movies = Movie.all.decorate + @movies = Movie.all.paginate(:page => params[:page], :per_page => 5).decorate end def show @movie = Movie.find(params[:id]) + if current_user + @user_comment_exists = @movie.comments.find_by_user_id(current_user.id) + @comment = @movie.comments.build unless @user_comment_exists + end end - + def send_info @movie = Movie.find(params[:id]) - MovieInfoMailer.send_info(current_user, @movie).deliver_now + SendMovieInfoJob.perform_later(current_user, @movie) redirect_back(fallback_location: root_path, notice: "Email sent with movie info") end def export file_path = "tmp/movies.csv" - MovieExporter.new.call(current_user, file_path) + CsvMoviesExportJob.perform_later(current_user, file_path) redirect_to root_path, notice: "Movies exported" end end diff --git a/app/controllers/rewards_controller.rb b/app/controllers/rewards_controller.rb new file mode 100644 index 0000000..584f5dc --- /dev/null +++ b/app/controllers/rewards_controller.rb @@ -0,0 +1,5 @@ +class RewardsController < ApplicationController + def index + @rewards = Rewards.instance().get_rewards + end +end diff --git a/app/decorators/movie_decorator.rb b/app/decorators/movie_decorator.rb index 77f1200..d2749ee 100644 --- a/app/decorators/movie_decorator.rb +++ b/app/decorators/movie_decorator.rb @@ -1,9 +1,12 @@ class MovieDecorator < Draper::Decorator delegate_all - def cover "http://lorempixel.com/100/150/" + %w[abstract nightlife transport].sample + "?a=" + SecureRandom.uuid end + + def self.collection_decorator_class + PaginatingDecorator + end end diff --git a/app/decorators/paginating_decorator.rb b/app/decorators/paginating_decorator.rb new file mode 100644 index 0000000..7c5c912 --- /dev/null +++ b/app/decorators/paginating_decorator.rb @@ -0,0 +1,3 @@ +class PaginatingDecorator < Draper::CollectionDecorator + delegate :current_page, :total_entries, :total_pages, :per_page, :offset +end \ No newline at end of file diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/csv_movies_export_job.rb b/app/jobs/csv_movies_export_job.rb new file mode 100644 index 0000000..952c722 --- /dev/null +++ b/app/jobs/csv_movies_export_job.rb @@ -0,0 +1,8 @@ +class CsvMoviesExportJob < ApplicationJob + queue_as :default + + def perform(user, file_path) + # Do something later + MovieExporter.new.call(user, file_path) + end +end diff --git a/app/jobs/send_movie_info_job.rb b/app/jobs/send_movie_info_job.rb new file mode 100644 index 0000000..3089f93 --- /dev/null +++ b/app/jobs/send_movie_info_job.rb @@ -0,0 +1,7 @@ +class SendMovieInfoJob < ApplicationJob + queue_as :default + + def perform(user, movie) + MovieInfoMailer.send_info(user, movie).deliver_now + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 0000000..ba4c454 --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,11 @@ +class Comment < ApplicationRecord + belongs_to :user + belongs_to :movie + + after_save :update_rewards + after_destroy :update_rewards + + def update_rewards + Rewards.instance().update_rewards + end +end diff --git a/app/models/movie.rb b/app/models/movie.rb index c57a9cb..66e4a59 100644 --- a/app/models/movie.rb +++ b/app/models/movie.rb @@ -14,4 +14,29 @@ class Movie < ApplicationRecord belongs_to :genre + has_many :comments + + validates_with TitleBracketsValidator + + def api_data + api_request = ApiRequest.new() + api_request.movie_details(self.title) + end + + def poster + (@api_data ||= api_data) + @api_data ? URI.join(ApiRequest::API_SERVER, @api_data['poster']) : nil + end + + def plot + (@api_data ||= api_data) + @api_data ? @api_data['plot'] : nil + end + + def rating + (@api_data ||= api_data) + @api_data ? @api_data['rating'] : nil + end end + + \ No newline at end of file diff --git a/app/models/title_brackets_validator.rb b/app/models/title_brackets_validator.rb new file mode 100644 index 0000000..858e08e --- /dev/null +++ b/app/models/title_brackets_validator.rb @@ -0,0 +1,33 @@ +class TitleBracketsValidator < ActiveModel::Validator + def validate(record) + validate_brackets(record, "(", ")") + validate_brackets(record, "{", "}") + validate_brackets(record, "[", "]") + end + + private + def validate_brackets(record, left_bracket, right_bracket) + count = 0 + if record.title.include?("#{left_bracket}#{right_bracket}") + record.errors.add :base, 'has invalid title' + return false + end + record.title.split("").each do |ch| + if ch == left_bracket + count += 1 + elsif ch == right_bracket + if (count == 0) + record.errors.add :base, 'has invalid title' + return false + else + count -= 1 + end + end + end + if (count == 0) + return true + end + record.errors.add :base, 'has invalid title' + return false + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 3d79cb0..a0cf7f2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,6 +22,7 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable + has_many :comments validates :phone_number, format: { with: /\A[+]?\d+(?>[- .]\d+)*\z/, allow_nil: true } end diff --git a/app/services/api_request.rb b/app/services/api_request.rb new file mode 100644 index 0000000..04a3359 --- /dev/null +++ b/app/services/api_request.rb @@ -0,0 +1,13 @@ +class ApiRequest + API_SERVER = "https://pairguru-api.herokuapp.com" + + def movie_details(movie_title) + uri = URI.parse(URI.escape("#{API_SERVER}/api/v1/movies/#{movie_title}")) + Net::HTTP.start(uri.host) do |http| + request = Net::HTTP::Get.new(uri.request_uri) + response = http.request request + json_body = JSON.parse(response.body) + Net::HTTPSuccess and json_body.key?('data') ? json_body['data']['attributes'] : nil + end + end +end diff --git a/app/services/rewards.rb b/app/services/rewards.rb new file mode 100644 index 0000000..722b9b7 --- /dev/null +++ b/app/services/rewards.rb @@ -0,0 +1,22 @@ +class Rewards + include Singleton + attr_accessor :rewards + + def initialize + update_rewards + end + + def update_rewards + summary = Comment + .where(:created_at => (Date.today-7.days).beginning_of_day..Date.today.end_of_day) + .select(:user_id, "count(id) as count") + .group(:user_id) + .order("count desc") + .limit(10) + @rewards = summary.map{|it| {email: User.find(it["user_id"]).email, count: it["count"]}} + end + + def get_rewards + @rewards + end +end diff --git a/app/views/api/movies/index.json.jbuilder b/app/views/api/movies/index.json.jbuilder new file mode 100644 index 0000000..781cc79 --- /dev/null +++ b/app/views/api/movies/index.json.jbuilder @@ -0,0 +1,11 @@ +json.array! @movies do |movie| + json.id movie.id + json.title movie.title + if @with_details + json.genre do + json.id movie.genre.id + json.name movie.genre.name + json.count_movies movie.genre.movies.count + end + end +end \ No newline at end of file diff --git a/app/views/layouts/_navigation_links.html.haml b/app/views/layouts/_navigation_links.html.haml index 80ac251..10f6440 100644 --- a/app/views/layouts/_navigation_links.html.haml +++ b/app/views/layouts/_navigation_links.html.haml @@ -1,4 +1,5 @@ %li= link_to 'Movies', movies_path %li= link_to 'Genres', genres_path +%li= link_to 'Rewards', rewards_path - if user_signed_in? %li= link_to 'Export', export_movies_path diff --git a/app/views/movies/_comments_table.html.haml b/app/views/movies/_comments_table.html.haml new file mode 100644 index 0000000..b821201 --- /dev/null +++ b/app/views/movies/_comments_table.html.haml @@ -0,0 +1,17 @@ +%table.table.table-striped + %caption Comments + %thead + %tr + %td created_at + %td email + %td comment + %td + %tbody + - @movie.comments.order(id: :desc).each_with_index do |comment, i| + - if comment.id #prevent to show empty comment from build (when comment for current_user doesn't exists) + %tr + %td= comment.created_at.strftime("%Y-%m-%d %H:%M:%S") + %td= comment.user.email + %td= comment.body + %td= link_to 'delete', [@movie, comment], :method => :delete, data: {confirm: "Are you sure?"} if current_user == comment.user + \ No newline at end of file diff --git a/app/views/movies/_form.html.haml b/app/views/movies/_form.html.haml new file mode 100644 index 0000000..4be11d9 --- /dev/null +++ b/app/views/movies/_form.html.haml @@ -0,0 +1,5 @@ += form_for [@movie, @comment], method: :post do |f| + = f.label 'Comment' + %div.fields + = f.text_area :body, class: 'form-control' + = f.submit "Add comment" \ No newline at end of file diff --git a/app/views/movies/_movies_table.html.haml b/app/views/movies/_movies_table.html.haml index ff259e0..62ae559 100644 --- a/app/views/movies/_movies_table.html.haml +++ b/app/views/movies/_movies_table.html.haml @@ -13,3 +13,11 @@ = link_to movie.genre.name, movies_genre_path(movie.genre) = ' (' + movie.released_at.to_s + ')' %p= movie.description + + %img{ src: movie.poster } + + %h4 Plot + %p= movie.plot + + %h4 Rating + %p= movie.rating \ No newline at end of file diff --git a/app/views/movies/index.html.haml b/app/views/movies/index.html.haml index 87d2c64..aa3fafd 100644 --- a/app/views/movies/index.html.haml +++ b/app/views/movies/index.html.haml @@ -1,4 +1,5 @@ .row .col-md-8.col-md-offset-2.col-lg-8.col-lg-offset-2 %h1.text-center Movies + = will_paginate @movies = render 'movies_table', movies: @movies diff --git a/app/views/movies/show.html.haml b/app/views/movies/show.html.haml index d51ef79..736eb89 100644 --- a/app/views/movies/show.html.haml +++ b/app/views/movies/show.html.haml @@ -1,5 +1,21 @@ %h1= @movie.title .jumbotron = @movie.description + + - if user_signed_in? and !@user_comment_exists + = render "form" + %hr + - if @movie.comments.count > 0 + = render "comments_table" + + %hr + %div.text-center + %img{ src: @movie.poster } + %hr + %h4 Plot + %p= @movie.plot + %h4 Rating + %p= @movie.rating + - if user_signed_in? %p= link_to 'Email me details about this movie', send_info_movie_path(@movie), class: 'btn btn-sm btn-default' diff --git a/app/views/rewards/index.html.haml b/app/views/rewards/index.html.haml new file mode 100644 index 0000000..baf0968 --- /dev/null +++ b/app/views/rewards/index.html.haml @@ -0,0 +1,14 @@ +- if @rewards + %table.table.table-striped + %thead + %tr + %td # + %td email + %td comments count + %td + %tbody + - @rewards.each_with_index do |reward, i| + %tr + %td= i + 1 + %td= reward[:email] + %td= reward[:count] \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 27d4929..01ba75a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,7 +10,6 @@ module Pairguru class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.0 - # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers # -- all .rb files in that directory are automatically loaded after loading diff --git a/config/routes.rb b/config/routes.rb index 5761421..3de4a1d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,7 +7,13 @@ get "movies" end end + + resources :rewards, only: :index do + end + resources :movies, only: [:index, :show] do + resources :comments + member do get :send_info end @@ -15,4 +21,9 @@ get :export end end + + namespace :api, only: :index, :defaults => {:format => :json} do + resources :movies do + end + end end diff --git a/db/migrate/20190710173702_create_comments.rb b/db/migrate/20190710173702_create_comments.rb new file mode 100644 index 0000000..27e4317 --- /dev/null +++ b/db/migrate/20190710173702_create_comments.rb @@ -0,0 +1,11 @@ +class CreateComments < ActiveRecord::Migration[5.2] + def change + create_table :comments do |t| + t.text :body + t.references :movie, index: true + t.references :user, index: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 21dbb29..a026982 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,12 +10,22 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170406223727) do +ActiveRecord::Schema.define(version: 2019_07_10_173702) do + + create_table "comments", force: :cascade do |t| + t.text "body" + t.integer "movie_id" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["movie_id"], name: "index_comments_on_movie_id" + t.index ["user_id"], name: "index_comments_on_user_id" + end create_table "genres", force: :cascade do |t| t.string "name" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "movies", force: :cascade do |t| @@ -24,8 +34,8 @@ t.datetime "released_at" t.string "avatar" t.integer "genre_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["genre_id"], name: "index_movies_on_genre_id" end @@ -40,8 +50,8 @@ t.datetime "last_sign_in_at" t.string "current_sign_in_ip" t.string "last_sign_in_ip" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "phone_number" t.string "confirmation_token" t.datetime "confirmed_at" diff --git a/spec/controllers/api/movies_spec.rb b/spec/controllers/api/movies_spec.rb new file mode 100644 index 0000000..9e29d99 --- /dev/null +++ b/spec/controllers/api/movies_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' +describe Api::MoviesController, :type => :request do + describe "GET #index" do + let(:movie) {build(:movie)} + + before do + movie.save + end + + it "render template" do + get "/api/movies" + expect(response).to render_template(:index) + end + + it "returns http success" do + get "/api/movies" + expect(response).to have_http_status(:success) + end + + it "response with JSON body containing expected Movie attributes" do + get "/api/movies" + hash_body = JSON.parse(response.body) + expect(hash_body[0]).to match({id: movie.id, title: movie.title}.as_json) + end + + it "response with JSON body containing expected Movie attributes with genre data" do + get "/api/movies?details=true" + hash_body = JSON.parse(response.body) + expect(hash_body[0]).to include("genre") + end + end +end \ No newline at end of file diff --git a/spec/factories/comment.rb b/spec/factories/comment.rb new file mode 100644 index 0000000..c64393c --- /dev/null +++ b/spec/factories/comment.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :comment do + created_at {Time.now} + body {"Comment body"} + user + movie + end +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb new file mode 100644 index 0000000..a2cd400 --- /dev/null +++ b/spec/factories/user.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :user do + email {"user@test.pl"} + password {"password"} + password_confirmation {"password"} + confirmed_at {Time.now} + end +end + diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb new file mode 100644 index 0000000..9fd938d --- /dev/null +++ b/spec/models/comment_spec.rb @@ -0,0 +1,15 @@ +require "rails_helper" + +describe Comment do + let(:comment) { build(:comment) } + + it "triggers update_rewards on save" do + expect(comment).to receive(:update_rewards) + comment.save + end + + it "triggers update_rewards on destroy" do + expect(comment).to receive(:update_rewards) + comment.destroy + end +end diff --git a/spec/models/movie_spec.rb b/spec/models/movie_spec.rb new file mode 100644 index 0000000..80e81c7 --- /dev/null +++ b/spec/models/movie_spec.rb @@ -0,0 +1,35 @@ +require "rails_helper" + +describe Movie do + let(:movie) { build(:movie) } + + it "movie should be have a poster" do + movie.title = "Deadpool" + expect(movie.poster().to_s).to include(".jpg") + end + + it "movie not should be have a poster" do + movie.title = "Deadpol" + expect(movie.poster()).to eq(nil) + end + + it "movie should be have a plot" do + movie.title = "Deadpool" + expect(movie.plot()).to_not eq(nil) + end + + it "movie not should be have a plot" do + movie.title = "Deadpol" + expect(movie.plot()).to eq(nil) + end + + it "movie should be have a rating" do + movie.title = "Deadpool" + expect(movie.rating()).to_not eq(nil) + end + + it "movie not should be have a rating" do + movie.title = "Deadpol" + expect(movie.rating()).to eq(nil) + end +end diff --git a/spec/requests/rewards.rb b/spec/requests/rewards.rb new file mode 100644 index 0000000..347bba9 --- /dev/null +++ b/spec/requests/rewards.rb @@ -0,0 +1,11 @@ +require "rails_helper" + +describe "Rewards requests", type: :request do + describe "users rewards" do + it "displays table" do + visit "/rewards" + expect(page).to have_css(".table-striped") + end + end +end + diff --git a/spec/services/rewards_spec.rb b/spec/services/rewards_spec.rb new file mode 100644 index 0000000..364cc77 --- /dev/null +++ b/spec/services/rewards_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +describe "Rewards" do + let(:comment) {build(:comment)} + + before do + Singleton.__init__(Rewards) + end + + it "rewards list should be not empty" do + comment.created_at = Time.now + comment.save + expect(Rewards.instance.rewards).to_not be_empty + end + + it "rewards list should be empty" do + comment.created_at = Time.now - 10.days + comment.save + expect(Rewards.instance.rewards).to be_empty + end +end diff --git a/spec/validators/title_brackets_validator_spec.rb b/spec/validators/title_brackets_validator_spec.rb index 8c4f9d0..75ef602 100644 --- a/spec/validators/title_brackets_validator_spec.rb +++ b/spec/validators/title_brackets_validator_spec.rb @@ -1,87 +1,87 @@ -# require "rails_helper" - -# describe TitleBracketsValidator do -# subject { Validatable.new(title: title) } - -# shared_examples "has valid title" do -# it "should be valid" do -# expect(subject).to be_valid -# end -# end - -# shared_examples "has invalid title" do -# it "should not be valid" do -# expect(subject).not_to be_valid -# end -# end - -# context "with curly brackets" do -# let(:title) { "The Fellowship of the Ring {Peter Jackson}" } -# it_behaves_like "has valid title" -# end - -# context "with square brackets" do -# let(:title) { "The Fellowship of the Ring [Lord of The Rings]" } -# it_behaves_like "has valid title" -# end - -# context "with not closed brackets" do -# let(:title) { "The Fellowship of the Ring (2001" } -# it_behaves_like "has invalid title" -# end - -# context "with not opened brackets" do -# let(:title) { "The Fellowship of the Ring 2001)" } -# it_behaves_like "has invalid title" -# end - -# context "with not too much closing brackets" do -# let(:title) { "The Fellowship of the Ring (2001) - 2003)" } -# it_behaves_like "has invalid title" -# end - -# context "with not too much opening brackets" do -# let(:title) { "The Fellowship of the Ring (2001 - (2003)" } -# it_behaves_like "has invalid title" -# end - -# context "with empty brackets" do -# let(:title) { "The Fellowship of the Ring ()" } -# it_behaves_like "has invalid title" -# end - -# context "with brackets in wrong order" do -# let(:title) { "The Fellowship of the )Ring(" } -# it_behaves_like "has invalid title" -# end - -# context "with matching brackets" do -# let(:title) { "The Fellowship of the Ring (2001)" } -# it_behaves_like "has valid title" -# end - -# context "with multiple matching brackets" do -# let(:title) { "The Fellowship of the Ring [Lord of The Rings] (2001) {Peter Jackson}" } -# it_behaves_like "has valid title" -# end - -# context "with nested matching brackets" do -# let(:title) { "The Fellowship of the Ring [Lord of The Rings {Peter Jackson}] (2012)" } -# it_behaves_like "has valid title" -# end - -# context "with no brackets" do -# let(:title) { "Lord of The Rings" } -# it_behaves_like "has valid title" -# end -# end - -# class Validatable -# include ActiveModel::Validations -# validates_with TitleBracketsValidator -# attr_accessor :title - -# def initialize(title:) -# @title = title -# end -# end +require "rails_helper" + +describe TitleBracketsValidator do + subject { Validatable.new(title) } + + shared_examples "has valid title" do + it "should be valid" do + expect(subject).to be_valid + end + end + + shared_examples "has invalid title" do + it "should not be valid" do + expect(subject).not_to be_valid + end + end + + context "with curly brackets" do + let(:title) { "The Fellowship of the Ring {Peter Jackson}" } + it_behaves_like "has valid title" + end + + context "with square brackets" do + let(:title) { "The Fellowship of the Ring [Lord of The Rings]" } + it_behaves_like "has valid title" + end + + context "with not closed brackets" do + let(:title) { "The Fellowship of the Ring (2001" } + it_behaves_like "has invalid title" + end + + context "with not opened brackets" do + let(:title) { "The Fellowship of the Ring 2001)" } + it_behaves_like "has invalid title" + end + + context "with not too much closing brackets" do + let(:title) { "The Fellowship of the Ring (2001) - 2003)" } + it_behaves_like "has invalid title" + end + + context "with not too much opening brackets" do + let(:title) { "The Fellowship of the Ring (2001 - (2003)" } + it_behaves_like "has invalid title" + end + + context "with empty brackets" do + let(:title) { "The Fellowship of the Ring ()" } + it_behaves_like "has invalid title" + end + + context "with brackets in wrong order" do + let(:title) { "The Fellowship of the )Ring(" } + it_behaves_like "has invalid title" + end + + context "with matching brackets" do + let(:title) { "The Fellowship of the Ring (2001)" } + it_behaves_like "has valid title" + end + + context "with multiple matching brackets" do + let(:title) { "The Fellowship of the Ring [Lord of The Rings] (2001) {Peter Jackson}" } + it_behaves_like "has valid title" + end + + context "with nested matching brackets" do + let(:title) { "The Fellowship of the Ring [Lord of The Rings {Peter Jackson}] (2012)" } + it_behaves_like "has valid title" + end + + context "with no brackets" do + let(:title) { "Lord of The Rings" } + it_behaves_like "has valid title" + end +end + +class Validatable + include ActiveModel::Validations + validates_with TitleBracketsValidator + attr_accessor :title + + def initialize(title) + @title = title + end +end