Skip to content

Commit

Permalink
Track persistence independent of presence of an identifier
Browse files Browse the repository at this point in the history
Fixes a loop when adding `ActiveModel::Conversion` because conversion
defines `to_param` in terms of `persisted?`, which Valkyrie previously
defined in terms of `to_param`

Fixes #338
  • Loading branch information
jcoyne committed Dec 20, 2017
1 parent cccdf66 commit 6203dd1
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 79 deletions.
4 changes: 3 additions & 1 deletion lib/valkyrie/persistence/memory/persister.rb
Expand Up @@ -41,7 +41,9 @@ def wipe!
private

def generate_id(resource)
resource.new(id: SecureRandom.uuid, created_at: Time.current)
resource.new(id: SecureRandom.uuid,
created_at: Time.current,
new_record: false)
end

def normalize_dates!(resource)
Expand Down
164 changes: 90 additions & 74 deletions lib/valkyrie/persistence/postgres/orm_converter.rb
Expand Up @@ -6,113 +6,129 @@ def initialize(orm_object)
@orm_object = orm_object
end

# Create a new instance of the class described in attributes[:internal_resource]
# and send it all the attributes that @orm_object has
def convert!
@resource ||= attributes[:internal_resource].constantize.new(attributes)
@resource ||= resource
end

# @return [Hash] Valkyrie-style hash of attributes.
def attributes
@attributes ||= orm_object.attributes.merge(rdf_metadata).symbolize_keys
end
private

def rdf_metadata
@rdf_metadata ||= RDFMetadata.new(orm_object.metadata).result
end
def resource
resource_klass.new(attributes.merge(new_record: false))
end

class RDFMetadata
attr_reader :metadata
def initialize(metadata)
@metadata = metadata
def resource_klass
internal_resource.constantize
end

def result
Hash[
metadata.map do |key, value|
[key, PostgresValue.for(value).result]
end
]
def internal_resource
attributes[:internal_resource]
end

class PostgresValue < ::Valkyrie::ValueMapper
# @return [Hash] Valkyrie-style hash of attributes.
def attributes
@attributes ||= orm_object.attributes.merge(rdf_metadata).symbolize_keys
end
# Converts {RDF::Literal} typed-literals from JSON-LD stored into an
# {RDF::Literal}
class HashValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.is_a?(Hash) && value["@value"]
end

def result
RDF::Literal.new(value["@value"], language: value["@language"])
end
def rdf_metadata
@rdf_metadata ||= RDFMetadata.new(orm_object.metadata).result
end

# Converts stored IDs into {Valkyrie::ID}s
class IDValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.is_a?(Hash) && value["id"] && !value["internal_resource"]
class RDFMetadata
attr_reader :metadata
def initialize(metadata)
@metadata = metadata
end

def result
Valkyrie::ID.new(value["id"])
Hash[
metadata.map do |key, value|
[key, PostgresValue.for(value).result]
end
]
end
end

# Converts stored URIs into {RDF::URI}s
class URIValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.is_a?(Hash) && value["@id"]
class PostgresValue < ::Valkyrie::ValueMapper
end
# Converts {RDF::Literal} typed-literals from JSON-LD stored into an
# {RDF::Literal}
class HashValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.is_a?(Hash) && value["@value"]
end

def result
::RDF::URI.new(value["@id"])
def result
RDF::Literal.new(value["@value"], language: value["@language"])
end
end
end

# Converts nested records into {Valkyrie::Resource}s
class NestedRecord < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.is_a?(Hash) && value.keys.length > 1
end
# Converts stored IDs into {Valkyrie::ID}s
class IDValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.is_a?(Hash) && value["id"] && !value["internal_resource"]
end

def result
RDFMetadata.new(value).result.symbolize_keys
def result
Valkyrie::ID.new(value["id"])
end
end
end

class DateValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
return false unless value.is_a?(String)
return false unless value[4] == "-"
year = value.to_s[0..3]
return false unless year.length == 4 && year.to_i.to_s == year
DateTime.iso8601(value)
rescue
false
# Converts stored URIs into {RDF::URI}s
class URIValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.is_a?(Hash) && value["@id"]
end

def result
::RDF::URI.new(value["@id"])
end
end

def result
DateTime.iso8601(value).utc
# Converts nested records into {Valkyrie::Resource}s
class NestedRecord < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.is_a?(Hash) && value.keys.length > 1
end

def result
RDFMetadata.new(value).result.symbolize_keys
end
end
end

class EnumeratorValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.respond_to?(:each)
class DateValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
return false unless value.is_a?(String)
return false unless value[4] == "-"
year = value.to_s[0..3]
return false unless year.length == 4 && year.to_i.to_s == year
DateTime.iso8601(value)
rescue
false
end

def result
DateTime.iso8601(value).utc
end
end

def result
value.map do |value|
calling_mapper.for(value).result
class EnumeratorValue < ::Valkyrie::ValueMapper
PostgresValue.register(self)
def self.handles?(value)
value.respond_to?(:each)
end

def result
value.map do |value|
calling_mapper.for(value).result
end
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/valkyrie/persistence/solr/orm_converter.rb
Expand Up @@ -13,7 +13,7 @@ def convert!
end

def resource
resource_klass.new(attributes.symbolize_keys)
resource_klass.new(attributes.symbolize_keys.merge(new_record: false))
end

def resource_klass
Expand Down
3 changes: 2 additions & 1 deletion lib/valkyrie/resource.rb
Expand Up @@ -20,6 +20,7 @@ def self.inherited(subclass)
subclass.attribute :internal_resource, Valkyrie::Types::Any.default(subclass.to_s)
subclass.attribute :created_at, Valkyrie::Types::DateTime.optional
subclass.attribute :updated_at, Valkyrie::Types::DateTime.optional
subclass.attribute :new_record, Types::Bool.default(true)
end

# @return [Array<Symbol>] Array of fields defined for this class.
Expand Down Expand Up @@ -75,7 +76,7 @@ def column_for_attribute(name)

# @return [Boolean]
def persisted?
to_param.present?
@new_record == false
end

def to_key
Expand Down
5 changes: 4 additions & 1 deletion lib/valkyrie/specs/shared_specs/persister.rb
Expand Up @@ -24,7 +24,10 @@ class CustomResource < Valkyrie::Resource
it { is_expected.to respond_to(:delete).with_keywords(:resource) }

it "can save a resource" do
expect(persister.save(resource: resource).id).not_to be_blank
expect(resource).not_to be_persisted
saved = persister.save(resource: resource)
expect(saved).to be_persisted
expect(saved.id).not_to be_blank
end

it "can save multiple resources at once" do
Expand Down
4 changes: 3 additions & 1 deletion lib/valkyrie/specs/shared_specs/queries.rb
Expand Up @@ -55,7 +55,9 @@ class SecondResource < Valkyrie::Resource
it "returns a resource by id" do
resource = persister.save(resource: resource_class.new)

expect(query_service.find_by(id: resource.id).id).to eq resource.id
found = query_service.find_by(id: resource.id)
expect(found.id).to eq resource.id
expect(found).to be_persisted
end

it "returns a Valkyrie::Persistence::ObjectNotFoundError for a non-found ID" do
Expand Down

0 comments on commit 6203dd1

Please sign in to comment.