1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
# frozen-string-literal: true
require 'csv'
module Sequel
module Plugins
# csv_serializer handles serializing entire Sequel::Model objects to CSV,
# as well as support for deserializing CSV directly into Sequel::Model
# objects. It requires the csv standard library.
#
# Basic Example:
#
# album = Album[1]
# album.to_csv(write_headers: true)
# # => "id,name,artist_id\n1,RF,2\n"
#
# You can provide options to control the CSV output:
#
# album.to_csv(only: :name)
# album.to_csv(except: [:id, :artist_id])
# # => "RF\n"
#
# +to_csv+ also exists as a class and dataset method, both of which return
# all objects in the dataset:
#
# Album.to_csv
# Album.where(artist_id: 1).to_csv
#
# If you have an existing array of model instances you want to convert to
# CSV, you can call the class to_csv method with the :array option:
#
# Album.to_csv(array: [Album[1], Album[2]])
#
# In addition to creating CSV, this plugin also enables Sequel::Model
# classes to create instances directly from CSV using the from_csv class
# method:
#
# csv = album.to_csv
# album = Album.from_csv(csv)
#
# The array_from_csv class method exists to parse arrays of model instances
# from CSV:
#
# csv = Album.where(artist_id: 1).to_csv
# albums = Album.array_from_csv(csv)
#
# These do not necessarily round trip, since doing so would let users
# create model objects with arbitrary values. By default, from_csv will
# call set with the values in the hash. If you want to specify the allowed
# fields, you can use the :headers option.
#
# Album.from_csv(album.to_csv, headers: %w'id name')
#
# If you want to update an existing instance, you can use the from_csv
# instance method:
#
# album.from_csv(csv)
#
# Usage:
#
# # Add CSV output capability to all model subclass instances (called
# # before loading subclasses)
# Sequel::Model.plugin :csv_serializer
#
# # Add CSV output capability to Album class instances
# Album.plugin :csv_serializer
module CsvSerializer
# Set up the column readers to do deserialization and the column writers
# to save the value in deserialized_values
def self.configure(model, opts = OPTS)
model.instance_exec do
@csv_serializer_opts = (@csv_serializer_opts || OPTS).merge(opts)
end
end
# Avoid keyword argument separation warnings on Ruby 2.7, while still
# being compatible with 1.9.
if RUBY_VERSION >= "2.0"
instance_eval(<<-END, __FILE__, __LINE__+1)
def self.csv_call(*args, opts, &block)
CSV.send(*args, **opts, &block)
end
END
else
# :nocov:
# :nodoc:
def self.csv_call(*args, opts, &block)
CSV.send(*args, opts, &block)
end
# :nodoc:
# :nocov:
end
module ClassMethods
# The default opts to use when serializing model objects to CSV
attr_reader :csv_serializer_opts
# Attempt to parse an array of instances from the given CSV string
def array_from_csv(csv, opts = OPTS)
CsvSerializer.csv_call(:parse, csv, process_csv_serializer_opts(opts)).map do |row|
row = row.to_hash
row.delete(nil)
new(row)
end
end
# Freeze csv serializier opts when freezing model class
def freeze
@csv_serializer_opts.freeze.each_value do |v|
v.freeze if v.is_a?(Array) || v.is_a?(Hash)
end
super
end
# Attempt to parse a single instance from the given CSV string
def from_csv(csv, opts = OPTS)
new.from_csv(csv, opts)
end
# Convert the options hash to one that can be passed to CSV.
def process_csv_serializer_opts(opts)
opts = (csv_serializer_opts || OPTS).merge(opts)
opts_cols = opts.delete(:columns)
opts_include = opts.delete(:include)
opts_except = opts.delete(:except)
only = opts.delete(:only)
opts[:headers] ||= Array(only || opts_cols || columns) + Array(opts_include) - Array(opts_except)
opts
end
Plugins.inherited_instance_variables(
self, :@csv_serializer_opts => lambda do |csv_serializer_opts|
opts = {}
csv_serializer_opts.each do |k, v|
opts[k] = (v.is_a?(Array) || v.is_a?(Hash)) ? v.dup : v
end
opts
end)
Plugins.def_dataset_methods(self, :to_csv)
end
module InstanceMethods
# Update the object using the data provided in the first line in CSV. Options:
#
# :headers :: The headers to use for the CSV line. Use nil for a header
# to specify the column should be ignored.
def from_csv(csv, opts = OPTS)
row = CsvSerializer.csv_call(:parse_line, csv, model.process_csv_serializer_opts(opts)).to_hash
row.delete(nil)
set(row)
end
# Return a string in CSV format. Accepts the same options as CSV.new,
# as well as the following options:
#
# :except :: Symbol or Array of Symbols of columns not to include in
# the CSV output.
# :only :: Symbol or Array of Symbols of columns to include in the CSV
# output, ignoring all other columns
# :include :: Symbol or Array of Symbols specifying non-column
# attributes to include in the CSV output.
def to_csv(opts = OPTS)
opts = model.process_csv_serializer_opts(opts)
headers = opts[:headers]
CsvSerializer.csv_call(:generate, model.process_csv_serializer_opts(opts)) do |csv|
csv << headers.map{|k| public_send(k)}
end
end
end
module DatasetMethods
# Return a CSV string representing an array of all objects in this
# dataset. Takes the same options as the instance method, and passes
# them to every instance. Accepts the same options as CSV.new, as well
# as the following options:
#
# :array :: An array of instances. If this is not provided, calls #all
# on the receiver to get the array.
def to_csv(opts = OPTS)
opts = model.process_csv_serializer_opts({:columns=>columns}.merge!(opts))
items = opts.delete(:array) || self
headers = opts[:headers]
CsvSerializer.csv_call(:generate, opts) do |csv|
items.each do |object|
csv << headers.map{|header| object.public_send(header)}
end
end
end
end
end
end
end
|