From 707e219ec4c9a2f1132fb716ecfe119c61d15723 Mon Sep 17 00:00:00 2001 From: gangelo Date: Tue, 16 Jan 2024 20:13:56 -0500 Subject: [PATCH] Implement dsu project create --- current_project.bak | 2 +- lib/dsu/models/project.rb | 91 +++++++-- .../presenters/project/create_presenter.rb | 2 +- lib/dsu/presenters/project/use_presenter.rb | 70 +++++++ lib/dsu/subcommands/project.rb | 19 +- lib/dsu/support/ask.rb | 13 +- lib/dsu/support/color_themable.rb | 2 +- lib/dsu/support/command_hookable.rb | 2 +- lib/dsu/support/fileable.rb | 3 +- lib/dsu/support/project_file_system.rb | 28 ++- lib/dsu/views/project/create.rb | 19 +- lib/dsu/views/project/use.rb | 94 +++++++++ lib/locales/en/miscellaneous.yml | 2 + lib/locales/en/subcommands.yml | 50 +++-- spec/dsu/features/dsu_theme_features_spec.rb | 4 +- spec/dsu/models/project_spec.rb | 178 ++++++++++++++---- spec/dsu/support/project_file_system_spec.rb | 12 +- spec/factories/projects.rb | 18 ++ spec/fixtures/files/current_project.json | 2 +- 19 files changed, 512 insertions(+), 99 deletions(-) create mode 100644 lib/dsu/presenters/project/use_presenter.rb create mode 100644 lib/dsu/views/project/use.rb diff --git a/current_project.bak b/current_project.bak index 8ffd1663..b33cfdd7 100644 --- a/current_project.bak +++ b/current_project.bak @@ -1,4 +1,4 @@ { "version": 20230613121411, - "project": "default" + "project_name": "default" } diff --git a/lib/dsu/models/project.rb b/lib/dsu/models/project.rb index 576289e1..16595ac9 100644 --- a/lib/dsu/models/project.rb +++ b/lib/dsu/models/project.rb @@ -4,8 +4,9 @@ require_relative '../crud/json_file' require_relative '../migration/version' -require_relative '../models/entry_group' +require_relative '../models/configuration' require_relative '../services/project/hydrator_service' +require_relative '../support/descriptable' require_relative '../support/fileable' require_relative '../support/project_file_system' require_relative '../validators/description_validator' @@ -17,8 +18,9 @@ module Models # This class represents a project. A project is a collection of entry groups. class Project include ActiveModel::Model - include Support::ProjectFileSystem + include Support::Descriptable include Support::Fileable + include Support::ProjectFileSystem VERSION = Migration::VERSION MIN_PROJECT_NAME_LENGTH = 2 @@ -65,6 +67,20 @@ def create! end alias save! create! + def current_project? + project_name == self.class.current_project_name + end + + def default! + return if default_project? + + self.class.default!(project: self) + end + + def default_project? + project_name == self.class.default_project_name + end + # def delete # self.class.delete(project_name: project_name) # end @@ -110,9 +126,21 @@ def update! self.class.update!(project_name: project_name, description: description, version: version, options: options) end + def use! + return if current_project? + + self.class.use!(project: self) + end + class << self delegate :project_folder_for, to: Support::Fileable + def all + project_metadata.map do |metadata| + find(project_name: metadata[:project_name]) + end + end + def create(project_name:, description: nil, options: {}) Models::Project.new(project_name: project_name, description: description, options: options).tap do |project| project.validate! @@ -131,15 +159,22 @@ def create!(project_name:, description: nil, options: {}) end def current_project - current_project_file = Support::Fileable.current_project_file - Crud::JsonFile.read!(file_path: current_project_file).fetch(:project).tap do |project_name| - # description = "#{project_name.capitalize} project}" - # unless project_exist?(project_name: project_name) - # create!(project_name: project_name, description: description) - # end + find(project_name: current_project_name) + end + + def default!(project:) + project.validate! + + Models::Configuration.new.tap do |configuration| + configuration.default_project = project.project_name + configuration.save! end end + def default_project + find(project_name: default_project_name) + end + # def all # entry_groups.filter_map do |file_path| # entry_group_file_name = File.basename(file_path) @@ -169,17 +204,17 @@ def current_project # # superclass.delete!(file_path: project_folder_for(project_name: project_name)) # end - def project_exist?(project_name:) - Dir.exist?(project_folder_for(project_name: project_name)) - end - alias project_persisted? project_exist? + # def project_exist?(project_name:) + # Dir.exist?(project_folder_for(project_name: project_name)) + # end + # alias project_persisted? project_exist? - def file_exist?(project_name:) - return false unless project_exist?(project_name: project_name) + # def file_exist?(project_name:) + # return false unless project_exist?(project_name: project_name) - File.exist?(current_project_file_path_for(current_project_file: project_folder_for(project_name: project_name))) - end - alias file_persisted? file_exist? + # File.exist?(current_project_file_path_for(current_project_file: project_folder_for(project_name: project_name))) + # end + # alias file_persisted? file_exist? # def entry_groups(between:) # entry_group_times(between: between).filter_map do |time| @@ -188,13 +223,13 @@ def file_exist?(project_name:) # end def find(project_name:) - unless project_exist?(project_name: project_name) + unless project_folder_exist?(project_name: project_name) raise I18n.t('models.project.errors.does_not_exist', project_name: project_name) end - project_file_path = current_project_file_path_for(current_project_file: current_project_file) + project_file_path = project_file_for(project_name: project_name) - unless file_exist?(project_name: project_name) + unless project_file_exist?(project_name: project_name) raise I18n.t('models.project.errors.project_file_not_exist', project_file: project_file_path) end @@ -202,6 +237,15 @@ def find(project_name:) Services::Project::HydratorService.new(project_hash: project_hash).call end + def find_by_number(project_number:) + project = project_metadata.find do |metadata| + metadata[:project_number] == project_number + end + return unless project + + find(project_name: project[:project_name]) + end + def find_or_create(project_name:) find_or_initialize(project_name: project_name).tap do |project| project.write! unless project.persisted? @@ -258,6 +302,13 @@ def update(project_name:, description:, version:, options:) def update!(project_name:, description:, version:, options:) end + def use!(project:) + project.validate! + + current_project_hash = { version: project.version, project_name: project.project_name } + Crud::JsonFile.write!(file_data: current_project_hash, file_path: current_project_file) + end + private def current_project_file_path_for(current_project_file:) diff --git a/lib/dsu/presenters/project/create_presenter.rb b/lib/dsu/presenters/project/create_presenter.rb index 1d0d8f7c..48843afb 100644 --- a/lib/dsu/presenters/project/create_presenter.rb +++ b/lib/dsu/presenters/project/create_presenter.rb @@ -15,7 +15,7 @@ def initialize(project_name:, description:, options: {}) @project = Models::Project.new(project_name: project_name, description: description, options: options) end - def render(response:) + def respond(response:) return false unless response project.create! diff --git a/lib/dsu/presenters/project/use_presenter.rb b/lib/dsu/presenters/project/use_presenter.rb new file mode 100644 index 00000000..17e7d857 --- /dev/null +++ b/lib/dsu/presenters/project/use_presenter.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require_relative '../../models/project' +require_relative '../base_presenter_ex' + +module Dsu + module Presenters + module Project + class UsePresenter < BasePresenterEx + def initialize(project_name_or_number:, options: {}) + super(options: options) + + @project_name_or_number = project_name_or_number + end + + def respond(response:) + return false unless response + + project.use! + end + + def project + @project ||= if project_name? + Dsu::Models::Project.find(project_name: project_name) + elsif project_number? + Dsu::Models::Project.find_by_number(project_number: project_number) + elsif project_default? + Dsu::Models::Project.default_project + end + end + + def project_does_not_exist? + !project.exist? + end + + def project_errors? + project.invalid? + end + + private + + attr_reader :options, :project_name_or_number + + def project_name? + !(project_number? || project_default?) + end + + def project_number? + project_name_or_number =~ /\A\d+\z/ + end + + def project_default? + project_name_or_number.blank? + end + + def project_name + return unless project_name? + + project_name_or_number + end + + def project_number + return -1 unless project_number? + + project_name_or_number.to_i + end + end + end + end +end diff --git a/lib/dsu/subcommands/project.rb b/lib/dsu/subcommands/project.rb index 6e776eae..afa7a570 100644 --- a/lib/dsu/subcommands/project.rb +++ b/lib/dsu/subcommands/project.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true require_relative '../presenters/project/create_presenter' +require_relative '../presenters/project/use_presenter' require_relative '../views/project/create' +require_relative '../views/project/use' require_relative '../views/shared/error' require_relative 'base_subcommand' @@ -29,7 +31,7 @@ def create end desc I18n.t('subcommands.project.delete.desc'), I18n.t('subcommands.project.delete.usage') - long_desc I18n.t('subcommands.liet.delete.long_desc') + long_desc I18n.t('subcommands.project.delete.long_desc') option :project_name, type: :string, required: true, aliases: '-n', banner: 'PROJECT_NAME' option :prompts, type: :hash, default: {}, hide: true, aliases: '-p' def delete @@ -38,7 +40,7 @@ def delete end desc I18n.t('subcommands.project.list.desc'), I18n.t('subcommands.project.list.usage') - long_desc I18n.t('subcommands.liet.list.long_desc') + long_desc I18n.t('subcommands.project.list.long_desc') option :prompts, type: :hash, default: {}, hide: true, aliases: '-p' def list # Views::Import.new(presenter: all_presenter(import_file_path: options[:import_file], @@ -46,7 +48,7 @@ def list end desc I18n.t('subcommands.project.show.desc'), I18n.t('subcommands.project.show.usage') - long_desc I18n.t('subcommands.liet.show.long_desc') + long_desc I18n.t('subcommands.project.show.long_desc') option :project_name, type: :string, required: true, aliases: '-n', banner: 'PROJECT_NAME' option :prompts, type: :hash, default: {}, hide: true, aliases: '-p' def show @@ -55,12 +57,13 @@ def show end desc I18n.t('subcommands.project.use.desc'), I18n.t('subcommands.project.use.usage') - long_desc I18n.t('subcommands.liet.use.long_desc') - option :project_name, type: :string, required: true, aliases: '-n', banner: 'PROJECT_NAME' + long_desc I18n.t('subcommands.project.use.long_desc') option :prompts, type: :hash, default: {}, hide: true, aliases: '-p' - def use - # Views::Import.new(presenter: all_presenter(import_file_path: options[:import_file], - # options: options)).render + def use(project_name_or_number = nil) + options = configuration.to_h.merge(self.options).with_indifferent_access + presenter = Presenters::Project::UsePresenter.new(project_name_or_number: project_name_or_number, + options: options) + Views::Project::Use.new(presenter: presenter, options: options).render end end end diff --git a/lib/dsu/support/ask.rb b/lib/dsu/support/ask.rb index 75c3d08b..d0cd314f 100644 --- a/lib/dsu/support/ask.rb +++ b/lib/dsu/support/ask.rb @@ -1,13 +1,20 @@ # frozen_string_literal: true +require 'io/console' require 'thor' module Dsu module Support module Ask - def ask(prompt) - options = {} - Thor::LineEditor.readline(prompt, options) + def ask_while(prompt, options: {}) + loop do + print prompt + char = $stdin.getch + puts char + return char if yield(char) + + char + end end def yes?(prompt, options: {}) diff --git a/lib/dsu/support/color_themable.rb b/lib/dsu/support/color_themable.rb index a1038621..23de965c 100644 --- a/lib/dsu/support/color_themable.rb +++ b/lib/dsu/support/color_themable.rb @@ -8,7 +8,7 @@ module ColorThemable def prompt_with_options(prompt:, options:) # HACK: This module needs to be refactored to be more generic. target_color_theme = defined?(color_theme) ? color_theme : self - options = "[#{options.join('/')}]" + options = "[#{options.join(',')}]" "#{apply_theme(prompt, theme_color: target_color_theme.prompt)} " \ "#{apply_theme(options, theme_color: target_color_theme.prompt_options)}" \ "#{apply_theme('>', theme_color: target_color_theme.prompt)}" diff --git a/lib/dsu/support/command_hookable.rb b/lib/dsu/support/command_hookable.rb index a968ca9d..8c70e585 100644 --- a/lib/dsu/support/command_hookable.rb +++ b/lib/dsu/support/command_hookable.rb @@ -44,7 +44,7 @@ def display_dsu_footer private def project - Models::Project.current_project + Models::Project.current_project_name end def suspend_header?(args, _options) diff --git a/lib/dsu/support/fileable.rb b/lib/dsu/support/fileable.rb index 9de75057..6a5aa25b 100644 --- a/lib/dsu/support/fileable.rb +++ b/lib/dsu/support/fileable.rb @@ -26,7 +26,8 @@ def config_path # Entries def entries_folder - File.join(projects_folder, 'entries') + project_folder = project_folder_for(project_name: Models::Project.current_project_name) + File.join(project_folder, 'entries') end def entries_file_name(time:, file_name_format: nil) diff --git a/lib/dsu/support/project_file_system.rb b/lib/dsu/support/project_file_system.rb index 238f85cc..71664ff3 100644 --- a/lib/dsu/support/project_file_system.rb +++ b/lib/dsu/support/project_file_system.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true require 'fileutils' +require 'pathname' require_relative '../crud/json_file' require_relative '../migration/version' +require_relative '../models/configuration' require_relative 'fileable' module Dsu @@ -26,8 +28,14 @@ def project_initialized? module ClassMethods include Fileable - def current_project - Crud::JsonFile.read!(file_path: current_project_file).fetch(:project) + # Returns the currently selected (used) project name + # from dsu/current_project.json + def current_project_name + Crud::JsonFile.read!(file_path: current_project_file).fetch(:project_name) + end + + def default_project_name + Models::Configuration.new.default_project end def initialize_project(project_name:) @@ -37,7 +45,7 @@ def initialize_project(project_name:) unless current_project_file_exist? file_data = { version: Dsu::Migration::VERSION, - project_name: 'default' + project_name: default_project_name } Crud::JsonFile.write!(file_data: file_data, file_path: current_project_file) end @@ -75,6 +83,20 @@ def projects_folder_exist? def project_folder_exist?(project_name:) Dir.exist?(project_folder_for(project_name: project_name)) end + + def project_metadata + Pathname.new(projects_folder).children + .select(&:directory?) + .map(&:basename) + .map(&:to_s).each_with_index.with_object([]) do |(project_name, index), array| + array << { + project_number: index, + project_name: project_name, + current_project: project_name == current_project_name, + default_projet: project_name == default_project_name + } + end + end end end end diff --git a/lib/dsu/views/project/create.rb b/lib/dsu/views/project/create.rb index 26022bd3..ff4f4507 100644 --- a/lib/dsu/views/project/create.rb +++ b/lib/dsu/views/project/create.rb @@ -22,10 +22,10 @@ def render return display_project_already_exists if presenter.project_already_exists? response = display_project_create_prompt - if presenter.render response: response + if presenter.respond response: response display_project_created_message else - display_cancelled_message + display_project_cancelled_message end rescue StandardError => e puts apply_theme(e.message, theme_color: color_theme.error) @@ -39,17 +39,24 @@ def project_name presenter.project.project_name end - def display_cancelled_message - message = I18n.t('subcommands.project.create.messages.cancelled', project_name: project_name) + def display_project_cancelled_message + message = I18n.t('subcommands.project.messages.cancelled', project_name: project_name) puts apply_theme(message, theme_color: color_theme.info) end def display_project_create_prompt - yes?(prompt_with_options(prompt: create_prompt, options: create_prompt_options), options: options) + response = ask_while(prompt_with_options(prompt: create_prompt, + options: create_prompt_options), options: options) do |input| + message = I18n.t('information.input.try_again', options: create_prompt_options.join(',')) + puts apply_theme(message, theme_color: color_theme.info) unless create_prompt_options.include?(input) + create_prompt_options.include?(input) + end + response == create_prompt_options.first end def display_project_created_message - puts apply_theme(I18n.t('subcommands.project.create.messages.created', project_name: project_name), theme_color: color_theme.info) + message = I18n.t('subcommands.project.create.messages.created', project_name: project_name) + puts apply_theme(message, theme_color: color_theme.success) end def display_project_errors diff --git a/lib/dsu/views/project/use.rb b/lib/dsu/views/project/use.rb new file mode 100644 index 00000000..f2f22695 --- /dev/null +++ b/lib/dsu/views/project/use.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require_relative '../../env' +require_relative '../../models/color_theme' +require_relative '../../support/ask' +require_relative '../../support/color_themable' + +module Dsu + module Views + module Project + class Use + include Support::Ask + include Support::ColorThemable + + def initialize(presenter:, options: {}) + @presenter = presenter + @options = options&.dup || {} + @color_theme = Models::ColorTheme.find(theme_name: theme_name) + end + + def render + return display_project_errors if presenter.project_errors? + return display_project_does_not_exists if presenter.project_does_not_exist? + + response = display_project_use_prompt + if presenter.respond response: response + display_using_project_message + else + display_use_project_cancelled_message + end + rescue StandardError => e + puts apply_theme(e.message, theme_color: color_theme.error) + puts e.backtrace_locations.join("\n") if Dsu.env.local? + end + + private + + attr_reader :presenter, :color_theme, :options + + def project_name + presenter.project.project_name + end + + def project_description + presenter.project.description + end + + def display_project_use_prompt + response = ask_while(prompt_with_options(prompt: use_prompt, + options: use_prompt_options), options: options) do |input| + message = I18n.t('information.input.try_again', options: use_prompt_options.join(',')) + puts apply_theme(message, theme_color: color_theme.info) unless use_prompt_options.include?(input) + use_prompt_options.include?(input) + end + response == use_prompt_options.first + end + + def display_use_project_cancelled_message + message = I18n.t('subcommands.project.messages.cancelled', project_name: project_name) + puts apply_theme(message, theme_color: color_theme.info) + end + + def display_project_errors + presenter.project.errors.full_messages.each do |error| + puts apply_theme(error, theme_color: color_theme.error) + end + end + + def display_project_does_not_exists + message = I18n.t('subcommands.project.messages.does_not_exist', project_name: project_name) + puts apply_theme(message, theme_color: color_theme.error) + end + + def display_using_project_message + message = I18n.t('subcommands.project.use.messages.using_project', project_name: project_name) + puts apply_theme(message, theme_color: color_theme.success) + end + + def use_prompt + I18n.t('subcommands.project.use.prompts.use_confirm', + project_name: project_name, description: project_description) + end + + def use_prompt_options + I18n.t('subcommands.project.use.prompts.use_options') + end + + def theme_name + @theme_name ||= options.fetch(:theme_name, Models::Configuration.new.theme_name) + end + end + end + end +end diff --git a/lib/locales/en/miscellaneous.yml b/lib/locales/en/miscellaneous.yml index ba3f0f0f..36c2aee1 100644 --- a/lib/locales/en/miscellaneous.yml +++ b/lib/locales/en/miscellaneous.yml @@ -21,6 +21,8 @@ en: information: dates: through: "%{from} thru %{to}" + input: + try_again: "Please try again. Valid options are [%{options}]." migrations: error: failed: "Error running migrations: %{message}" diff --git a/lib/locales/en/subcommands.yml b/lib/locales/en/subcommands.yml index d016fb6c..111b6a0c 100644 --- a/lib/locales/en/subcommands.yml +++ b/lib/locales/en/subcommands.yml @@ -287,7 +287,7 @@ en: export_dates_confirm: Export all the entries for %{from} thru %{to} (%{count} entry groups)? options: - y - - N + - n import: dates: desc: dates|dd OPTIONS @@ -379,7 +379,7 @@ en: import_dates_confirm: Import all the entry groups for %{from} thru %{to} (%{count} entry groups)? options: - y - - N + - n list: date: desc: date|d DATE|MNEMONIC @@ -496,13 +496,16 @@ en: $ dsu p c -n "My project" messages: created: Created project "%{project_name}". - cancelled: Cancelled. already_exists: Project "%{project_name}" already exists. prompts: create_confirm: Create project "%{project_name}"? create_options: - y - - N + - n + use_confirm: Use project "%{project_name}"? + use_options: + - y + - n delete: desc: delete|d OPTIONS usage: Permanently deletes the DSU project indicated by the given OPTIONS @@ -557,24 +560,45 @@ en: $ dsu p s -n "My project" use: - desc: use|u OPTIONS - usage: Selects the DSU project to use, based on the given OPTIONS + desc: use|u [PROJECT_NAME|PROJECT_NUMBER] + usage: Selects the DSU project to use, based on the given PROJECT_NAME or PROJECT_NUMBER long_desc: | - Selects the DSU project to use, based on the given OPTIONS. + Selects the DSU project to use, based on the given PROJECT_NAME or PROJECT_NUMBER. - dsu project use OPTIONS + dsu project use [PROJECT_NAME|PROJECT_NUMBER] - dsu p u OPTIONS + dsu p u [PROJECT_NAME|PROJECT_NUMBER] - OPTIONS: + NOTES - -n|--name PROJECT_NAME: Opeional. The name of the project to use. If PRONECT_NAME is not provided, the default project will be used.s + PROJECT_NAME|PROJECT_NUMBER are optional. The project name or project number of the project to use. If PRONECT_NAME is not provided, the default project will be used. If PROJECT_NUMBER is used, the project equating to the nth project in the list of projects will be used (see `dsu project list`). EXAMPLES: - $ dsu project use -n "My project" + These will use the default project. + $ dsu project use + + $ dsu p u + + These will use the project named "My project". + $ dsu project use "My project" + + $ dsu p u "My project" - $ dsu p u -n "My project" + These will use the 2nd project in the list of projects. + $ dsu project use 2 + + $ dsu p u 2 + messages: + using_project: Now using project "%{project_name}". + prompts: + use_confirm: "Use project %{project_name}: \"%{description}\"?" + use_options: + - y + - n + messages: + cancelled: Cancelled. + does_not_exist: Project "%{project_name}" does not exist. theme: create: desc: create THEME_NAME [OPTIONS] diff --git a/spec/dsu/features/dsu_theme_features_spec.rb b/spec/dsu/features/dsu_theme_features_spec.rb index a9d75cab..9d333b29 100644 --- a/spec/dsu/features/dsu_theme_features_spec.rb +++ b/spec/dsu/features/dsu_theme_features_spec.rb @@ -217,11 +217,11 @@ end def create_color_theme_prompt - "Create color theme \"#{theme_name}\"? [y/N]>" + "Create color theme \"#{theme_name}\"? [y,N]>" end def delete_color_theme_prompt - "Delete color theme \"#{theme_name}\"? [y/N]>" + "Delete color theme \"#{theme_name}\"? [y,N]>" end def color_theme_regex_for(theme_names:, default_theme_name: Dsu::Models::ColorTheme::DEFAULT_THEME_NAME) diff --git a/spec/dsu/models/project_spec.rb b/spec/dsu/models/project_spec.rb index ff942213..b577e416 100644 --- a/spec/dsu/models/project_spec.rb +++ b/spec/dsu/models/project_spec.rb @@ -44,38 +44,14 @@ it_behaves_like 'an error is raised' end - - context 'when project_name is not a string' do - let(:project_name) { :not_a_string } - let(:expected_error) { 'project_name is not a String' } - - it_behaves_like 'an error is raised' - end - - context 'when description is nil' do - let(:description) { nil } - let(:expected_error) { 'description is blank' } - - it_behaves_like 'an error is raised' - end - - context 'when description is an empty string' do - let(:description) { '' } - let(:expected_error) { 'description is blank' } - - it_behaves_like 'an error is raised' - end - - context 'when description is not a string' do - let(:description) { :not_a_string } - let(:expected_error) { 'description is not a String' } - - it_behaves_like 'an error is raised' - end end end describe 'validations' do + subject(:project) do + described_class.new(project_name: project_name, description: description, version: version, options: options).validate! + end + it 'validates #description attribute with the DescriptionValidator' do expect(described_class).to validate_with_validator(Dsu::Validators::DescriptionValidator) end @@ -88,9 +64,33 @@ expect(described_class).to validate_with_validator(Dsu::Validators::VersionValidator) end - context 'when project_name is less than or greater than the min/max length' + context 'when project_name is less than the min/max length' do + let(:project_name) { 'x' } + let(:expected_error) { /Project name is too short/ } + + it_behaves_like 'an error is raised' + end + + context 'when project_name is greater than the min/max length' do + let(:project_name) { 'x' * (1 + Dsu::Models::Project::MAX_PROJECT_NAME_LENGTH) } + let(:expected_error) { /Project name is too long/ } + + it_behaves_like 'an error is raised' + end + + context 'when description is less than the min/max length' do + let(:description) { 'x' } + let(:expected_error) { /Description is too short/ } - context 'when description is less than or greater than the min/max length' + it_behaves_like 'an error is raised' + end + + context 'when description is greater than the min/max length' do + let(:description) { 'x' * (1 + Dsu::Models::Project::MAX_DESCRIPTION_LENGTH) } + let(:expected_error) { /Description is too long/ } + + it_behaves_like 'an error is raised' + end end describe '#==' do @@ -194,6 +194,82 @@ end end + describe '#current_project' do + context 'when the project is the current project' do + subject(:project) { create(:project, :current_project, project_name: 'new') } + + it 'returns true' do + expect(project == described_class.current_project).to be true + end + end + + context 'when the project is not the current project' do + subject(:project) { create(:project, project_name: 'new') } + + it 'returns false' do + expect(project == described_class.current_project).to be false + end + end + end + + describe '#current_project?' do + subject(:project) { create(:project, project_name: 'new') } + + context 'when the project is the current project' do + it 'returns true' do + project.use! + expect(project.current_project?).to be true + end + end + + context 'when the project is not the current project' do + it 'returns false' do + expect(project.current_project?).to be false + end + end + end + + describe '#default_project' do + context 'when the project is the current project' do + subject(:project) { create(:project, :default_project, project_name: 'new') } + + it 'returns true' do + expect(project == described_class.default_project).to be true + end + end + + context 'when the project is not the current project' do + subject(:project) { create(:project, project_name: 'new') } + + it 'returns false' do + expect(project == described_class.default_project).to be false + end + end + end + + describe '#default_project?' do + subject(:project) { create(:project, project_name: 'new') } + + context 'when the project is the default project' do + it 'returns true' do + project.default! + expect(project.default_project?).to be true + end + end + + context 'when the project is not the default project' do + it 'returns false' do + expect(project.default_project?).to be false + end + end + end + + describe '#hash' do + it 'returns an Integer' do + expect(project.hash).to be_a(Integer) + end + end + describe '#persisted?' do context 'when the project does not exist' do it 'returns false' do @@ -212,12 +288,6 @@ end end - describe '#hash' do - it 'returns an Integer' do - expect(project.hash).to be_a(Integer) - end - end - describe '#to_h' do let(:expected_hash) do { @@ -237,4 +307,40 @@ describe '#update!' do end + + describe 'class methods' do + describe '.all' do + context 'when there are is only the default project' do + it 'returns an empty Array' do + expect(described_class.all).to eq([described_class.default_project]) + end + end + + context 'when there are projects' do + let!(:projects) do + [ + described_class.default_project, + create(:project, project_name: 'project a'), + create(:project, project_name: 'project b'), + create(:project, project_name: 'project c') + ] + end + + it 'returns an Array of projects' do + expect(described_class.all).to match_array(projects) + end + end + end + + describe '.default_project' do + it do + expect(described_class.default_project.default_project?).to be true + end + + it 'returns the default project' do + default_project_name = Dsu::Models::Configuration.new.default_project + expect(described_class.default_project.project_name).to eq(default_project_name) + end + end + end end diff --git a/spec/dsu/support/project_file_system_spec.rb b/spec/dsu/support/project_file_system_spec.rb index ca21ec20..db48979f 100644 --- a/spec/dsu/support/project_file_system_spec.rb +++ b/spec/dsu/support/project_file_system_spec.rb @@ -15,14 +15,14 @@ def initialize(project_name:) let(:project_name) { 'Test' } - describe '.current_project' do + describe '.current_project_name' do context 'when the current project file exists' do before do project_file_system.class.initialize_project(project_name: project_name) end it 'returns the current project' do - expect(project_file_system.class.current_project).to eq('default') + expect(project_file_system.class.current_project_name).to eq('default') end end @@ -114,4 +114,12 @@ def initialize(project_name:) end end end + + describe '.project_metadata' do + it 'does something' + end + + describe '.default_project_name' do + it 'does something' + end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index d4daf852..6a24b843 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -10,5 +10,23 @@ initialize_with do new(project_name: project_name, description: description, version: version, options: options) end + + after(:create) do |project, evaluator| + project.use! if evaluator.make_current_project + project.default! if evaluator.make_default_project + end + + transient do + make_current_project { false } + make_default_project { false } + end + + trait :current_project do + make_current_project { true } + end + + trait :default_project do + make_default_project { true } + end end end diff --git a/spec/fixtures/files/current_project.json b/spec/fixtures/files/current_project.json index 8ffd1663..b33cfdd7 100644 --- a/spec/fixtures/files/current_project.json +++ b/spec/fixtures/files/current_project.json @@ -1,4 +1,4 @@ { "version": 20230613121411, - "project": "default" + "project_name": "default" }