Skip to content

Commit

Permalink
Merge pull request #169 from jrochkind/model_dup
Browse files Browse the repository at this point in the history
Closer compat to ActiveModel conventions
  • Loading branch information
jrochkind authored Jan 5, 2023
2 parents de28a6d + 86bb1a4 commit eb4c4b4
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 2 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ Notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/jrochkind/attr_json/compare/v1.4.0...HEAD)
## [Unreleased](https://github.com/jrochkind/attr_json/compare/v1.4.1...HEAD)

### Added

* AttrJson::Model#dup will properly deep-dup attributes https://github.com/jrochkind/attr_json/pull/169

* AttrJson::Model#freeze will freeze attributes -- but not deep-freeze. https://github.com/jrochkind/attr_json/pull/169

* AttrJson::Model has some methods conventional in ActiveModel classes: Klass.attribute_types, Klass.attribute_names, and instance.attribute_names.

## [1.4.1](https://github.com/jrochkind/attr_json/compare/v1.4.0...v1.4.1)

Expand Down
31 changes: 31 additions & 0 deletions lib/attr_json/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,17 @@ def to_serialization_coder
@serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
end

# like the ActiveModel::Attributes method
def attribute_names
attr_json_registry.attribute_names
end

# like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values
def attribute_types
attribute_names.collect { |name| [name.to_s, attr_json_registry.type_for_attribute(name)]}.to_h
end


# Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
# be looked up in `ActiveModel::Type.lookup`
#
Expand Down Expand Up @@ -197,6 +208,12 @@ def initialize(attributes = {})
fill_in_defaults!
end

# inspired by https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb
def initialize_dup(other) # :nodoc:
@attributes = @attributes.deep_dup
super
end

def attributes
@attributes ||= {}
end
Expand Down Expand Up @@ -232,6 +249,11 @@ def has_attribute?(str)
self.class.attr_json_registry.has_attribute?(str)
end

# like the ActiveModel::Attributes method
def attribute_names
self.class.attribute_names
end

# Override from ActiveModel::Serialization to #serialize
# by type to make sure any values set directly on hash still
# get properly type-serialized.
Expand Down Expand Up @@ -278,6 +300,15 @@ def _destroy
false
end

# like ActiveModel::Attributes at
# https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb#L120
#
# is not a full deep freeze
def freeze
attributes.freeze unless frozen?
super
end

private

def fill_in_defaults!
Expand Down
85 changes: 84 additions & 1 deletion spec/model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,74 @@ def self.model_name
end
end

describe "#dup" do
let(:nested_class) do
Class.new do
include AttrJson::Model

attr_json :str, :string
end
end
let(:klass) do
nested_class_type = nested_class.to_type
Class.new do
include AttrJson::Model

attr_json :str_array, :string, array: true
attr_json :str, :string
attr_json :nested, nested_class_type, default: {}
end
end

it "dups all nested data" do
obj = klass.new(str: "one", str_array: ["a", "b"], nested: nested_class.new(str: "two"))
obj_dup = obj.dup

expect(obj_dup.str).not_to equal(obj.str)
expect(obj_dup.str_array).not_to equal(obj.str_array)
expect(obj_dup.str_array.first).not_to equal(obj.str_array.first)

expect(obj_dup.nested).not_to equal(obj.nested)
expect(obj_dup.nested.str).not_to equal(obj.nested.str)
end
end

describe "#freeze" do
let(:nested_class) do
Class.new do
include AttrJson::Model

attr_json :str, :string
end
end
let(:klass) do
nested_class_type = nested_class.to_type
Class.new do
include AttrJson::Model

attr_json :str_array, :string, array: true
attr_json :str, :string
attr_json :nested, nested_class_type, default: {}
end
end

it "makes it not possible to set attributes" do
obj = klass.new(str: "one", str_array: ["a", "b"], nested: nested_class.new(str: "two"))

obj.freeze

# ruby previous to 2.5 didn't have FrozenError
expected_error = defined?(FrozenError) ? FrozenError : RuntimeError

expect {
obj.str = "foo"
}.to raise_error(expected_error)

# note it's not actually deep-frozen, you can mutate attributes including
# arrays. deep freeze could be a different feature.
end
end

describe "unknown keys" do
let(:klass) do
Class.new do
Expand Down Expand Up @@ -204,9 +272,24 @@ def self.model_name ; ActiveModel::Name.new(self, nil, "Klass") ; end
end
end

it "available" do
it "available in registry" do
expect(klass.attr_json_registry.attribute_names).to match([:str_one, :int_one])
end

it "available via .attribute_types" do
expect(klass.attribute_types).to eq({
"str_one" => ActiveModel::Type.lookup(:string),
"int_one" => ActiveModel::Type.lookup(:integer),
})
end

it "available via .attribute_names" do
expect(klass.attribute_names).to match([:str_one, :int_one])
end

it "available via #attribute_names" do
expect(klass.new.attribute_names).to match([:str_one, :int_one])
end
end

describe "#==" do
Expand Down

0 comments on commit eb4c4b4

Please sign in to comment.