├── test
├── helpers
│ └── .keep
├── mailers
│ └── .keep
├── models
│ └── .keep
├── controllers
│ └── .keep
├── dummy
│ ├── log
│ │ └── .keep
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── apple-touch-icon.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── app
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── models
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── user.rb
│ │ │ └── application_record.rb
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ └── application_controller.rb
│ │ ├── views
│ │ │ └── layouts
│ │ │ │ ├── mailer.text.erb
│ │ │ │ ├── mailer.html.erb
│ │ │ │ └── application.html.erb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── channels
│ │ │ └── application_cable
│ │ │ │ ├── channel.rb
│ │ │ │ └── connection.rb
│ │ ├── mailers
│ │ │ └── application_mailer.rb
│ │ └── jobs
│ │ │ └── application_job.rb
│ ├── config
│ │ ├── routes.rb
│ │ ├── environment.rb
│ │ ├── cable.yml
│ │ ├── boot.rb
│ │ ├── initializers
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── permissions_policy.rb
│ │ │ ├── assets.rb
│ │ │ ├── inflections.rb
│ │ │ └── content_security_policy.rb
│ │ ├── database.yml
│ │ ├── application.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── storage.yml
│ │ ├── puma.rb
│ │ └── environments
│ │ │ ├── test.rb
│ │ │ ├── development.rb
│ │ │ └── production.rb
│ ├── bin
│ │ ├── rake
│ │ ├── rails
│ │ └── setup
│ ├── test
│ │ ├── models
│ │ │ └── user_test.rb
│ │ └── fixtures
│ │ │ └── users.yml
│ ├── config.ru
│ ├── Rakefile
│ └── db
│ │ ├── migrate
│ │ └── 20221008115344_create_users.rb
│ │ └── schema.rb
├── integration
│ ├── .keep
│ └── navigation_test.rb
├── fixtures
│ └── files
│ │ └── .keep
├── test_helper.rb
└── art_vandelay_test.rb
├── app
├── models
│ ├── concerns
│ │ └── .keep
│ └── art_vandelay
│ │ └── application_record.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ └── art_vandelay
│ │ └── application_controller.rb
├── assets
│ ├── images
│ │ └── art_vandelay
│ │ │ └── .keep
│ ├── config
│ │ └── art_vandelay_manifest.js
│ └── stylesheets
│ │ └── art_vandelay
│ │ └── application.css
├── helpers
│ └── art_vandelay
│ │ └── application_helper.rb
├── jobs
│ └── art_vandelay
│ │ └── application_job.rb
├── mailers
│ └── art_vandelay
│ │ └── application_mailer.rb
└── views
│ └── layouts
│ └── art_vandelay
│ └── application.html.erb
├── .tool-versions
├── config
└── routes.rb
├── .standard.yml
├── lib
├── art_vandelay
│ ├── engine.rb
│ └── version.rb
├── tasks
│ └── art_vandelay_tasks.rake
└── art_vandelay.rb
├── CODE_OF_CONDUCT.md
├── Rakefile
├── .gitignore
├── CODEOWNERS
├── .github
└── workflows
│ ├── dynamic-security.yml
│ └── ci.yml
├── bin
├── ci
├── setup
└── rails
├── SECURITY.md
├── Gemfile
├── art_vandelay.gemspec
├── MIT-LICENSE
├── NEWS.md
└── README.md
/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | ruby 3.3.4
2 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/art_vandelay/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | ArtVandelay::Engine.routes.draw do
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/.standard.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - 'test/dummy/**/*'
3 | - 'db/migrate/*'
4 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/assets/config/art_vandelay_manifest.js:
--------------------------------------------------------------------------------
1 | //= link_directory ../stylesheets/art_vandelay .css
2 |
--------------------------------------------------------------------------------
/app/helpers/art_vandelay/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ArtVandelay
2 | module ApplicationHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | validates :password, presence: true
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | mount ArtVandelay::Engine => "/art_vandelay"
3 | end
4 |
--------------------------------------------------------------------------------
/app/jobs/art_vandelay/application_job.rb:
--------------------------------------------------------------------------------
1 | module ArtVandelay
2 | class ApplicationJob < ActiveJob::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/lib/art_vandelay/engine.rb:
--------------------------------------------------------------------------------
1 | module ArtVandelay
2 | class Engine < ::Rails::Engine
3 | isolate_namespace ArtVandelay
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/tasks/art_vandelay_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :art_vandelay do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 | //= link art_vandelay_manifest.js
4 |
--------------------------------------------------------------------------------
/app/controllers/art_vandelay/application_controller.rb:
--------------------------------------------------------------------------------
1 | module ArtVandelay
2 | class ApplicationController < ActionController::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: "from@example.com"
3 | layout "mailer"
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/art_vandelay/application_record.rb:
--------------------------------------------------------------------------------
1 | module ArtVandelay
2 | class ApplicationRecord < ActiveRecord::Base
3 | self.abstract_class = true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/art_vandelay/version.rb:
--------------------------------------------------------------------------------
1 | module ArtVandelay
2 | VERSION = "0.2.0".freeze
3 | RAILS_VERSION = ">= 7.0".freeze
4 | MINIMUM_RUBY_VERSION = ">= 3.1".freeze
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/test/dummy/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class UserTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/test/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | email: user@example.com
5 | password: Password
6 |
--------------------------------------------------------------------------------
/test/integration/navigation_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class NavigationTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/app/mailers/art_vandelay/application_mailer.rb:
--------------------------------------------------------------------------------
1 | module ArtVandelay
2 | class ApplicationMailer < ActionMailer::Base
3 | default from: "from@example.com"
4 | layout "mailer"
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of conduct
2 |
3 | By participating in this project, you agree to abide by the
4 | [thoughtbot code of conduct][1].
5 |
6 | [1]: https://thoughtbot.com/open-source-code-of-conduct
7 |
--------------------------------------------------------------------------------
/test/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | if Rails::VERSION::MAJOR < 7
3 | self.abstract_class = true
4 | else
5 | primary_abstract_class
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 |
3 | APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4 | load "rails/tasks/engine.rake"
5 |
6 | load "rails/tasks/statistics.rake"
7 |
8 | require "bundler/gem_tasks"
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /doc/
3 | /log/*.log
4 | /pkg/
5 | /tmp/
6 | /test/dummy/db/*.sqlite3
7 | /test/dummy/db/*.sqlite3-*
8 | /test/dummy/log/*.log
9 | /test/dummy/storage/
10 | /test/dummy/tmp/
11 | Gemfile.lock
12 |
--------------------------------------------------------------------------------
/test/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
3 |
4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
6 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Lines starting with '#' are comments.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # More details are here: https://help.github.com/articles/about-codeowners/
5 |
6 | # The '*' pattern is global owners.
7 |
8 | # Global rule:
9 | * @stevepolitodesign
10 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/art_vandelay/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Art vandelay
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag "art_vandelay/application", media: "all" %>
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 |
6 | <%= csrf_meta_tags %>
7 | <%= csp_meta_tag %>
8 |
9 | <%= stylesheet_link_tag "application" %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20221008115344_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"]
2 | def change
3 | create_table :users do |t|
4 | t.string :email, null: false
5 | t.string :password, null: false
6 |
7 | t.timestamps
8 | end
9 |
10 | add_index :users, :email, unique: true
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of
4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
5 | # notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/.github/workflows/dynamic-security.yml:
--------------------------------------------------------------------------------
1 | name: update-security
2 |
3 | on:
4 | push:
5 | paths:
6 | - SECURITY.md
7 | branches:
8 | - main
9 | workflow_dispatch:
10 |
11 | jobs:
12 | update-security:
13 | permissions:
14 | contents: write
15 | pull-requests: write
16 | pages: write
17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main
18 | secrets:
19 | token: ${{ secrets.GITHUB_TOKEN }}
20 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/bin/ci:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | green() { printf "\033[32m${1}\033[0m\n"; }
4 | red() { printf "\033[31m${1}\033[0m\n"; }
5 |
6 | set -e
7 |
8 | green "[bin/ci] Running linters..."
9 | if bundle exec standardrb
10 | then
11 | green "[bin/ci] Running test suite..."
12 | if bundle exec rails t --fail-fast
13 | then
14 | green "[bin/ci] CI Passed 🎉"
15 | else
16 | red "[bin/ci] Test suite failed. Exiting."
17 | exit 1
18 | fi
19 | else
20 | red "[bin/ci] There were linting errors. Exiting."
21 | exit 1
22 | fi
23 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | echo "[bin/setup] Installing gems"
6 | rm -f Gemfile.lock
7 | bundle install
8 |
9 | echo "[bin/setup] Dropping and recreating the database"
10 | bundle exec rails db:reset || bundle exec rails db:migrate
11 |
12 | echo
13 | echo "[bin/setup] Setup complete."
14 | echo
15 | echo "[bin/setup] If you want to build against a different version of Rails run
16 | the following before running the setup script."
17 | echo
18 | echo "export RAILS_VERSION=MAJOR.MINOR (e.g. export RAILS_VERSION=6.1)"
19 | echo
20 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path("..", __dir__)
6 | ENGINE_PATH = File.expand_path("../lib/art_vandelay/engine", __dir__)
7 | APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
8 |
9 | # Set up gems listed in the Gemfile.
10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12 |
13 | require "rails/all"
14 | require "rails/engine/commands"
15 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 | # Security Policy
3 |
4 | ## Supported Versions
5 |
6 | Only the the latest version of this project is supported at a given time. If
7 | you find a security issue with an older version, please try updating to the
8 | latest version first.
9 |
10 | If for some reason you can't update to the latest version, please let us know
11 | your reasons so that we can have a better understanding of your situation.
12 |
13 | ## Reporting a Vulnerability
14 |
15 | For security inquiries or vulnerability reports, visit
16 | .
17 |
18 | If you have any suggestions to improve this policy, visit .
19 |
20 |
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem "sqlite3"
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Specify your gem's dependencies in art_vandelay.gemspec.
5 | gemspec
6 |
7 | # Start debugger with binding.b [https://github.com/ruby/debug]
8 | # gem "debug", ">= 1.0.0"
9 | #
10 | rails_version = ENV.fetch("RAILS_VERSION", "7.0")
11 |
12 | rails_constraint = if rails_version == "main"
13 | {github: "rails/rails"}
14 | else
15 | "~> #{rails_version}.0"
16 | end
17 |
18 | if rails_version.start_with? "6"
19 | gem "net-imap", require: false
20 | gem "net-pop", require: false
21 | gem "net-smtp", require: false
22 | gem "psych", "< 4"
23 | end
24 | gem "rails", rails_constraint
25 | gem "sprockets-rails"
26 | gem "sqlite3", "~> 1.4"
27 | gem "standardrb"
28 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require_relative "../test/dummy/config/environment"
5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
6 | ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__)
7 | require "rails/test_help"
8 |
9 | # Load fixtures from the engine
10 | if ActiveSupport::TestCase.respond_to?(:fixture_path=)
11 | ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
12 | ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
13 | ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files"
14 | ActiveSupport::TestCase.fixtures :all
15 | end
16 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/art_vandelay/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 | require "art_vandelay"
9 |
10 | module Dummy
11 | class Application < Rails::Application
12 | config.load_defaults Rails::VERSION::STRING.to_f
13 |
14 | # For compatibility with applications that use this config
15 | config.action_controller.include_all_helpers = false
16 |
17 | # Configuration for the application, engines, and railties goes here.
18 | #
19 | # These settings can be overridden in specific environments using the files
20 | # in config/environments, which are processed later.
21 | #
22 | # config.time_zone = "Central Time (US & Canada)"
23 | # config.eager_load_paths << Rails.root.join("extras")
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # "true": "foo"
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/art_vandelay.gemspec:
--------------------------------------------------------------------------------
1 | require_relative "lib/art_vandelay/version"
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "art_vandelay"
5 | spec.version = ArtVandelay::VERSION
6 | spec.required_ruby_version = ArtVandelay::MINIMUM_RUBY_VERSION
7 | spec.authors = ["Steve Polito"]
8 | spec.email = ["stevepolito@hey.com"]
9 | spec.homepage = "https://github.com/thoughtbot/art_vandelay"
10 | spec.summary = "Art Vandelay is an importer/exporter for Rails"
11 | spec.description = "Art Vandelay is an importer/exporter for Rails"
12 | spec.license = "MIT"
13 |
14 | spec.metadata["homepage_uri"] = spec.homepage
15 | spec.metadata["source_code_uri"] = "https://github.com/thoughtbot/art_vandelay"
16 | spec.metadata["changelog_uri"] = "https://github.com/thoughtbot/art_vandelay/NEWS.md"
17 |
18 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
19 | Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
20 | end
21 |
22 | spec.add_dependency "rails", ArtVandelay::RAILS_VERSION
23 | end
24 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [pull_request]
3 |
4 | jobs:
5 | test:
6 | strategy:
7 | fail-fast: false
8 | matrix:
9 | ruby-version:
10 | - "3.1"
11 | - "3.2"
12 | - "3.3"
13 | rails-version:
14 | - "7.0"
15 | - "7.1"
16 | - "main"
17 |
18 | env:
19 | RAILS_VERSION: ${{ matrix.rails-version }}
20 |
21 | name: ${{ format('Tests (Ruby {0}, Rails {1})', matrix.ruby-version, matrix.rails-version) }}
22 | runs-on: ubuntu-latest
23 |
24 | steps:
25 | - uses: actions/checkout@v3
26 | - name: Set up Ruby
27 | uses: ruby/setup-ruby@v1
28 | with:
29 | ruby-version: ${{ matrix.ruby-version }}
30 | - name: Install dependencies
31 | run: bundle install
32 | - name: Run linters
33 | run: bundle exec standardrb
34 | - name: Run migrations
35 | run: bundle exec rails db:migrate
36 | - name: Run tests
37 | run: bundle exec rails test
38 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Steve Polito and thoughtbot, inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 2022_10_08_115344) do
14 |
15 | create_table "users", force: :cascade do |t|
16 | t.string "email", null: false
17 | t.string "password", null: false
18 | t.datetime "created_at", null: false
19 | t.datetime "updated_at", null: false
20 | t.index ["email"], name: "index_users_on_email", unique: true
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap and inline scripts
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/test/dummy/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket-<%= Rails.env %>
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket-<%= Rails.env %>
23 |
24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name-<%= Rails.env %>
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/NEWS.md:
--------------------------------------------------------------------------------
1 | Unreleased
2 |
3 | - Drops support for Ruby 3.0, since it is EOL.
4 | - Drops support for Rails 6.
5 | - Adds `Import#json`, `Export#json`, and `Export#email` (#34).
6 |
7 | The `#json` interface for importing and exporting JSON have been designed to work the same way they already work for the CSV interfaces. For example:
8 |
9 | ```ruby
10 | json_string = [
11 | {
12 | email: "george@vandelay_industries.com",
13 | password: "bosco"
14 | }
15 | ].to_json
16 |
17 | result = ArtVandelay::Import.new(:users).json(json_string, attributes: {email_address: :email, passcode: :password})
18 | ```
19 |
20 | `ArtVandelay::Export#email_csv` has been changed to a more-generic `ArtVandelay::Export#email` method that takes a new `:format` option. The new option defaults to `:csv` but can also be used with `:json`. Since the old `#email_csv` method no longer exists, you'll need to update your application code accordingly. For example:
21 |
22 | ```diff
23 | -ArtVandelay::Export.email_csv(to: my_email_address)
24 | +ArtVandelay::Export.email(to: my_email_address)
25 | ```
26 |
27 | *Benjamin Wil*
28 |
29 | 0.2.0 (June 15, 2023)
30 |
31 | Add option that allows stripping of whitespace for all values (#19)
32 |
33 | ```ruby
34 | csv_string = CSV.generate do |csv|
35 | csv << ["email_address ", " passcode "]
36 | csv << [" george@vandelay_industries.com ", " bosco "]
37 | end
38 |
39 | result = ArtVandelay::Import.new(:users, strip: true).csv(csv_string, attributes: {email_address: :email, passcode: :password})
40 | # => #
41 |
42 | result.rows_accepted
43 | # => [{:row=>["george@vandelay_industries.com", "bosco"], :id=>1}]
44 | ```
45 |
46 | 0.1.0 (December 9, 2022)
47 |
48 | Initial release 🎉
49 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/test/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 | #
33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 | #
40 | # preload_app!
41 |
42 | # Allow puma to be restarted by `bin/rails restart` command.
43 | plugin :tmp_restart
44 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
12 | config.cache_classes = true
13 |
14 | # Eager loading loads your whole application. When running a single test locally,
15 | # this probably isn't necessary. It's a good idea to do in a continuous integration
16 | # system, or in some way before deploying your code.
17 | config.eager_load = ENV["CI"].present?
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Store uploaded files on the local file system in a temporary directory.
37 | config.active_storage.service = :test
38 |
39 | config.action_mailer.perform_caching = false
40 |
41 | # Tell Action Mailer not to deliver emails to the real world.
42 | # The :test delivery method accumulates sent emails in the
43 | # ActionMailer::Base.deliveries array.
44 | config.action_mailer.delivery_method = :test
45 |
46 | # Print deprecation notices to the stderr.
47 | config.active_support.deprecation = :stderr
48 |
49 | # Raise exceptions for disallowed deprecations.
50 | config.active_support.disallowed_deprecation = :raise
51 |
52 | # Tell Active Support which deprecation messages to disallow.
53 | config.active_support.disallowed_deprecation_warnings = []
54 |
55 | # Raises error for missing translations.
56 | # config.i18n.raise_on_missing_translations = true
57 |
58 | # Annotate rendered view with file names.
59 | # config.action_view.annotate_rendered_view_with_filenames = true
60 | end
61 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = {
28 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
29 | }
30 | else
31 | config.action_controller.perform_caching = false
32 |
33 | config.cache_store = :null_store
34 | end
35 |
36 | # Store uploaded files on the local file system (see config/storage.yml for options).
37 | config.active_storage.service = :local
38 |
39 | # Don't care if the mailer can't send.
40 | config.action_mailer.raise_delivery_errors = false
41 |
42 | config.action_mailer.perform_caching = false
43 |
44 | # Print deprecation notices to the Rails logger.
45 | config.active_support.deprecation = :log
46 |
47 | # Raise exceptions for disallowed deprecations.
48 | config.active_support.disallowed_deprecation = :raise
49 |
50 | # Tell Active Support which deprecation messages to disallow.
51 | config.active_support.disallowed_deprecation_warnings = []
52 |
53 | # Raise an error on page load if there are pending migrations.
54 | config.active_record.migration_error = :page_load
55 |
56 | # Highlight code that triggered database queries in logs.
57 | config.active_record.verbose_query_logs = true
58 |
59 | # Suppress logger output for asset requests.
60 | config.assets.quiet = true
61 |
62 | # Raises error for missing translations.
63 | # config.i18n.raise_on_missing_translations = true
64 |
65 | # Annotate rendered view with file names.
66 | # config.action_view.annotate_rendered_view_with_filenames = true
67 |
68 | # Uncomment if you wish to allow Action Cable access from any origin.
69 | # config.action_cable.disable_request_forgery_protection = true
70 | end
71 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.cache_classes = true
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
26 |
27 | # Compress CSS using a preprocessor.
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
34 | # config.asset_host = "http://assets.example.com"
35 |
36 | # Specifies the header that your server uses for sending files.
37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
39 |
40 | # Store uploaded files on the local file system (see config/storage.yml for options).
41 | config.active_storage.service = :local
42 |
43 | # Mount Action Cable outside main process or domain.
44 | # config.action_cable.mount_path = nil
45 | # config.action_cable.url = "wss://example.com/cable"
46 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
47 |
48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
49 | # config.force_ssl = true
50 |
51 | # Include generic and useful information about system operation, but avoid logging too much
52 | # information to avoid inadvertent exposure of personally identifiable information (PII).
53 | config.log_level = :info
54 |
55 | # Prepend all log lines with the following tags.
56 | config.log_tags = [ :request_id ]
57 |
58 | # Use a different cache store in production.
59 | # config.cache_store = :mem_cache_store
60 |
61 | # Use a real queuing backend for Active Job (and separate queues per environment).
62 | # config.active_job.queue_adapter = :resque
63 | # config.active_job.queue_name_prefix = "dummy_production"
64 |
65 | config.action_mailer.perform_caching = false
66 |
67 | # Ignore bad email addresses and do not raise email delivery errors.
68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
69 | # config.action_mailer.raise_delivery_errors = false
70 |
71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
72 | # the I18n.default_locale when a translation cannot be found).
73 | config.i18n.fallbacks = true
74 |
75 | # Don't log any deprecations.
76 | config.active_support.report_deprecations = false
77 |
78 | # Use default logging formatter so that PID and timestamp are not suppressed.
79 | config.log_formatter = ::Logger::Formatter.new
80 |
81 | # Use a different logger for distributed setups.
82 | # require "syslog/logger"
83 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
84 |
85 | if ENV["RAILS_LOG_TO_STDOUT"].present?
86 | logger = ActiveSupport::Logger.new(STDOUT)
87 | logger.formatter = config.log_formatter
88 | config.logger = ActiveSupport::TaggedLogging.new(logger)
89 | end
90 |
91 | # Do not dump schema after migrations.
92 | config.active_record.dump_schema_after_migration = false
93 | end
94 |
--------------------------------------------------------------------------------
/lib/art_vandelay.rb:
--------------------------------------------------------------------------------
1 | require "art_vandelay/version"
2 | require "art_vandelay/engine"
3 | require "csv"
4 |
5 | module ArtVandelay
6 | mattr_accessor :filtered_attributes, :from_address, :in_batches_of
7 | @@filtered_attributes = [:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn]
8 | @@in_batches_of = 10000
9 |
10 | def self.setup
11 | yield self
12 | end
13 |
14 | class Error < StandardError
15 | end
16 |
17 | class Export
18 | class Result
19 | attr_reader :exports
20 |
21 | def initialize(exports)
22 | @exports = exports
23 | end
24 | end
25 |
26 | # TODO attributes: self.filtered_attributes
27 | def initialize(records, export_sensitive_data: false, attributes: [], in_batches_of: ArtVandelay.in_batches_of)
28 | @records = records
29 | @export_sensitive_data = export_sensitive_data
30 | @attributes = attributes
31 | @in_batches_of = in_batches_of
32 | end
33 |
34 | def csv
35 | csv_exports = []
36 |
37 | if records.is_a?(ActiveRecord::Relation)
38 | records.in_batches(of: in_batches_of) do |relation|
39 | csv_exports << CSV.parse(generate_csv(relation), headers: true)
40 | end
41 | elsif records.is_a?(ActiveRecord::Base)
42 | csv_exports << CSV.parse(generate_csv(records), headers: true)
43 | end
44 |
45 | Result.new(csv_exports)
46 | end
47 |
48 | def json
49 | json_exports = []
50 |
51 | if records.is_a?(ActiveRecord::Relation)
52 | records.in_batches(of: in_batches_of) do |relation|
53 | json_exports << relation
54 | .map { |record| row(record.attributes, format: :hash) }
55 | .to_json
56 | end
57 | elsif records.is_a?(ActiveRecord::Base)
58 | json_exports << [row(records.attributes, format: :hash)].to_json
59 | end
60 |
61 | Result.new(json_exports)
62 | end
63 |
64 | def email(
65 | to:,
66 | from: ArtVandelay.from_address,
67 | subject: "#{model_name} export",
68 | body: "#{model_name} export",
69 | format: :csv
70 | )
71 | if from.nil?
72 | raise ArtVandelay::Error, "missing keyword: :from. Alternatively, set a value on ArtVandelay.from_address"
73 | end
74 |
75 | mailer = ActionMailer::Base.mail(to: to, from: from, subject: subject, body: body)
76 | exports = public_send(format).exports
77 |
78 | exports.each.with_index(1) do |export, index|
79 | if exports.one?
80 | mailer.attachments[file_name(format: format)] = export
81 | else
82 | file = file_name(suffix: "-#{index}", format: format)
83 | mailer.attachments[file] = export
84 | end
85 | end
86 |
87 | mailer.deliver
88 | end
89 |
90 | private
91 |
92 | attr_reader :records, :export_sensitive_data, :attributes, :in_batches_of
93 |
94 | def file_name(**options)
95 | options = options.symbolize_keys
96 | format = options[:format]
97 | suffix = options[:suffix]
98 | prefix = model_name.downcase
99 | timestamp = Time.current.in_time_zone("UTC").strftime("%Y-%m-%d-%H-%M-%S-UTC")
100 |
101 | "#{prefix}-export-#{timestamp}#{suffix}.#{format}"
102 | end
103 |
104 | def filtered_values(attributes, format:)
105 | attributes =
106 | if export_sensitive_data
107 | ActiveSupport::ParameterFilter.new([]).filter(attributes)
108 | else
109 | ActiveSupport::ParameterFilter.new(ArtVandelay.filtered_attributes).filter(attributes)
110 | end
111 |
112 | case format
113 | when :hash then attributes
114 | when :array then attributes.values
115 | end
116 | end
117 |
118 | def generate_csv(relation)
119 | CSV.generate do |csv|
120 | csv << header
121 | if relation.is_a?(ActiveRecord::Relation)
122 | relation.each do |record|
123 | csv << row(record.attributes)
124 | end
125 | elsif relation.is_a?(ActiveRecord::Base)
126 | csv << row(records.attributes)
127 | end
128 | end
129 | end
130 |
131 | def header
132 | if attributes.any?
133 | model.attribute_names.select do |column_name|
134 | standardized_attributes.include?(column_name)
135 | end
136 | else
137 | model.attribute_names
138 | end
139 | end
140 |
141 | def model
142 | model_name.constantize
143 | end
144 |
145 | def model_name
146 | records.model_name.name
147 | end
148 |
149 | def row(attributes, format: :array)
150 | if self.attributes.any?
151 | filtered_values(attributes.slice(*standardized_attributes), format:)
152 | else
153 | filtered_values(attributes, format:)
154 | end
155 | end
156 |
157 | def standardized_attributes
158 | attributes.map(&:to_s)
159 | end
160 | end
161 |
162 | class Import
163 | class Result
164 | attr_reader :rows_accepted, :rows_rejected
165 |
166 | def initialize(rows_accepted:, rows_rejected:)
167 | @rows_accepted = rows_accepted
168 | @rows_rejected = rows_rejected
169 | end
170 | end
171 |
172 | def initialize(model_name, **options)
173 | @options = options.symbolize_keys
174 | @rollback = options[:rollback]
175 | @strip = options[:strip]
176 | @model_name = model_name
177 | end
178 |
179 | def csv(csv_string, **options)
180 | options = options.symbolize_keys
181 | headers = options[:headers] || true
182 | attributes = options[:attributes] || {}
183 | rows = build_csv(csv_string, headers)
184 |
185 | if rollback
186 | # TODO: It would be nice to still return a result object during a
187 | # failure
188 | active_record.transaction do
189 | parse_rows(rows, attributes, raise_on_error: true)
190 | end
191 | else
192 | parse_rows(rows, attributes)
193 | end
194 | end
195 |
196 | def json(json_string, **options)
197 | options = options.symbolize_keys
198 | attributes = options[:attributes] || {}
199 | array = JSON.parse(json_string)
200 |
201 | if rollback
202 | active_record.transaction do
203 | parse_json_data(array, attributes, raise_on_error: true)
204 | end
205 | else
206 | parse_json_data(array, attributes)
207 | end
208 | end
209 |
210 | private
211 |
212 | attr_reader :model_name, :rollback, :strip
213 |
214 | def active_record
215 | model_name.to_s.classify.constantize
216 | end
217 |
218 | def build_csv(csv_string, headers)
219 | CSV.parse(csv_string, headers: headers)
220 | end
221 |
222 | def build_params(row, attributes)
223 | attributes = attributes.stringify_keys
224 |
225 | if strip
226 | row.to_h.stringify_keys.transform_keys do |key|
227 | attributes[key.strip] || key.strip
228 | end.tap do |new_params|
229 | new_params.transform_values!(&:strip)
230 | end
231 | else
232 | row.to_h.stringify_keys.transform_keys do |key|
233 | attributes[key] || key
234 | end
235 | end
236 | end
237 |
238 | def parse_json_data(array, attributes, **options)
239 | raise_on_error = options[:raise_on_error] || false
240 | result = Result.new(rows_accepted: [], rows_rejected: [])
241 |
242 | array.each do |entry|
243 | params = build_params(entry, attributes)
244 | record = active_record.new(params)
245 |
246 | if raise_on_error ? record.save! : record.save
247 | result.rows_accepted << {row: entry, id: record.id}
248 | else
249 | result.rows_rejected << {row: entry, errors: record.errors.messages}
250 | end
251 | end
252 |
253 | result
254 | end
255 |
256 | def parse_rows(rows, attributes, **options)
257 | options = options.symbolize_keys
258 | raise_on_error = options[:raise_on_error] || false
259 | result = Result.new(rows_accepted: [], rows_rejected: [])
260 |
261 | rows.each do |row|
262 | params = build_params(row, attributes)
263 | record = active_record.new(params)
264 |
265 | if raise_on_error ? record.save! : record.save
266 | result.rows_accepted << {row: row.fields, id: record.id}
267 | else
268 | result.rows_rejected << {row: row.fields, errors: record.errors.messages}
269 | end
270 | end
271 |
272 | result
273 | end
274 | end
275 | end
276 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🌐 Art Vandelay
2 | [](https://github.com/thoughtbot/art_vandelay/actions/workflows/ci.yml)
4 |
5 | Art Vandelay is an importer/exporter for Rails 7.0 and higher.
6 |
7 | Have you ever been on a project where, out of nowhere, someone asks you to send them a CSV of data? You think to yourself, “Ok, cool. No big deal. Just gimme five minutes”, but then that five minutes turns into a few hours. Art Vandelay can help.
8 |
9 | **At a high level, here’s what Art Vandelay can do:**
10 |
11 | - 🕶 Automatically [filters out sensitive information](#%EF%B8%8F-configuration).
12 | - 🔁 Export data [in batches](#exporting-in-batches).
13 | - 📧 [Email](#artvandelayexportemail) exported data.
14 | - 📥 [Import data](#-importing) from a CSV or JSON file.
15 |
16 | ## ✅ Installation
17 |
18 | Add this line to your application's Gemfile:
19 |
20 | ```ruby
21 | gem "art_vandelay"
22 | ```
23 |
24 | And then execute:
25 | ```bash
26 | $ bundle
27 | ```
28 |
29 | ## ⚙️ Configuration
30 |
31 | ```ruby
32 | # config/initializers/art_vandelay.rb
33 | ArtVandelay.setup do |config|
34 | config.filtered_attributes = [:credit_card, :birthday]
35 | config.from_address = "no-reply-export@example.com"
36 | config.in_batches_of = 5000
37 | end
38 | ```
39 | #### Default Values
40 |
41 | |Attribute|Value|Description|
42 | |---------|-----|-----------|
43 | |`filtered_attributes`|`[:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn]`|Attributes that will be automatically filtered when exported|
44 | |`from_address`|`nil`|The email address used when sending an email of exports|
45 | |`in_batches_of`|`10000`|The number of records that will be exported into each CSV|
46 |
47 | ## 🧰 Usage
48 |
49 | ### 📤 Exporting
50 |
51 | Art Vandelay supports exporting CSVs and JSON files.
52 |
53 | ```ruby
54 | ArtVandelay::Export.new(records, export_sensitive_data: false, attributes: [], in_batches_of: ArtVandelay.in_batches_of)
55 | ```
56 |
57 | |Argument|Description|
58 | |--------|-----------|
59 | |`records`|An [Active Record Relation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html) or an instance of an Active Record. E.g. `User.all`, `User.first`, `User.where(...)`, `User.find_by`|
60 | |`export_sensitive_data`|Export sensitive data. Defaults to `false`. Can be configured with `ArtVandelay.filtered_attributes`.|
61 | |`attributes`|An array attributes to export. Default to all.|
62 | |`in_batches_of`|The number of records that will be exported into each file. Defaults to 10,000. Can be configured with `ArtVandelay.in_batches_of`|
63 |
64 | #### ArtVandelay::Export#csv
65 |
66 | Returns an instance of `ArtVandelay::Export::Result`.
67 |
68 | ```ruby
69 | result = ArtVandelay::Export.new(User.all).csv
70 | # => #
71 |
72 | csv_exports = result.csv_exports
73 | # => [#, #, ...]
74 |
75 | csv = csv_exports.first.to_a
76 | # => [["id", "email", "password", "created_at", "updated_at"], ["1", "user@example.com", "[FILTERED]", "2022-10-25 09:20:28 UTC", "2022-10-25 09:20:28 UTC"]]
77 | ```
78 |
79 | #### ArtVandelay::Export#json
80 |
81 | Returns an instance of `ArtVandelay::Export::Result`.
82 |
83 | ```ruby
84 | result = ArtVandelay::Export.new(User.all).json
85 | # => #
86 |
87 | json_exports = result.json_exports
88 | # => [#, #, ...]
89 |
90 | json = JSON.parse(json_exports.first)
91 | # => [{"id"=>1, "email"=>"user@example.com", "password"=>"[FILTERED]", "created_at"=>"2022-10-25 09:20:28.123Z", "updated_at"=>"2022-10-25 09:20:28.123Z"}]
92 | ```
93 |
94 | ##### Exporting Sensitive Data
95 |
96 | ```ruby
97 | result = ArtVandelay::Export.new(User.all, export_sensitive_data: true).csv
98 | # => #
99 |
100 | password = result.csv_exports.first["password"]
101 | # => ["bosco"]
102 | ```
103 |
104 | ##### Exporting Specific Attributes
105 |
106 | ```ruby
107 | result = ArtVandelay::Export.new(User.all, attributes: [:email]).csv
108 | # => #
109 |
110 | csv = result.csv_exports.first.to_a
111 | # => [["email"], ["george@vandelay_industries.com"]]
112 | ```
113 |
114 | ##### Exporting in Batches
115 |
116 | ```ruby
117 | result = ArtVandelay::Export.new(User.all, in_batches_of: 100).csv
118 | # => #
119 |
120 | csv_size = result.csv_exports.first.size
121 | # => 100
122 | ```
123 |
124 | #### ArtVandelay::Export#email
125 |
126 | Emails the recipient(s) exports as attachments.
127 |
128 | ```ruby
129 | email(to:, from: ArtVandelay.from_address, subject: "#{model_name} export", body: "#{model_name} export")
130 | ```
131 |
132 | |Argument|Description|
133 | |---------|-----|
134 | |`to`|An array of email addresses representing who should receive the email.|
135 | |`from`|The email address of the sender.|
136 | |`subject`|The email subject. Defaults to the following pattern: "User export"|
137 | |`body`|The email body. Defaults to the following pattern: "User export"|
138 | |`format`|The format of the export file. Either `:csv` or `:json`.|
139 |
140 | ```ruby
141 | ArtVandelay::Export
142 | .new(User.where.not(confirmed: nil))
143 | .email(
144 | to: ["george@vandelay_industries.com", "kel_varnsen@vandelay_industries.com"],
145 | from: "noreply@vandelay_industries.com",
146 | subject: "List of confirmed users",
147 | body: "Here's an export of all confirmed users in our database.",
148 | format: :json
149 | )
150 | # => ActionMailer::Base#mail: processed outbound mail in...
151 | ```
152 |
153 | ### 📥 Importing
154 |
155 | ```ruby
156 | ArtVandelay::Import.new(model_name, **options)
157 | ```
158 |
159 | |Argument|Description|
160 | |--------|-----------|
161 | |`model_name`|The name of the model being imported. E.g. `:users`, `:user`, `"users"` or `"user"`|
162 | |`**options`|A hash of options. Available options are `rollback:`, `strip:`|
163 |
164 | #### Options
165 |
166 | |Option|Description|
167 | |------|-----------|
168 | |`rollback:`|Whether the import should rollback if any of the records fails to save.|
169 | |`strip:`|Strips leading and trailing whitespace from all values, including headers.|
170 |
171 | #### ArtVandelay::Import#csv
172 |
173 | Imports records from the supplied CSV. Returns an instance of `ArtVandelay::Import::Result`.
174 |
175 | ```ruby
176 | csv_string = CSV.generate do |csv|
177 | csv << ["email", "password"]
178 | csv << ["george@vandelay_industries.com", "bosco"]
179 | csv << ["kel_varnsen@vandelay_industries.com", nil]
180 | end
181 |
182 | result = ArtVandelay::Import.new(:users).csv(csv_string)
183 | # => #
184 |
185 | result.rows_accepted
186 | # => [{:row=>["george@vandelay_industries.com", "bosco"], :id=>1}]
187 |
188 | result.rows_rejected
189 | # => [{:row=>["kel_varnsen@vandelay_industries.com", nil], :errors=>{:password=>["can't be blank"]}}]
190 | ```
191 |
192 | ```ruby
193 | csv(csv_string, **options)
194 | ```
195 |
196 | |Argument|Description|
197 | |--------|-----------|
198 | |`csv_string`|Data in the form of a CSV string.|
199 | |`**options`|A hash of options. Available options are `headers:` and `attributes:`|
200 |
201 | ##### Options
202 |
203 | |Option|Description|
204 | |------|-----------|
205 | |`headers:`|The CSV headers. Use when the supplied CSV string does not have headers.|
206 | |`attributes:`|The attributes the headers should map to. Useful if the headers do not match the model's attributes.|
207 |
208 | ##### Setting headers
209 |
210 | ```ruby
211 | csv_string = CSV.generate do |csv|
212 | csv << ["george@vandelay_industries.com", "bosco"]
213 | end
214 |
215 | result = ArtVandelay::Import.new(:users).csv(csv_string, headers: [:email, :password])
216 | # => #
217 | ```
218 |
219 | #### ArtVandelay::Import#json
220 |
221 | Imports records from the supplied JSON. Returns an instance of `ArtVandelay::Import::Result`.
222 |
223 | ```ruby
224 | json_string = [
225 | {
226 | email: "george@vandelay_industries.com",
227 | password: "bosco"
228 | },
229 | {
230 | email: "kel_varnsen@vanderlay_industries.com",
231 | password: nil
232 | }
233 | ].to_json
234 |
235 | result = ArtVandelay::Import.new(:users).json(json_string)
236 | # => #
237 |
238 | result.rows_accepted
239 | # => [{:row=>[{"email"=>"george@vandelay_industries.com", "password"=>"bosco"}], :id=>1}]
240 |
241 | result.rows_rejected
242 | # => [{:row=>[{"email"=>"kel_varnsen@vandelay_industries.com", "password"=>nil}], :errors=>{:password=>["can't be blank"]}}]
243 | ```
244 |
245 | ```ruby
246 | json(json_string, **options)
247 | ```
248 |
249 | ##### Options
250 |
251 | |Option|Description|
252 | |------|-----------|
253 | |`attributes:`|The attributes the JSON object keys should map to. Useful if the headers do not match the model's attributes.|
254 |
255 | #### Rolling back if a record fails to save
256 |
257 | `ArtVandelay::Import.new` supports a `:rollback` keyword argument. It imports all rows as a single transaction and does not persist any records if one record fails due to an exception.
258 |
259 | ```ruby
260 | csv_string = CSV.generate do |csv|
261 | csv << ["email", "password"]
262 | csv << ["george@vandelay_industries.com", "bosco"]
263 | csv << ["kel_varnsen@vandelay_industries.com", nil]
264 | end
265 |
266 | result = ArtVandelay::Import.new(:users, rollback: true).csv(csv_string)
267 | # => rollback transaction
268 | ```
269 |
270 | #### Mapping custom headers
271 |
272 | Both `ArtVandelay::Import#csv` and `#json` support an `:attributes` keyword argument. This lets you map fields in the import document to your Active Record model's attributes.
273 |
274 | ```ruby
275 | csv_string = CSV.generate do |csv|
276 | csv << ["email_address", "passcode"]
277 | csv << ["george@vandelay_industries.com", "bosco"]
278 | end
279 |
280 | result = ArtVandelay::Import.new(:users).csv(csv_string, attributes: {email_address: :email, passcode: :password})
281 | # => #
282 | ```
283 |
284 | #### Stripping whitespace
285 |
286 | `ArtVandelay::Import.new` supports a `:strip` keyword argument to strip whitespace from values in the import document.
287 |
288 | ```ruby
289 | csv_string = CSV.generate do |csv|
290 | csv << ["email_address ", " passcode "]
291 | csv << [" george@vandelay_industries.com ", " bosco "]
292 | end
293 |
294 | result = ArtVandelay::Import.new(:users, strip: true).csv(csv_string, attributes: {email_address: :email, passcode: :password})
295 | # => #
296 |
297 | result.rows_accepted
298 | # => [{:row=>["george@vandelay_industries.com", "bosco"], :id=>1}]
299 | ```
300 |
301 | ## 🙏 Contributing
302 |
303 | 1. Run `./bin/setup`.
304 | 2. Make your changes.
305 | 3. Ensure `./bin/ci` passes.
306 | 4. Create a [pull request](https://github.com/thoughtbot/art_vandelay/compare).
307 |
308 | ## 📜 License
309 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
310 |
--------------------------------------------------------------------------------
/test/art_vandelay_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "csv"
3 |
4 | class ArtVandelayTest < ActiveSupport::TestCase
5 | class VERSION < ArtVandelayTest
6 | test "it has a version number" do
7 | assert ArtVandelay::VERSION
8 | end
9 |
10 | test "it has a supported Rails version" do
11 | assert_equal ">= 7.0", ArtVandelay::RAILS_VERSION
12 | end
13 |
14 | test "it has a supported Ruby version" do
15 | assert_equal ">= 3.1", ArtVandelay::MINIMUM_RUBY_VERSION
16 | end
17 | end
18 |
19 | class Setup < ArtVandelayTest
20 | test "it has the correct default values" do
21 | filtered_attributes = ArtVandelay.filtered_attributes
22 | from_address = ArtVandelay.from_address
23 | in_batches_of = ArtVandelay.in_batches_of
24 |
25 | assert_equal(
26 | [:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn],
27 | filtered_attributes
28 | )
29 | assert_nil from_address
30 | assert_equal 10000, in_batches_of
31 | end
32 | end
33 |
34 | class Export < ArtVandelayTest
35 | include ActionMailer::TestHelper
36 |
37 | test "CSV files return a ArtVandelay::Export::Result instance" do
38 | User.create!(email: "user@xample.com", password: "password")
39 |
40 | result = ArtVandelay::Export.new(User.all).csv
41 |
42 | assert_instance_of ArtVandelay::Export::Result, result
43 | end
44 |
45 | test "JSON files return a ArtVandelay::Export::Result instance" do
46 | User.create!(email: "user@xample.com", password: "password")
47 |
48 | result = ArtVandelay::Export.new(User.all).json
49 |
50 | assert_instance_of ArtVandelay::Export::Result, result
51 | end
52 |
53 | test "it creates a CSV containing the correct data" do
54 | user = User.create!(email: "user@xample.com", password: "password")
55 |
56 | result = ArtVandelay::Export.new(User.all).csv
57 | csv = result.exports.first
58 |
59 | assert_equal(
60 | [
61 | ["id", "email", "password", "created_at", "updated_at"],
62 | [user.id.to_s, user.email.to_s, "[FILTERED]", user.created_at.to_s, user.updated_at.to_s]
63 | ],
64 | csv.to_a
65 | )
66 | assert_equal(
67 | ["id", "email", "password", "created_at", "updated_at"],
68 | csv.headers
69 | )
70 | end
71 |
72 | test "it creates a JSON file containing the correct data" do
73 | user_1 = User.create!(email: "user_1@example.com", password: "password")
74 | user_2 = User.create!(email: "user_2@example.com", password: "password")
75 |
76 | result = ArtVandelay::Export.new(User.all).json
77 | json = result.exports.first
78 |
79 | assert_equal(
80 | [
81 | {
82 | "id" => user_1.id,
83 | "email" => "user_1@example.com",
84 | "password" => "[FILTERED]",
85 | "created_at" => user_1.created_at.iso8601(3),
86 | "updated_at" => user_1.updated_at.iso8601(3)
87 | },
88 | {
89 | "id" => user_2.id,
90 | "email" => "user_2@example.com",
91 | "password" => "[FILTERED]",
92 | "created_at" => user_2.created_at.iso8601(3),
93 | "updated_at" => user_2.updated_at.iso8601(3)
94 | }
95 | ],
96 | JSON.parse(json)
97 | )
98 | end
99 |
100 | test "it creates a CSV when passed one record" do
101 | user = User.create!(email: "user@xample.com", password: "password")
102 |
103 | result = ArtVandelay::Export.new(User.first).csv
104 | csv = result.exports.first
105 |
106 | assert_equal(
107 | [
108 | ["id", "email", "password", "created_at", "updated_at"],
109 | [user.id.to_s, user.email.to_s, "[FILTERED]", user.created_at.to_s, user.updated_at.to_s]
110 | ],
111 | csv.to_a
112 | )
113 | assert_equal(
114 | ["id", "email", "password", "created_at", "updated_at"],
115 | csv.headers
116 | )
117 | end
118 |
119 | test "it creates a JSON file when passed one record" do
120 | user = User.create!(email: "user@xample.com", password: "password")
121 | result = ArtVandelay::Export.new(User.first).json
122 | json = result.exports.first
123 |
124 | assert_equal(
125 | [
126 | {
127 | "id" => user.id,
128 | "email" => user.email.to_s,
129 | "password" => "[FILTERED]",
130 | "created_at" => user.created_at.iso8601(3),
131 | "updated_at" => user.updated_at.iso8601(3)
132 | }
133 | ],
134 | JSON.parse(json)
135 | )
136 | end
137 |
138 | test "it controls what data is filtered from CSV output" do
139 | user = User.create!(email: "user@xample.com", password: "password")
140 | ArtVandelay.setup do |config|
141 | config.filtered_attributes << :email
142 | end
143 |
144 | csv = ArtVandelay::Export.new(User.all).csv.exports.first
145 |
146 | assert_equal(
147 | [
148 | ["id", "email", "password", "created_at", "updated_at"],
149 | [user.id.to_s, "[FILTERED]", "[FILTERED]", user.created_at.to_s, user.updated_at.to_s]
150 | ],
151 | csv.to_a
152 | )
153 | ArtVandelay.filtered_attributes.delete(:email)
154 | end
155 |
156 | test "it controls what data is filtered from JSON output" do
157 | user = User.create!(email: "user@example.com", password: "password")
158 | ArtVandelay.setup do |config|
159 | config.filtered_attributes << :email
160 | end
161 |
162 | json = ArtVandelay::Export.new(User.all).json.exports.first
163 |
164 | assert_equal(
165 | [
166 | {
167 | "id" => user.id,
168 | "email" => "[FILTERED]",
169 | "password" => "[FILTERED]",
170 | "created_at" => user.created_at.iso8601(3),
171 | "updated_at" => user.updated_at.iso8601(3)
172 | }
173 | ],
174 | JSON.parse(json)
175 | )
176 |
177 | ArtVandelay.filtered_attributes.delete(:email)
178 | end
179 |
180 | test "it allows for unfiltered CSV exports" do
181 | user = User.create!(email: "user@xample.com", password: "password")
182 |
183 | csv = ArtVandelay::Export.new(User.all, export_sensitive_data: true).csv.exports.first
184 |
185 | assert_equal(
186 | [
187 | ["id", "email", "password", "created_at", "updated_at"],
188 | [user.id.to_s, user.email.to_s, "password", user.created_at.to_s, user.updated_at.to_s]
189 | ],
190 | csv.to_a
191 | )
192 | end
193 |
194 | test "it allows for unfiltered JSON exports" do
195 | user = User.create!(email: "user@example.com", password: "password")
196 | json = ArtVandelay::Export.new(User.all, export_sensitive_data: true)
197 | .json
198 | .exports
199 | .first
200 |
201 | assert_equal(
202 | [
203 | {
204 | "id" => user.id,
205 | "email" => user.email,
206 | "password" => "password",
207 | "created_at" => user.created_at.iso8601(3),
208 | "updated_at" => user.updated_at.iso8601(3)
209 | }
210 | ],
211 | JSON.parse(json)
212 | )
213 | end
214 |
215 | test "it controls what attributes are exported to CSVs" do
216 | user = User.create!(email: "user@xample.com", password: "password")
217 |
218 | csv = ArtVandelay::Export.new(User.all, attributes: [:id, "email"]).csv.exports.first
219 |
220 | assert_equal(
221 | [
222 | ["id", "email"],
223 | [user.id.to_s, user.email.to_s]
224 | ],
225 | csv.to_a
226 | )
227 | end
228 |
229 | test "controls what attributes are exported to JSON" do
230 | user = User.create!(email: "user@xample.com", password: "password")
231 | json = ArtVandelay::Export.new(User.all, attributes: [:id, "email"])
232 | .json
233 | .exports
234 | .first
235 |
236 | assert_equal [{"id" => user.id, "email" => user.email}],
237 | JSON.parse(json)
238 | end
239 |
240 | test "it batches CSV exports" do
241 | User.create!(email: "one@xample.com", password: "password")
242 | User.create!(email: "two@xample.com", password: "password")
243 |
244 | result = ArtVandelay::Export.new(User.all, in_batches_of: 1).csv
245 | csv_1 = result.exports.first
246 | csv_2 = result.exports.last
247 |
248 | assert "one@example.com", csv_1.first["email"]
249 | assert "two@example.com", csv_2.first["email"]
250 | end
251 |
252 | test "it batches JSON exports" do
253 | User.create!(email: "one@example.com", password: "password")
254 | User.create!(email: "two@example.com", password: "password")
255 |
256 | result = ArtVandelay::Export.new(User.all, in_batches_of: 1).json
257 | json_1 = result.exports.first
258 | json_2 = result.exports.last
259 |
260 | assert "one@example.com", json_1.first["email"]
261 | assert "two@example.com", json_2.first["email"]
262 | end
263 |
264 | test "it can set the default batch size" do
265 | User.create!(email: "one@xample.com", password: "password")
266 | User.create!(email: "two@xample.com", password: "password")
267 | ArtVandelay.setup do |config|
268 | config.in_batches_of = 1
269 | end
270 |
271 | result = ArtVandelay::Export.new(User.all).csv
272 | csv_1 = result.exports.first
273 | csv_2 = result.exports.last
274 |
275 | assert "one@example.com", csv_1.first["email"]
276 | assert "two@example.com", csv_2.first["email"]
277 |
278 | ArtVandelay.in_batches_of = 10000
279 | end
280 |
281 | test "it emails a CSV" do
282 | travel_to Date.new(1989, 12, 31).beginning_of_day
283 | user = User.create!(email: "user@xample.com", password: "password")
284 |
285 | assert_emails 1 do
286 | ArtVandelay::Export.new(User.all).email(
287 | to: ["recipient_1@examaple.com", "recipient_2@example.com"],
288 | from: "sender@example.com"
289 | )
290 | end
291 |
292 | email = ActionMailer::Base.deliveries.last
293 | csv = email.attachments.first
294 |
295 | assert_equal(
296 | ["recipient_1@examaple.com", "recipient_2@example.com"],
297 | email.to
298 | )
299 | assert_equal(
300 | [
301 | ["id", "email", "password", "created_at", "updated_at"],
302 | [user.id.to_s, user.email.to_s, "[FILTERED]", user.created_at.to_s, user.updated_at.to_s]
303 | ],
304 | CSV.parse(csv.body.raw_source)
305 | )
306 | assert_equal "user-export-1989-12-31-00-00-00-UTC.csv", csv.filename
307 | end
308 |
309 | test "it emails a JSON file" do
310 | travel_to Date.new(1989, 12, 31).beginning_of_day
311 | user = User.create!(email: "user@xample.com", password: "password")
312 |
313 | assert_emails 1 do
314 | ArtVandelay::Export.new(User.all).email(
315 | to: ["recipient_1@examaple.com", "recipient_2@example.com"],
316 | from: "sender@example.com",
317 | format: :json
318 | )
319 | end
320 |
321 | email = ActionMailer::Base.deliveries.last
322 | json = email.attachments.first
323 |
324 | assert_equal(
325 | ["recipient_1@examaple.com", "recipient_2@example.com"],
326 | email.to
327 | )
328 | assert_equal(
329 | [
330 | {
331 | "id" => user.id,
332 | "email" => user.email,
333 | "password" => "[FILTERED]",
334 | "created_at" => user.created_at.iso8601(3),
335 | "updated_at" => user.updated_at.iso8601(3)
336 | }
337 | ],
338 | JSON.parse(json.body.raw_source)
339 | )
340 | assert_equal "user-export-1989-12-31-00-00-00-UTC.json", json.filename
341 | end
342 |
343 | test "it requires a from address" do
344 | User.create!(email: "user@xample.com", password: "password")
345 |
346 | assert_raises ArtVandelay::Error do
347 | ArtVandelay::Export.new(User.all).email(
348 | to: ["recipient_1@examaple.com"]
349 | )
350 | end
351 | end
352 |
353 | test "it emails a CSV when one record is passed" do
354 | travel_to Date.new(1989, 12, 31).beginning_of_day
355 | user = User.create!(email: "user@xample.com", password: "password")
356 |
357 | assert_emails 1 do
358 | ArtVandelay::Export.new(User.first).email(
359 | to: ["recipient_1@examaple.com", "recipient_2@example.com"],
360 | from: "sender@example.com"
361 | )
362 | end
363 |
364 | email = ActionMailer::Base.deliveries.last
365 | csv = email.attachments.first
366 |
367 | assert_equal(
368 | ["recipient_1@examaple.com", "recipient_2@example.com"],
369 | email.to
370 | )
371 | assert_equal(
372 | [
373 | ["id", "email", "password", "created_at", "updated_at"],
374 | [user.id.to_s, user.email.to_s, "[FILTERED]", user.created_at.to_s, user.updated_at.to_s]
375 | ],
376 | CSV.parse(csv.body.raw_source)
377 | )
378 | assert_equal "user-export-1989-12-31-00-00-00-UTC.csv", csv.filename
379 | end
380 |
381 | test "it emails multiple CSV attachments" do
382 | travel_to Date.new(1989, 12, 31).beginning_of_day
383 | User.create!(email: "one@example.com", password: "password")
384 | User.create!(email: "two@example.com", password: "password")
385 |
386 | assert_emails 1 do
387 | ArtVandelay::Export.new(User.all, in_batches_of: 1).email(
388 | to: ["recipient_1@examaple.com"],
389 | from: "sender@example.com"
390 | )
391 | end
392 |
393 | email = ActionMailer::Base.deliveries.last
394 | csv_1 = email.attachments.first
395 | csv_2 = email.attachments.last
396 |
397 | assert_match "one@example.com", csv_1.body.raw_source
398 | assert_match "two@example.com", csv_2.body.raw_source
399 | assert_equal "user-export-1989-12-31-00-00-00-UTC-1.csv", csv_1.filename
400 | assert_equal "user-export-1989-12-31-00-00-00-UTC-2.csv", csv_2.filename
401 | end
402 |
403 | test "it raises an error if there is no data to export" do
404 | skip
405 | end
406 |
407 | test "it has a default subject" do
408 | User.create!(email: "user@xample.com", password: "password")
409 |
410 | ArtVandelay::Export.new(User.all).email(
411 | to: ["recipient_1@examaple.com", "recipient_2@example.com"],
412 | from: "sender@example.com"
413 | )
414 | email = ActionMailer::Base.deliveries.last
415 |
416 | assert_equal "User export", email.subject
417 | end
418 |
419 | test "it can set the subject" do
420 | User.create!(email: "user@xample.com", password: "password")
421 |
422 | ArtVandelay::Export.new(User.all).email(
423 | to: ["recipient_1@examaple.com", "recipient_2@example.com"],
424 | from: "sender@example.com",
425 | subject: "CUSTOM SUBJECT"
426 | )
427 | email = ActionMailer::Base.deliveries.last
428 |
429 | assert_equal "CUSTOM SUBJECT", email.subject
430 | end
431 |
432 | test "it can set a from address" do
433 | User.create!(email: "user@xample.com", password: "password")
434 |
435 | ArtVandelay::Export.new(User.all).email(
436 | to: ["recipient_1@examaple.com", "recipient_2@example.com"],
437 | from: "FROM@EMAIL.COM"
438 | )
439 | email = ActionMailer::Base.deliveries.last
440 |
441 | assert_equal "FROM@EMAIL.COM", email.from.first
442 | end
443 |
444 | test "it can set a default from address" do
445 | User.create!(email: "user@xample.com", password: "password")
446 | ArtVandelay.setup do |config|
447 | config.from_address = "DEFAULT@EMAIL.COM"
448 | end
449 | ArtVandelay::Export.new(User.all).email(
450 | to: ["recipient_1@examaple.com", "recipient_2@example.com"]
451 | )
452 | email = ActionMailer::Base.deliveries.last
453 |
454 | assert_equal "DEFAULT@EMAIL.COM", email.from.first
455 |
456 | ArtVandelay.from_address = nil
457 | end
458 |
459 | test "it has a default body" do
460 | User.create!(email: "user@xample.com", password: "password")
461 | ArtVandelay::Export.new(User.all).email(
462 | to: ["recipient_1@examaple.com", "recipient_2@example.com"],
463 | from: "sender@example.com"
464 | )
465 | email = ActionMailer::Base.deliveries.last
466 |
467 | assert_equal "User export", email.body.raw_source
468 | end
469 |
470 | test "it can set the body" do
471 | User.create!(email: "user@xample.com", password: "password")
472 | ArtVandelay::Export.new(User.all).email(
473 | to: ["recipient_1@examaple.com", "recipient_2@example.com"],
474 | from: "sender@example.com",
475 | body: "CUSTOM BODY"
476 | )
477 | email = ActionMailer::Base.deliveries.last
478 |
479 | assert_equal "CUSTOM BODY", email.body.raw_source
480 | end
481 | end
482 |
483 | class Import < ArtVandelayTest
484 | test "it imports data from a CSV string" do
485 | csv_string = CSV.generate do |csv|
486 | csv << %w[email password]
487 | csv << %w[email_1@example.com s3krit]
488 | csv << %w[email_2@example.com s3kure!]
489 | end
490 |
491 | assert_difference("User.count", 2) do
492 | ArtVandelay::Import.new(:users).csv(csv_string)
493 | end
494 |
495 | user_1 = User.find_by!(email: "email_1@example.com")
496 | user_2 = User.find_by!(email: "email_2@example.com")
497 |
498 | assert_equal "email_1@example.com", user_1.email
499 | assert_equal "s3krit", user_1.password
500 | assert_equal "email_2@example.com", user_2.email
501 | assert_equal "s3kure!", user_2.password
502 | end
503 |
504 | test "it imports data from a JSON string" do
505 | json_string = [
506 | {email: "email_1@example.com", password: "s3krit"},
507 | {email: "email_2@example.com", password: "s3kure!"}
508 | ].to_json
509 |
510 | assert_difference("User.count", 2) do
511 | ArtVandelay::Import.new(:users).json(json_string)
512 | end
513 |
514 | user_1 = User.find_by!(email: "email_1@example.com")
515 | user_2 = User.find_by!(email: "email_2@example.com")
516 |
517 | assert_equal "email_1@example.com", user_1.email
518 | assert_equal "s3krit", user_1.password
519 | assert_equal "email_2@example.com", user_2.email
520 | assert_equal "s3kure!", user_2.password
521 | end
522 |
523 | test "it strips whitespace from CSVs when strip configuration is passed" do
524 | csv_string = CSV.generate do |csv|
525 | csv << [" email ", " password "]
526 | csv << [" email_1@example.com ", " s3krit "]
527 | csv << [" email_2@example.com ", " s3kure! "]
528 | end
529 |
530 | assert_difference("User.count", 2) do
531 | ArtVandelay::Import.new(:users, strip: true).csv(csv_string)
532 | end
533 |
534 | user_1 = User.find_by!(email: "email_1@example.com")
535 | user_2 = User.find_by!(email: "email_2@example.com")
536 |
537 | assert_equal "email_1@example.com", user_1.email
538 | assert_equal "s3krit", user_1.password
539 | assert_equal "email_2@example.com", user_2.email
540 | assert_equal "s3kure!", user_2.password
541 | end
542 |
543 | test "it strips whitespace from JSON when strip configuration is passed" do
544 | json_string = [
545 | {email: " email_1@example.com ", password: " s3krit "},
546 | {email: " email_2@example.com ", password: " s3kure! "}
547 | ].to_json
548 |
549 | assert_difference("User.count", 2) do
550 | ArtVandelay::Import.new(:users, strip: true).json(json_string)
551 | end
552 |
553 | user_1 = User.find_by!(email: "email_1@example.com")
554 | user_2 = User.find_by!(email: "email_2@example.com")
555 |
556 | assert_equal "email_1@example.com", user_1.email
557 | assert_equal "s3krit", user_1.password
558 | assert_equal "email_2@example.com", user_2.email
559 | assert_equal "s3kure!", user_2.password
560 | end
561 |
562 | test "it sets the CSV headers" do
563 | csv_string = CSV.generate do |csv|
564 | csv << %w[email_1@example.com s3krit]
565 | csv << %w[email_2@example.com s3kure!]
566 | end
567 |
568 | assert_difference("User.count", 2) do
569 | ArtVandelay::Import.new(:users).csv(csv_string, headers: [:email, :password])
570 | end
571 |
572 | user_1 = User.find_by!(email: "email_1@example.com")
573 | user_2 = User.find_by!(email: "email_2@example.com")
574 |
575 | assert_equal "email_1@example.com", user_1.email
576 | assert_equal "s3krit", user_1.password
577 | assert_equal "email_2@example.com", user_2.email
578 | assert_equal "s3kure!", user_2.password
579 | end
580 |
581 | test "it maps CSV headers to Active Record attributes" do
582 | csv_string = CSV.generate do |csv|
583 | csv << %w[email_address passcode]
584 | csv << %w[email_1@example.com s3krit]
585 | csv << %w[email_2@example.com s3kure!]
586 | end
587 |
588 | assert_difference("User.count", 2) do
589 | ArtVandelay::Import.new(:users).csv(csv_string, attributes: {:email_address => :email, "passcode" => "password"})
590 | end
591 |
592 | user_1 = User.find_by!(email: "email_1@example.com")
593 | user_2 = User.find_by!(email: "email_2@example.com")
594 |
595 | assert_equal "email_1@example.com", user_1.email
596 | assert_equal "s3krit", user_1.password
597 | assert_equal "email_2@example.com", user_2.email
598 | assert_equal "s3kure!", user_2.password
599 | end
600 |
601 | test "it maps JSON keys to Active Record attributes" do
602 | json_string = [
603 | {email_address: "email_1@example.com", passcode: "s3krit"},
604 | {email_address: "email_2@example.com", passcode: "s3kure!"}
605 | ].to_json
606 |
607 | assert_difference("User.count", 2) do
608 | ArtVandelay::Import
609 | .new(:users)
610 | .json(
611 | json_string,
612 | attributes: {:email_address => :email, "passcode" => "password"}
613 | )
614 | end
615 |
616 | user_1 = User.find_by!(email: "email_1@example.com")
617 | user_2 = User.find_by!(email: "email_2@example.com")
618 |
619 | assert_equal "email_1@example.com", user_1.email
620 | assert_equal "s3krit", user_1.password
621 | assert_equal "email_2@example.com", user_2.email
622 | assert_equal "s3kure!", user_2.password
623 | end
624 |
625 | test "strips whitespace from CSVs if strip configuration is passed when using custom attributes" do
626 | csv_string = CSV.generate do |csv|
627 | csv << ["email_address ", " passcode "]
628 | csv << [" email_1@example.com ", " s3krit "]
629 | csv << [" email_2@example.com", " s3kure! "]
630 | end
631 |
632 | assert_difference("User.count", 2) do
633 | ArtVandelay::Import
634 | .new(:users, strip: true)
635 | .csv(csv_string, attributes: {:email_address => :email, "passcode" => "password"})
636 | end
637 |
638 | user_1 = User.find_by!(email: "email_1@example.com")
639 | user_2 = User.find_by!(email: "email_2@example.com")
640 |
641 | assert_equal "email_1@example.com", user_1.email
642 | assert_equal "s3krit", user_1.password
643 | assert_equal "email_2@example.com", user_2.email
644 | assert_equal "s3kure!", user_2.password
645 | end
646 |
647 | test "strips whitespace from JSON if the strip configuration is passed when using custom attributes" do
648 | json_string = [
649 | {email_address: " email_1@example.com ", passcode: " s3krit "},
650 | {email_address: " email_2@example.com", passcode: " s3kure! "}
651 | ].to_json
652 |
653 | assert_difference("User.count", 2) do
654 | ArtVandelay::Import
655 | .new(:users, strip: true)
656 | .json(
657 | json_string,
658 | attributes: {:email_address => :email, "passcode" => "password"}
659 | )
660 | end
661 |
662 | user_1 = User.find_by!(email: "email_1@example.com")
663 | user_2 = User.find_by!(email: "email_2@example.com")
664 |
665 | assert_equal "email_1@example.com", user_1.email
666 | assert_equal "s3krit", user_1.password
667 | assert_equal "email_2@example.com", user_2.email
668 | assert_equal "s3kure!", user_2.password
669 | end
670 |
671 | test "it no-ops if one record fails to save and 'rollback' is enabled" do
672 | csv_string = CSV.generate do |csv|
673 | csv << %w[email password]
674 | csv << %w[valid@example.com s3kure!]
675 | csv << %w[invalid@example.com]
676 | csv << %w[valid@example.com s3kure!]
677 | end
678 |
679 | json_string = [
680 | {email: "valid@example.com", password: "s3kure!"},
681 | {email: "invalid@example.com"},
682 | {email: "invalid2@example.com", password: nil}
683 | ].to_json
684 |
685 | assert_no_difference("User.count") do
686 | assert_raises ActiveRecord::RecordInvalid do
687 | ArtVandelay::Import.new(:users, rollback: true).csv(csv_string)
688 | end
689 | end
690 |
691 | assert_no_difference("User.count") do
692 | assert_raises ActiveRecord::RecordInvalid do
693 | ArtVandelay::Import.new(:users, rollback: true).json(json_string)
694 | end
695 | end
696 | end
697 |
698 | test "it saves other records if another fails to save" do
699 | csv_string = CSV.generate do |csv|
700 | csv << %w[email password]
701 | csv << %w[valid_1@example.com s3kure!]
702 | csv << %w[invalid@example.com]
703 | csv << %w[valid_2@example.com s3kure!]
704 | end
705 |
706 | assert_difference("User.count", 2) do
707 | ArtVandelay::Import.new(:users).csv(csv_string)
708 | end
709 |
710 | json_string = [
711 | {email: "valid_3@example.com", password: "s3kure!"},
712 | {email: "invalid@example.com"},
713 | {email: "invalid2@example.com", password: nil}
714 | ].to_json
715 |
716 | assert_difference("User.count", 1) do
717 | ArtVandelay::Import.new(:users).json(json_string)
718 | end
719 | end
720 |
721 | test "returns results" do
722 | csv_string = CSV.generate do |csv|
723 | csv << %w[email password]
724 | csv << %w[valid_1@example.com s3krit]
725 | csv << %w[invalid@example.com]
726 | csv << %w[valid_2@example.com s3krit]
727 | end
728 |
729 | csv_result = ArtVandelay::Import.new(:users).csv(csv_string)
730 |
731 | assert_equal(
732 | [
733 | {
734 | row: ["valid_1@example.com", "s3krit"],
735 | id: User.find_by!(email: "valid_1@example.com").id
736 | },
737 | {
738 | row: ["valid_2@example.com", "s3krit"],
739 | id: User.find_by!(email: "valid_2@example.com").id
740 | }
741 | ],
742 | csv_result.rows_accepted
743 | )
744 | assert_equal(
745 | [
746 | row: ["invalid@example.com", nil],
747 | errors: {password: [I18n.t("errors.messages.blank")]}
748 | ],
749 | csv_result.rows_rejected
750 | )
751 |
752 | json_string = [
753 | {email: "valid_3@example.com", password: "s3kure!"},
754 | {email: "invalid@example.com"},
755 | {email: "invalid2@example.com", password: nil}
756 | ].to_json
757 |
758 | json_result = ArtVandelay::Import.new(:users).json(json_string)
759 |
760 | assert_equal(
761 | [
762 | {
763 | row: {"email" => "valid_3@example.com", "password" => "s3kure!"},
764 | id: User.find_by!(email: "valid_3@example.com").id
765 | }
766 | ],
767 | json_result.rows_accepted
768 | )
769 |
770 | assert_equal(
771 | [
772 | {
773 | row: {"email" => "invalid@example.com"},
774 | errors: {password: [I18n.t("errors.messages.blank")]}
775 | },
776 | {
777 | row: {"email" => "invalid2@example.com", "password" => nil},
778 | errors: {password: [I18n.t("errors.messages.blank")]}
779 | }
780 | ],
781 | json_result.rows_rejected
782 | )
783 | end
784 |
785 | test "it returns results when rollback is enabled" do
786 | csv_string = CSV.generate do |csv|
787 | csv << %w[email password]
788 | csv << %w[valid_1@example.com s3krit]
789 | csv << %w[valid_2@example.com s3krit]
790 | end
791 |
792 | csv_result = ArtVandelay::Import.new(:users, rollback: true).csv(csv_string)
793 |
794 | assert_equal(
795 | [
796 | {
797 | row: ["valid_1@example.com", "s3krit"],
798 | id: User.find_by!(email: "valid_1@example.com").id
799 | },
800 | {
801 | row: ["valid_2@example.com", "s3krit"],
802 | id: User.find_by!(email: "valid_2@example.com").id
803 | }
804 | ],
805 | csv_result.rows_accepted
806 | )
807 | assert_empty csv_result.rows_rejected
808 |
809 | json_string = [
810 | {email: "valid_3@example.com", password: "s3kure!"}
811 | ].to_json
812 |
813 | json_result =
814 | ArtVandelay::Import.new(:users, rollback: true).json(json_string)
815 |
816 | assert_equal(
817 | [
818 | {
819 | row: {"email" => "valid_3@example.com", "password" => "s3kure!"},
820 | id: User.find_by!(email: "valid_3@example.com").id
821 | }
822 | ],
823 | json_result.rows_accepted
824 | )
825 | assert_empty json_result.rows_rejected
826 | end
827 |
828 | test "it updates existing records" do
829 | # TODO: This requires more thought. We need a way to identify the
830 | # record(s) and declare we want to update them. This seems like it could
831 | # be a responsibility of a different class?
832 | skip
833 | end
834 | end
835 | end
836 |
--------------------------------------------------------------------------------