diff --git a/.rubocop.yml b/.rubocop.yml index 110e525..b55e282 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,9 +1,13 @@ AllCops: TargetRubyVersion: 3.2 NewCops: enable + SuggestExtensions: false Style/StringLiterals: EnforcedStyle: single_quotes Style/StringLiteralsInInterpolation: EnforcedStyle: single_quotes + +Style/RedundantReturn: + Enabled: false diff --git a/.solargraph.yml b/.solargraph.yml index bdb3e30..6bb8f16 100644 --- a/.solargraph.yml +++ b/.solargraph.yml @@ -1,19 +1,19 @@ --- include: -- Rakefile -- Gemfile -- "*.gemspec" -- "**/*.rb" + - Rakefile + - Gemfile + - "*.gemspec" + - "**/*.rb" exclude: -- spec/**/* -- test/**/* -- vendor/**/* -- ".bundle/**/*" + - spec/**/* + - test/**/* + - vendor/**/* + - ".bundle/**/*" require: [] domains: [] reporters: -- rubocop -- require_not_found + - rubocop + # - require_not_found formatter: rubocop: cops: safe @@ -21,5 +21,6 @@ formatter: only: [] extra_args: [] require_paths: [] -plugins: [] +plugins: + - solargraph-rspec max_files: 5000 diff --git a/lib/psdk/cli/configuration.rb b/lib/psdk/cli/configuration.rb new file mode 100644 index 0000000..215c06d --- /dev/null +++ b/lib/psdk/cli/configuration.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'yaml' + +module Psdk + module Cli + # Class holding the configuration of the Cli + class Configuration + # Filename of the project configuration + PROJECT_CONFIGURATION_FILENAME = '.psdk-cli.yml' + + # Filename of the global configuration + GLOBAL_CONFIGURATION_FILENAME = File.join(Dir.home || ENV['USERPROFILE'] || '~', '.psdk-cli/config.yml') + + # Create a new configuration + # @param hash [Hash] configuration hash + def initialize(hash) + hash = {} unless hash.is_a?(Hash) + # @type [String] + @studio_path = '' + # @type [Array] + @project_paths = [] + + self.studio_path = hash[:studio_path] if hash.key?(:studio_path) + self.project_paths = hash[:project_paths] if hash.key?(:project_paths) + end + + # Get the Pokémon Studio path + # @return [String] + attr_reader :studio_path + + # Set the Pokémon Studio path + # @param path [String] + def studio_path=(path) + unless Dir.exist?(path) || path.empty? + puts "[Error] Invalid studio_path at `#{path}`, this path does not exists" + return + end + # TODO: add check for locating psdk-binaries + @studio_path = path + end + + # Get the project paths + # @return [Array] + attr_reader :project_paths + + # Set the project_paths + # @param paths [Array] + def project_paths=(paths) + unless paths.is_a?(Array) + puts '[Error] project_paths is not an array' + return + end + unless paths.all? { |path| path.is_a?(String) } + puts '[Error] some of the project paths are not path' + return + end + + @project_paths = paths + end + + def to_h + return { + studio_path: @studio_path, + project_paths: @project_paths + } + end + + class << self + @global = nil + @local = nil + + # Get the project path + # @return [String | nil] + attr_reader :project_path + + # Get the configuration + # @param type [:global | :local] + # @return [Configuration] + def get(type) + @global ||= Configuration.new(load_hash(GLOBAL_CONFIGURATION_FILENAME)) + return @global if type == :global + + project_path = find_project_path + @local = nil if @project_path != project_path + @project_path = project_path + @local ||= Configuration.new( + @global.to_h.merge(load_hash(File.join(*@project_path, PROJECT_CONFIGURATION_FILENAME))) + ) + + return @local + end + + # Save the configuration + def save + File.write(GLOBAL_CONFIGURATION_FILENAME, YAML.dump(@global.to_h)) + return unless @local && @project_path + + local_configuration = @local.to_h + # Delete global configuration keys + local_configuration.delete(:project_paths) + + File.write(File.join(@project_path, PROJECT_CONFIGURATION_FILENAME), YAML.dump(local_configuration)) + end + + private + + def load_hash(path) + return {} unless File.exist?(path) + + return YAML.load_file(path, symbolize_names: true, freeze: true) + rescue StandardError => e + puts "[Error] failed to load configuration (#{e.message})" + puts e.backtrace + return {} + end + + # Get the project path + # @return [String | nil] + def find_project_path + current_path = Dir.pwd.gsub('\\', '/').split('/') + all_options = current_path.size.downto(2).map { |i| File.join(*current_path[0...i]) } + return all_options.find { |path| File.exist?(File.join(path, 'project.studio')) } + end + end + end + end +end diff --git a/spec/psdk/cli/configuration_spec.rb b/spec/psdk/cli/configuration_spec.rb new file mode 100644 index 0000000..86ac46a --- /dev/null +++ b/spec/psdk/cli/configuration_spec.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/BlockLength +RSpec.describe Psdk::Cli::Configuration do + before(:example) do + Psdk::Cli::Configuration.instance_eval do + @global = nil + @local = nil + end + end + + it 'uses home for the global configuration path' do + # TODO: Fix for Windows if necessary + expect(Psdk::Cli::Configuration::GLOBAL_CONFIGURATION_FILENAME).to start_with(Dir.home) + expect(Psdk::Cli::Configuration::GLOBAL_CONFIGURATION_FILENAME).to end_with('/.psdk-cli/config.yml') + end + + it 'loads configuration with empty hash' do + config = Psdk::Cli::Configuration.new({}) + + expect(config.studio_path).to eq('') + expect(config.project_paths).to eq([]) + expect(config.to_h).to eq({ + studio_path: '', + project_paths: [] + }) + end + + it 'does not update invalid path for studio_path' do + config = Psdk::Cli::Configuration.new({}) + + expect(config).to receive(:puts).with( + '[Error] Invalid studio_path at `/dev/null/cannot_exist`, this path does not exists' + ).exactly(1).time + config.studio_path = '/dev/null/cannot_exist' + expect(config.studio_path).to eq('') + end + + it 'updates studio_path' do + config = Psdk::Cli::Configuration.new({}) + + allow(Dir).to receive(:exist?) { |filename| filename == 'tmp/studio' } + config.studio_path = 'tmp/studio' + expect(config.studio_path).to eq('tmp/studio') + end + + it 'does not update project_paths if it is not a valid array' do + config = Psdk::Cli::Configuration.new({}) + + expect(config).to receive(:puts).with( + '[Error] project_paths is not an array' + ).exactly(1).time + config.project_paths = '/dev/null/cannot_exist' + expect(config.project_paths).to eq([]) + end + + it 'does not update project_paths if it contains a non string value' do + config = Psdk::Cli::Configuration.new({}) + + expect(config).to receive(:puts).with( + '[Error] some of the project paths are not path' + ).exactly(1).time + config.project_paths = ['/dev/null/cannot_exist', 0] + expect(config.project_paths).to eq([]) + end + + it 'update project_paths' do + config = Psdk::Cli::Configuration.new({}) + + config.project_paths = ['tmp/project'] + expect(config.project_paths).to eq(['tmp/project']) + end + + it 'loads an empty configuration if neither global or local file exists' do + expect(File).to receive(:read).exactly(0).times + + global = Psdk::Cli::Configuration.get(:global) + local = Psdk::Cli::Configuration.get(:local) + + expect(global.to_h).to eq(local.to_h) + expect(global.studio_path).to eq('') + expect(global.project_paths).to eq([]) + expect(global.to_h).to eq({ + studio_path: '', + project_paths: [] + }) + end + + it 'loads global configuration and merge it to local' do + stub_const('Psdk::Cli::Configuration::GLOBAL_CONFIGURATION_FILENAME', 'tmp/global.yml') + allow(Dir).to receive(:exist?) { |filename| filename == 'tmp/studio' } + allow(File).to receive(:exist?) { |filename| filename == 'tmp/global.yml' } + allow(IO).to receive(:open) do |_, &block| + block.call(StringIO.new(YAML.dump({ studio_path: 'tmp/studio', project_paths: ['project_a'] }))) + end + + global = Psdk::Cli::Configuration.get(:global) + local = Psdk::Cli::Configuration.get(:local) + expect(global.to_h).to eq(local.to_h) + expect(global).to_not eq(local) + expect(global.to_h).to eq({ + studio_path: 'tmp/studio', + project_paths: ['project_a'] + }) + expect(Psdk::Cli::Configuration.project_path).to eq(nil) + end + + it 'loads global configuration and merge it to local while preserving local defined values' do + stub_const('Psdk::Cli::Configuration::GLOBAL_CONFIGURATION_FILENAME', 'tmp/global.yml') + allow(Dir).to receive(:exist?) { |filename| filename.start_with?('tmp/') } + allow(File).to receive(:exist?) { |filename| filename.end_with?('/project.studio') || filename.end_with?('.yml') } + allow(IO).to receive(:open) do |filename, &block| + if filename == 'tmp/global.yml' + block.call(StringIO.new(YAML.dump({ studio_path: 'tmp/studio', project_paths: ['project_a'] }))) + else + block.call(StringIO.new(YAML.dump({ studio_path: 'tmp/studio_repository' }))) + end + end + + global = Psdk::Cli::Configuration.get(:global) + local = Psdk::Cli::Configuration.get(:local) + expect(global.to_h).to eq({ + studio_path: 'tmp/studio', + project_paths: ['project_a'] + }) + expect(local.to_h).to eq({ + studio_path: 'tmp/studio_repository', + project_paths: ['project_a'] + }) + expect(Psdk::Cli::Configuration.project_path).to eq(Dir.pwd) + end + + it 'successfully saves the configurations' do + stub_const('Psdk::Cli::Configuration::GLOBAL_CONFIGURATION_FILENAME', 'tmp/global.yml') + allow(File).to receive(:exist?) { |filename| filename.end_with?('/project.studio') } + allow(Dir).to receive(:exist?) { |filename| filename.start_with?('tmp/') } + + global = Psdk::Cli::Configuration.get(:global) + local = Psdk::Cli::Configuration.get(:local) + + global.studio_path = 'tmp/studio' + local.studio_path = 'tmp/project' + local.project_paths = %w[a b c] # Never saved + + expect(File).to receive(:write).with('tmp/global.yml', + "---\n:studio_path: tmp/studio\n:project_paths: []\n") + expect(File).to receive(:write).with(File.join(Dir.pwd, '.psdk-cli.yml'), + "---\n:studio_path: tmp/project\n") + Psdk::Cli::Configuration.save + end + + it 'loads the project path as the path that contains project.studio' do + allow(Dir).to receive(:pwd) { '/users/user_a/documents/projects/super_game/Data/studio/maps' } + allow(Dir).to receive(:exist?) { |filename| filename == 'tmp/studio_repository' } + allow(File).to receive(:exist?) do |filename| + next filename.end_with?('/super_game/project.studio') || filename.end_with?('/.psdk-cli.yml') + end + allow(IO).to receive(:open) do |_, &block| + block.call(StringIO.new(YAML.dump({ studio_path: 'tmp/studio_repository' }))) + end + + local = Psdk::Cli::Configuration.get(:local) + expect(local.to_h).to eq({ studio_path: 'tmp/studio_repository', project_paths: [] }) + expect(Psdk::Cli::Configuration.project_path).to eq('/users/user_a/documents/projects/super_game') + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 56fb1cf..41877bf 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'psdk/cli' +require 'psdk/cli/configuration' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure