diff --git a/.travis.yml b/.travis.yml index 7fd1b23..37c1c89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,24 @@ language: ruby +dist: trusty +cache: bundler bundler_args: --without tools script: "bundle exec rake ci" env: - - CODECLIMATE_REPO_TOKEN=a49f28520e49a35d73c9b6ed3d78ae110e4a5d05b9f154b999578a5175add09d + global: + - CODECLIMATE_REPO_TOKEN=a49f28520e49a35d73c9b6ed3d78ae110e4a5d05b9f154b999578a5175add09d + - JRUBY_OPTS='--dev -J-Xmx1024M' +after_success: + - - '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter' rvm: - - 2.0 - - 2.1 + - 2.4.0 + - 2.3 - 2.2 - - 2.3.0 - - rbx-2 - - jruby-9000 + - rbx-3 + - jruby-9.1.7.0 - ruby-head matrix: allow_failures: + - rvm: rbx-3 - rvm: ruby-head notifications: webhooks: diff --git a/Gemfile b/Gemfile index b12fcbf..09c5c7d 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,9 @@ source 'https://rubygems.org' gemspec group :test do + gem 'dry-struct' gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: 'master' - gem 'virtus' + gem 'rom-repository', git: 'https://github.com/rom-rb/rom-repository', branch: 'master' gem 'rspec', '~> 3.1' gem 'codeclimate-test-reporter', require: false gem 'inflecto' diff --git a/README.md b/README.md index bf7867d..b6615c5 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Or install it yourself as: ## Usage -See `spec/integration/repository_spec.rb` or [examples/](https://github.com/rom-rb/rom-csv/tree/master/examples) folder for a sample usage. +See `spec/integration/gateway_spec.rb` or [examples/](https://github.com/rom-rb/rom-csv/tree/master/examples) folder for a sample usage. ## License diff --git a/examples/find_user.rb b/examples/find_user.rb index 5bb460f..22542cf 100644 --- a/examples/find_user.rb +++ b/examples/find_user.rb @@ -7,7 +7,6 @@ csv_file = ARGV[0] || File.expand_path("./users.csv", File.dirname(__FILE__)) configuration = ROM::Configuration.new(:csv, csv_file) -configuration.use(:macros) configuration.relation(:users) do def by_name(name) diff --git a/lib/rom/csv/commands/create.rb b/lib/rom/csv/commands/create.rb index adfd2a4..62a1406 100644 --- a/lib/rom/csv/commands/create.rb +++ b/lib/rom/csv/commands/create.rb @@ -10,7 +10,6 @@ class Create < ROM::Commands::Create def execute(tuples) insert_tuples = [tuples].flatten.map do |tuple| attributes = input[tuple] - validator.call(attributes) attributes.to_h end @@ -18,6 +17,8 @@ def execute(tuples) insert_tuples end + private + def insert(tuples) tuples.each { |tuple| dataset << new_row(tuple) } dataset.sync! diff --git a/lib/rom/csv/commands/delete.rb b/lib/rom/csv/commands/delete.rb index 8afed3b..944f751 100644 --- a/lib/rom/csv/commands/delete.rb +++ b/lib/rom/csv/commands/delete.rb @@ -19,6 +19,8 @@ def execute dataset.data end + private + def dataset relation.dataset end diff --git a/lib/rom/csv/commands/update.rb b/lib/rom/csv/commands/update.rb index 5add615..8979ec5 100644 --- a/lib/rom/csv/commands/update.rb +++ b/lib/rom/csv/commands/update.rb @@ -9,12 +9,13 @@ class Update < ROM::Commands::Update def execute(tuple) attributes = input[tuple] - validator.call(attributes) tuple = attributes.to_h update(tuple) end + private + def update(tuple) original_data = original_dataset.to_a output = [] diff --git a/lib/rom/csv/dataset.rb b/lib/rom/csv/dataset.rb index 293bfce..533066c 100644 --- a/lib/rom/csv/dataset.rb +++ b/lib/rom/csv/dataset.rb @@ -1,13 +1,24 @@ require 'rom/memory/dataset' +require 'rom/csv/storage' module ROM module CSV + # Type definition used to constrain the `connection` option + StorageType = Types.Definition(Storage).constrained(type: Storage) + # Dataset for CSV # # @api public class Dataset < ROM::Memory::Dataset - option :path, reader: true - option :file_options, reader: true + + # Connection to the file + # + # @return [Storage] + # + # @api private + option :connection, + optional: true, + type: StorageType # Convert each CSV::Row to a hash # @@ -17,23 +28,11 @@ def self.row_proc end def reload! - @data = load_data + @data = connection.load end def sync! - write_data && reload! - end - - def write_data - ::CSV.open(path, 'wb', file_options) do |csv| - data.to_a.each do |tuple| - csv << tuple - end - end - end - - def load_data - ::CSV.table(path, file_options).by_row! + connection.dump(data) && reload! end def count diff --git a/lib/rom/csv/gateway.rb b/lib/rom/csv/gateway.rb index 3875207..9ef86a2 100644 --- a/lib/rom/csv/gateway.rb +++ b/lib/rom/csv/gateway.rb @@ -1,6 +1,8 @@ require 'rom/gateway' +require 'rom/initializer' require 'rom/csv/dataset' require 'rom/csv/commands' +require 'rom/csv/storage' # Ruby Object Mapper # @@ -40,6 +42,20 @@ module CSV # # @api public class Gateway < ROM::Gateway + extend Initializer + + param :path, + reader: :private, + type: Types::Strict::String + param :csv_options, + default: proc { {} }, + reader: :private, + type: Types::Strict::Hash + + # @api private + attr_reader :datasets + private :datasets + # Expect a path to a single csv file which will be registered by rom to # the given name or :default as the gateway. # @@ -49,16 +65,15 @@ class Gateway < ROM::Gateway # * header_converters: :symbol # # @param path [String] path to csv - # @param options [Hash] options passed to CSV.table + # @param csv_options [Hash] options passed to CSV.table # # @api private # # @see CSV.table - def initialize(path, options = {}) + def initialize(*) + super @datasets = {} - @path = path - @options = options - @connection = ::CSV.table(path, options).by_row! + @connection = Storage.new(path, csv_options) end # Return dataset with the given name @@ -80,7 +95,7 @@ def [](name) # # @api public def dataset(name) - datasets[name] = Dataset.new(connection, dataset_options) + datasets[name] = Dataset.new(connection.load, connection: connection) end # Check if dataset exists @@ -91,15 +106,6 @@ def dataset(name) def dataset?(name) datasets.key?(name) end - - private - - def dataset_options - { path: path, file_options: options } - end - - # @api private - attr_reader :datasets, :path, :options end end end diff --git a/lib/rom/csv/relation.rb b/lib/rom/csv/relation.rb index 8871e86..5a15517 100644 --- a/lib/rom/csv/relation.rb +++ b/lib/rom/csv/relation.rb @@ -1,4 +1,5 @@ -require 'rom/relation' +require 'rom/memory' +require 'rom/plugins/relation/key_inference' module ROM module CSV @@ -9,10 +10,9 @@ module CSV # end # # @api public - class Relation < ROM::Relation + class Relation < ROM::Memory::Relation adapter :csv - - forward :join, :project, :restrict, :order + use :key_inference def count dataset.count diff --git a/lib/rom/csv/storage.rb b/lib/rom/csv/storage.rb new file mode 100644 index 0000000..8c3b888 --- /dev/null +++ b/lib/rom/csv/storage.rb @@ -0,0 +1,52 @@ +require 'csv' +require 'rom/initializer' + +module ROM + module CSV + # CSV file storage for datasets + # + # @api private + class Storage + extend Initializer + + # Path to the file + # + # @return [String] + # + # @api private + param :path, + type: Types::Strict::String + + # Options for file passed to `CSV` + # + # @return [Hash] + # + # @api private + param :csv_options, + default: proc { {} }, + type: Types::Strict::Hash + + # Dump the data to the file at `path` + # + # @return [undefined] + # + # @api public + def dump(data) + ::CSV.open(path, 'wb', csv_options) do |csv| + data.to_a.each do |tuple| + csv << tuple + end + end + end + + # Load the data from the file at `path` + # + # @return [CSV::Table] + # + # @api public + def load + ::CSV.table(path, csv_options).by_row! + end + end + end +end diff --git a/lib/rom/csv/types.rb b/lib/rom/csv/types.rb new file mode 100644 index 0000000..8b939d6 --- /dev/null +++ b/lib/rom/csv/types.rb @@ -0,0 +1,13 @@ +require 'rom/types' + +module ROM + module CSV + module Types + include ROM::Types + + def self.Definition(primitive) + Dry::Types::Definition.new(primitive) + end + end + end +end diff --git a/rom-csv.gemspec b/rom-csv.gemspec index 626ede2..edb33dd 100644 --- a/rom-csv.gemspec +++ b/rom-csv.gemspec @@ -18,9 +18,10 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - spec.add_runtime_dependency 'rom', '~> 1.0' + spec.add_runtime_dependency 'rom', '~> 3.0' spec.add_development_dependency 'bundler' - spec.add_development_dependency 'rake' + spec.add_development_dependency 'rake', '10.3' + spec.add_development_dependency 'rspec', '~> 3.5' spec.add_development_dependency 'rubocop', '~> 0.28.0' end diff --git a/spec/integration/commands/create_spec.rb b/spec/integration/commands/create_spec.rb index 3f80c08..240ea40 100644 --- a/spec/integration/commands/create_spec.rb +++ b/spec/integration/commands/create_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -require 'virtus' +require 'dry-struct' describe 'Commands / Create' do include_context 'database setup' @@ -9,17 +9,17 @@ before do configuration.relation(:users) - class User - include Virtus.model - - attribute :id, Integer - attribute :name, String - attribute :email, String + module Test + class User < Dry::Struct + attribute :user_id, Types::Strict::Int + attribute :name, Types::Strict::String + attribute :email, Types::Strict::String + end end configuration.mappers do define(:users) do - model User + model Test::User register_as :entity end end diff --git a/spec/integration/commands/delete_spec.rb b/spec/integration/commands/delete_spec.rb index fa32eb9..fe992fe 100644 --- a/spec/integration/commands/delete_spec.rb +++ b/spec/integration/commands/delete_spec.rb @@ -19,13 +19,6 @@ def by_id(id) end end - it 'raises error when tuple count does not match expectation' do - result = users.try { users.delete.call } - - expect(result.value).to be(nil) - expect(result.error).to be_instance_of(ROM::TupleCountMismatchError) - end - it 'deletes all tuples in a restricted relation' do result = users.try { users.delete.by_id(1).call } diff --git a/spec/integration/commands/update_spec.rb b/spec/integration/commands/update_spec.rb index 9687a29..68b6418 100644 --- a/spec/integration/commands/update_spec.rb +++ b/spec/integration/commands/update_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -require 'virtus' +require 'dry-struct' describe 'Commands / Updates' do include_context 'database setup' @@ -20,17 +20,17 @@ def by_id(id) end end - class User - include Virtus.model - - attribute :id, Integer - attribute :name, String - attribute :email, String + module Test + class User < Dry::Struct + attribute :user_id, Types::Strict::Int + attribute :name, Types::Strict::String + attribute :email, Types::Strict::String + end end configuration.mappers do define(:users) do - model User + model Test::User register_as :entity end end diff --git a/spec/integration/repository_spec.rb b/spec/integration/gateway_spec.rb similarity index 64% rename from spec/integration/repository_spec.rb rename to spec/integration/gateway_spec.rb index def4472..dcfa1bc 100644 --- a/spec/integration/repository_spec.rb +++ b/spec/integration/gateway_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' -require 'virtus' +require 'dry-struct' +require 'rom/repository' describe 'CSV gateway' do context 'without extra options' do @@ -9,6 +10,12 @@ configuration.relation(:users) do gateway :users + schema(:users) do + attribute :user_id, Types::Strict::Int + attribute :name, Types::Strict::String + attribute :email, Types::Strict::String + end + def by_name(name) restrict(name: name) end @@ -30,42 +37,40 @@ def with_addresses gateway :addresses end - class User - include Virtus.model - - attribute :user_id, Integer - attribute :name, String - attribute :email, String - end - - class UserWithAddress - include Virtus.model + module Test + class User < Dry::Struct + constructor_type :schema # allow missing keys - attribute :user_id, Integer - attribute :name, String - attribute :email, String - attribute :addresses - end + attribute :user_id, Types::Strict::Int.optional + attribute :name, Types::Strict::String + attribute :email, Types::Strict::String.optional + end - class Address - include Virtus.model + class Address < Dry::Struct + attribute :address_id, Types::Strict::Int + attribute :street, Types::Strict::String + end - attribute :address_id, Integer - attribute :street, String + class UserWithAddress < Dry::Struct + attribute :user_id, Types::Strict::Int + attribute :name, Types::Strict::String + attribute :email, Types::Strict::String + attribute :addresses, Types::Strict::Array.member(Test::Address) + end end configuration.mappers do define(:users) do - model User + model Test::User register_as :entity end define(:users_with_address, parent: :users) do - model UserWithAddress + model Test::UserWithAddress register_as :entity_with_address group :addresses do - model Address + model Test::Address attribute :address_id attribute :street @@ -107,10 +112,10 @@ class Address results = container.relation(:users).as(:entity_with_address) .with_addresses.first - expect(results.attributes.keys.sort) + expect(results.to_hash.keys.sort) .to eq([:user_id, :name, :email, :addresses].sort) - expect(results.addresses.first.attributes.keys.sort) + expect(results.addresses.first.to_hash.keys.sort) .to eq([:address_id, :street].sort) end end @@ -131,5 +136,18 @@ class Address expect(user[:email]).to eql('zolw@example.com') end end + + describe 'with a repository' do + let(:repo) do + Class.new(ROM::Repository[:users]).new(container) + end + + it 'auto-maps to structs' do + user = repo.users.first + + expect(user.name).to eql('Julie') + expect(user.email).to eql('julie.andrews@example.com') + end + end end end diff --git a/spec/integration/relation_spec.rb b/spec/integration/relation_spec.rb index d2860fb..2868c4e 100644 --- a/spec/integration/relation_spec.rb +++ b/spec/integration/relation_spec.rb @@ -1,16 +1,19 @@ require 'spec_helper' -require 'virtus' -describe 'CSV gateway' do +describe 'CSV relation' do context 'without extra options' do include_context 'database setup' before do - module TestPlugin; end + module Test + module RelationPlugin + # empty plugin for test purposes + end + end ROM.plugins do adapter :csv do - register :test_plugin, TestPlugin, type: :relation + register :test_plugin, Test::RelationPlugin, type: :relation end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 113eddd..657f631 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,14 +3,33 @@ require 'bundler' Bundler.require -if RUBY_ENGINE == 'rbx' - require 'codeclimate-test-reporter' - CodeClimate::TestReporter.start +if RUBY_ENGINE == 'ruby' && ENV['CI'] == 'true' + require 'simplecov' + SimpleCov.start do + add_filter '/spec/' + end end require 'rom' require 'rom-csv' -root = Pathname(__FILE__).dirname +SPEC_ROOT = root = Pathname(__FILE__).dirname -Dir[root.join('support/*.rb').to_s].each { |f| require f } +# Provide a `Types` module to specs +require 'dry-types' +module Types + include Dry::Types.module +end + +RSpec.configure do |config| + config.before do + module Test + end + end + + config.after do + Object.send(:remove_const, :Test) + end + + Dir[root.join('support/*.rb').to_s].each { |f| require f } +end diff --git a/spec/support/database_setup.rb b/spec/support/database_setup.rb index c57a420..539f6af 100644 --- a/spec/support/database_setup.rb +++ b/spec/support/database_setup.rb @@ -5,7 +5,7 @@ users: [:csv, path], addresses: [:csv, addresses_path], utf8: [:csv, users_with_utf8_path, { encoding: 'iso-8859-2', col_sep: ';' }] - ).use(:macros) + ) end let(:container) { ROM.container(configuration) } diff --git a/spec/unit/repository_spec.rb b/spec/unit/gateway_spec.rb similarity index 100% rename from spec/unit/repository_spec.rb rename to spec/unit/gateway_spec.rb