Skip to content

Commit

Permalink
Merge pull request #5 from DmitryTsepelev/array-types
Browse files Browse the repository at this point in the history
Add array type generation via Model#to_array_type
  • Loading branch information
DmitryTsepelev committed Apr 30, 2019
2 parents f1bd86d + 59ab3da commit 050cac9
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 57 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
Expand Up @@ -10,6 +10,7 @@ rvm:
- ruby-head
gemfile:
- gemfiles/rails_5_2.gemfile
- gemfiles/rails_6_0.gemfile
- gemfiles/railsmaster.gemfile

notifications:
Expand All @@ -18,9 +19,14 @@ notifications:
matrix:
fast_finish: true
exclude:
- rvm: 2.3
gemfile: gemfiles/rails_6_0.gemfile
- rvm: 2.3
gemfile: gemfiles/railsmaster.gemfile
- rvm: 2.4
gemfile: gemfiles/rails_6_0.gemfile
- rvm: 2.4
gemfile: gemfiles/railsmaster.gemfile
allow_failures:
- rvm: ruby-head
- gemfile: gemfiles/railsmaster.gemfile
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,9 @@

## master

- [PR #5](https://github.com/DmitryTsepelev/store_model/pull/5) Raise error when `#cast` cannot handle the passed instance ([@DmitryTsepelev][])
- [PR #5](https://github.com/DmitryTsepelev/store_model/pull/5) Add array type generation via Model#to_array_type ([@DmitryTsepelev][])

## 0.1.2 (2019-03-14)

- `:store_model` validation should not allow nil by default
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.local
@@ -0,0 +1,2 @@
gem "rails", github: "rails/rails"
gem "sqlite3", "~> 1.4"
12 changes: 12 additions & 0 deletions README.md
Expand Up @@ -70,6 +70,18 @@ class Product < ApplicationRecord
end
```

## Handling arrays

Should you store an array of models, you can use `#to_array_type` method:

```ruby
class Product < ApplicationRecord
attribute :configurations, Configuration.to_array_type
end
```

After that, your attribute will return array of `Configuration` instances.

## Validations

`StoreModel` supports all the validations shipped with `ActiveModel`. Start with defining validation for the store model:
Expand Down
6 changes: 6 additions & 0 deletions gemfiles/rails_6_0.gemfile
@@ -0,0 +1,6 @@
source "https://rubygems.org"

gem "sqlite3", "~> 1.4.0"
gem "activerecord", "~> 6.0.0.rc1"

gemspec path: "../"
2 changes: 1 addition & 1 deletion gemfiles/railsmaster.gemfile
@@ -1,6 +1,6 @@
source "https://rubygems.org"

gem "rails", github: "rails/rails"
gem "sqlite3", "~> 1.3.6"
gem "sqlite3", "~> 1.4"

gemspec path: ".."
42 changes: 0 additions & 42 deletions lib/store_model/json_model_type.rb

This file was deleted.

8 changes: 6 additions & 2 deletions lib/store_model/model.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require "store_model/json_model_type"
require "store_model/types"

module StoreModel
module Model
Expand All @@ -10,7 +10,11 @@ def self.included(base)

base.extend(Module.new do
def to_type
JsonModelType.new(self)
Types::JsonType.new(self)
end

def to_array_type
Types::ArrayType.new(self)
end
end)
end
Expand Down
10 changes: 10 additions & 0 deletions lib/store_model/types.rb
@@ -0,0 +1,10 @@
# frozen_string_literal: true

require "store_model/types/json_type"
require "store_model/types/array_type"

module StoreModel
module Types
class CastError < StandardError; end
end
end
55 changes: 55 additions & 0 deletions lib/store_model/types/array_type.rb
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require "active_model"

module StoreModel
module Types
class ArrayType < ActiveModel::Type::Value
def initialize(model_klass)
@model_klass = model_klass
end

def type
:array
end

def cast_value(value)
case value
when String then decode_and_initialize(value)
when Array then ensure_model_class(value)
else
raise StoreModel::Types::CastError,
"failed casting #{value.inspect}, only String or Array instances are allowed"
end
end

def serialize(value)
case value
when Array
ActiveSupport::JSON.encode(value)
else
super
end
end

def changed_in_place?(raw_old_value, new_value)
cast_value(raw_old_value) != new_value
end

private

# rubocop:disable Style/RescueModifier
def decode_and_initialize(array_value)
decoded = ActiveSupport::JSON.decode(array_value) rescue []
decoded.map { |attributes| @model_klass.new(attributes) }
end
# rubocop:enable Style/RescueModifier

def ensure_model_class(array)
array.map do |object|
object.is_a?(@model_klass) ? object : @model_klass.new(object)
end
end
end
end
end
51 changes: 51 additions & 0 deletions lib/store_model/types/json_type.rb
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require "active_model"

module StoreModel
module Types
class JsonType < ActiveModel::Type::Value
def initialize(model_klass)
@model_klass = model_klass
end

def type
:json
end

def cast_value(value)
case value
when String then decode_and_initialize(value)
when Hash then @model_klass.new(value)
when @model_klass then value
else
raise StoreModel::Types::CastError,
"failed casting #{value.inspect}, only String, " \
"Hash or #{@model_klass.name} instances are allowed"
end
end

def serialize(value)
case value
when Hash, @model_klass
ActiveSupport::JSON.encode(value)
else
super
end
end

def changed_in_place?(raw_old_value, new_value)
cast_value(raw_old_value) != new_value
end

private

# rubocop:disable Style/RescueModifier
def decode_and_initialize(value)
decoded = ActiveSupport::JSON.decode(value) rescue nil
@model_klass.new(decoded) unless decoded.nil?
end
# rubocop:enable Style/RescueModifier
end
end
end
16 changes: 15 additions & 1 deletion spec/store_model/model_spec.rb
Expand Up @@ -69,7 +69,7 @@
end
end

describe ".as_type" do
describe ".to_type" do
subject { custom_product_class.new }

let(:custom_product_class) do
Expand All @@ -82,4 +82,18 @@
expect(subject.configuration).to be_a_kind_of(Configuration)
end
end

describe ".to_array_type" do
subject { custom_product_class.new }

let(:custom_product_class) do
build_custom_product_class do
attribute :configuration, Configuration.to_array_type
end
end

it "configures type using field name" do
expect(subject.configuration).to be_a_kind_of(Array)
end
end
end
96 changes: 96 additions & 0 deletions spec/store_model/types/array_type_spec.rb
@@ -0,0 +1,96 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe StoreModel::Types::ArrayType do
let(:type) { described_class.new(Configuration) }

let(:attributes_array) do
[
{
color: "red",
disabled_at: Time.new(2019, 2, 22, 12, 30)
},
{
color: "green",
disabled_at: Time.new(2019, 3, 12, 8, 10)
}
]
end

describe "#type" do
subject { type.type }

it { is_expected.to eq(:array) }
end

describe "#changed_in_place?" do
let(:configurations) do
attributes_array.map { |attributes| Configuration.new(attributes) }
end

it "marks object as changed" do
expect(type.changed_in_place?([], configurations)).to be_truthy
end
end

describe "#cast_value" do
shared_examples "cast examples" do
subject { type.cast_value(value) }

it { is_expected.to be_a(Array) }
it "assigns attributes" do
subject.zip(attributes_array).each do |config, config_attributes|
expect(config).to have_attributes(config_attributes)
end
end
end

context "when String is passed" do
let(:value) { ActiveSupport::JSON.encode(attributes_array) }
include_examples "cast examples"
end

context "when Array of hashes is passed" do
let(:value) { attributes_array }
include_examples "cast examples"
end

context "when Array of instances is passed" do
let(:value) { attributes_array.map { |attrs| Configuration.new(attrs) } }
include_examples "cast examples"
end

context "when instance of illegal class is passed" do
let(:value) { {} }

it "raises exception" do
expect { type.cast_value(value) }.to raise_error(
StoreModel::Types::CastError,
"failed casting {}, only String or Array instances are allowed"
)
end
end
end

describe "#serialize" do
shared_examples "serialize examples" do
subject { type.serialize(value) }

it { is_expected.to be_a(String) }
it "is equal to attributes" do
expect(subject).to eq(ActiveSupport::JSON.encode(attributes_array))
end
end

context "when Array is passed" do
let(:value) { attributes_array }
include_examples "serialize examples"
end

context "when String is passed" do
let(:value) { ActiveSupport::JSON.encode(attributes_array) }
include_examples "serialize examples"
end
end
end

0 comments on commit 050cac9

Please sign in to comment.