Skip to content

Commit

Permalink
Handle UnknownAttributeError and put attributes to #unknown_attributes (
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitryTsepelev committed Aug 5, 2019
1 parent cd1f6e7 commit 7628b24
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

## master

- [PR #22](https://github.com/DmitryTsepelev/store_model/pull/22) Store unknown attributes in `#unknown_attributes` ([@DmitryTsepelev][])

## 0.4.1 (2019-08-31)

- [PR #21](https://github.com/DmitryTsepelev/store_model/pull/21) Properly validate and handle nested models ([@DmitryTsepelev][])
Expand Down
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -82,10 +82,11 @@ product.save
## Documentation

1. [Installation](./docs/installation.md)
2. `StoreModel::Model` API:
2. StoreModel::Model API:
* [Validations](./docs/validations.md)
* [Enums](./docs/enums.md)
* [Nested models](./docs/nested_models.md)
* [Unknown attributes](./docs/unknown_attributes.md)
3. [Array of stored models](./docs/array_of_stored_models.md)
4. [Alternatives](./docs/alternatives.md)

Expand Down
14 changes: 14 additions & 0 deletions docs/unknown_attributes.md
@@ -0,0 +1,14 @@
# Unknown attributes

Sometimes JSON structure changes and it contains more keys than attributes defined in your `StoreModel::Model` class. In such cases, "unknown" attributes can be found inside the `#unknown_attributes` hash:

```ruby
class Configuration
include StoreModel::Model

attribute :color, :string
end

configuration = Configuration.new(color: "red", archived: true)
configuration.unknown_attributes # => { "archived" => true }
```
4 changes: 4 additions & 0 deletions lib/store_model/model.rb
Expand Up @@ -39,5 +39,9 @@ def inspect
def type_for_attribute(attribute)
self.class.attribute_types[attribute.to_s]
end

def unknown_attributes
@unknown_attributes ||= {}
end
end
end
24 changes: 20 additions & 4 deletions lib/store_model/types/json_type.rb
Expand Up @@ -18,11 +18,10 @@ def cast_value(value)
when String then decode_and_initialize(value)
when Hash then @model_klass.new(value)
when @model_klass, nil then value
else
raise StoreModel::Types::CastError,
"failed casting #{value.inspect}, only String, " \
"Hash or #{@model_klass.name} instances are allowed"
else raise_cast_error(value)
end
rescue ActiveModel::UnknownAttributeError => e
handle_unknown_attribute(value, e)
end

def serialize(value)
Expand All @@ -44,8 +43,25 @@ def changed_in_place?(raw_old_value, new_value)
def decode_and_initialize(value)
decoded = ActiveSupport::JSON.decode(value) rescue nil
@model_klass.new(decoded) unless decoded.nil?
rescue ActiveModel::UnknownAttributeError => e
handle_unknown_attribute(decoded, e)
end
# rubocop:enable Style/RescueModifier

def raise_cast_error(value)
raise StoreModel::Types::CastError,
"failed casting #{value.inspect}, only String, " \
"Hash or #{@model_klass.name} instances are allowed"
end

def handle_unknown_attribute(value, exception)
attribute = exception.attribute.to_sym
value_symbolized = value.symbolize_keys

cast_value(value_symbolized.except(attribute)).tap do |configuration|
configuration.unknown_attributes[attribute.to_s] = value_symbolized[attribute]
end
end
end
end
end
42 changes: 34 additions & 8 deletions spec/store_model/types/json_type_spec.rb
Expand Up @@ -27,24 +27,24 @@
describe "#cast_value" do
subject { type.cast_value(value) }

shared_examples "cast examples" do
shared_examples "for known attributes" do
it { is_expected.to be_a(Configuration) }
it("assigns attributes") { is_expected.to have_attributes(attributes) }
end

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

context "when Hash is passed" do
let(:value) { attributes }
include_examples "cast examples"
include_examples "for known attributes"
end

context "when String is passed" do
let(:value) { ActiveSupport::JSON.encode(attributes) }
include_examples "for known attributes"
end

context "when Configuration instance is passed" do
let(:value) { Configuration.new(attributes) }
include_examples "cast examples"
include_examples "for known attributes"
end

context "when nil is passed" do
Expand All @@ -63,6 +63,32 @@
)
end
end

context "when some keys are not defined as attributes" do
shared_examples "for unknown attributes" do
it { is_expected.to be_a(Configuration) }

it("assigns attributes") { is_expected.to have_attributes(color: "red") }

it "assigns unknown_attributes" do
expect(subject.unknown_attributes).to eq(
"unknown_attribute" => "something", "one_more" => "anything"
)
end
end

let(:attributes) { { color: "red", unknown_attribute: "something", one_more: "anything" } }

context "when Hash is passed" do
let(:value) { attributes }
include_examples "for unknown attributes"
end

context "when String is passed" do
let(:value) { ActiveSupport::JSON.encode(attributes) }
include_examples "for unknown attributes"
end
end
end

describe "#serialize" do
Expand Down

0 comments on commit 7628b24

Please sign in to comment.