Sig
Sig

Reputation: 5950

Casting to custom type a PostgreSQL array

In a Rails (5.2) app I have the Project model with a tags attribute defined as a Postgresql array.

  create_table :projects do |t|
    ...
    t.text :tags, array: true, default: []
    ...
  end

Instead of handling tags as strings I'd like to cast them to Tags objects

class Tag
  ...
  attr_reader :name

  def initialize(name)
    @name = name
  end
  ...
end

To achieve so I'm trying to use the attributes API that comes with Rails 5.

class Project < ApplicationRecord
  attribute :tags, TagType.new, array: true
  ...
end

class TagType < ActiveRecord::Type::Value
  def cast(names)
    names.split(',').map { |name| Tag.new(name) }
  end
end

This kind of work, it creates Tags object but the first and last have brackets in the name.

Project.create(tags: ['one', 'two', 'three'])
Project.first.tags.map(&:name) #=> ['{one', 'two', 'three}']

Is there a better way than manually removing the brackets from names in TagType to get proper Tags?

Trying to find in Rails code where the array value is parsed but no luck so far.

Upvotes: 4

Views: 1297

Answers (2)

Johan
Johan

Reputation: 41

This is a more generic version of this, which is what I was looking for:

# config/initializers/text_array.rb
class TextArrayType < ActiveRecord::Type::Value
  include ActiveModel::Type::Helpers::Mutable

  def cast(value)
    case
    when value.is_a?(Array)
      value
    when value.present?
      value.split(/[\s,]+/)
    else
      []
    end
  end

  def deserialize(value)
    PG::TextDecoder::Array.new.decode(value)
  end

  def serialize(value)
    PG::TextEncoder::Array.new.encode(value)
  end

end

ActiveRecord::Type.register(:text_array, TextArrayType)

This will enable you to add tags like:

create_table :projects do |t|
  ...
  t.text :tags, array: true, default: []
  ...
end

class Project < ApplicationRecord
  attribute :tags, :text_array
end

What I wanted to achieve was to be able to add tags both as an array and as a comma separated list like:

Project.new(tags: ["x", "y", "z"] # => tags: ["x", "y", "z"]
Project.new(tags: "x, y, z") # => tags: ["x", "y", "z"]

This will enable you to add multiple tags in a form as a comma separated list:

f.text_area :tags, value: @project.tags.join(", ")

as well as managing tags as an array everywhere else in the project.

Upvotes: 4

Sig
Sig

Reputation: 5950

Here the code I ended up with

class TagType < ActiveRecord::Type::Value
  include ActiveModel::Type::Helpers::Mutable

  def cast(name)
    Tag.new(name)
  end

  def deserialize(names)
    PG::TextDecoder::Array.new.decode(names).map { |name| cast(name) }
  end

  def serialize(tags)
    PG::TextEncoder::Array.new.encode(tags.map(&:name))
  end
end

Hope this helps.

Upvotes: 3

Related Questions