Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎁 WIP Flexible metadata for Valkyrie Objects #6830

Draft
wants to merge 43 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c676801
initial dynamic loading work
orangewolf Jun 6, 2024
8ca106a
typo
orangewolf Jun 6, 2024
3d32fa7
do not load any metadata schemas if HYRAX_FLEXIBLE is true
orangewolf Jun 6, 2024
01b8ed1
dymanic loading of the model with m3 profiles
orangewolf Jun 6, 2024
7e06f18
refactor schema loader to reduce code duplication
orangewolf Jun 6, 2024
382c6d9
fix bugs, add db seed
orangewolf Jun 10, 2024
6f9935b
Add file_set_metadata properties to m3_profile
kirkkwang Jun 10, 2024
6209db7
:white_check_mark: test that Hyrax::Flexibility module is included
ShanaLMoore Jun 11, 2024
77ad3db
Merge branch 'double_combo' into flexible_metadata
ShanaLMoore Jun 11, 2024
c7ccccc
Update dassie's m3_profile.yaml
kirkkwang Jun 11, 2024
eb5382c
:white_check_mark: [i19] add specs for Hyrax::FlexibleSchema
ShanaLMoore Jun 11, 2024
797a48d
:lipstick: rubocop fixes
ShanaLMoore Jun 11, 2024
0ddbab7
Add basic_metadata properties to dassie m3_profile
kirkkwang Jun 11, 2024
42fca41
Add collection_resource properties to m3_profile
kirkkwang Jun 11, 2024
f186c18
Add monograph.yaml properties to m3_profile.yaml
kirkkwang Jun 12, 2024
bcaf5ab
Copy m3_profile to Koppie
kirkkwang Jun 12, 2024
6542b0d
dynamic indexers
orangewolf Jun 12, 2024
9115944
additional indexer calls
orangewolf Jun 13, 2024
629bb68
seperate m3 profiles from other metadata
orangewolf Jun 13, 2024
39dfc2d
Update Koppie m3_profile and add ENV guards
kirkkwang Jun 13, 2024
9f93a7f
Add migration and schema to Koppie
kirkkwang Jun 13, 2024
bf5bbc4
Introduce flexible? configuration
kirkkwang Jun 14, 2024
0980228
Introduce valkyrie_transition? config option
kirkkwang Jun 14, 2024
1a32864
Add Wings(Hyrax::Resource) to dassie m3
kirkkwang Jun 14, 2024
44b75f9
Update specs
kirkkwang Jun 17, 2024
6ceff73
Merge in double_combo
kirkkwang Jun 17, 2024
7d87100
Add back nil guard in SolrDocumentBehavior
kirkkwang Jun 17, 2024
282709e
Move sample_attribute.yaml to fixtures
kirkkwang Jun 18, 2024
4f06880
Add some tests for Hyrax::Flexibility
kirkkwang Jun 18, 2024
4275dbc
Revert resource_spec and remove sample_attribute
kirkkwang Jun 19, 2024
258a2e7
Fix m3_schema_loader_spec and format inline YAML
kirkkwang Jun 19, 2024
b06e6dd
Merge branch 'double_combo' into flexible_metadata
kirkkwang Jun 20, 2024
77c665c
make sure attributes defined in code are kept too
orangewolf Jun 24, 2024
2fb4906
smarter schema reservation and fix some new indexers
orangewolf Jun 25, 2024
12143a7
Merge branch 'main' into flexible_metadata
orangewolf Jun 25, 2024
6bd4cac
Update flexible_schema.rb
orangewolf Jun 25, 2024
35bf9a5
Merge branch 'main' of https://github.com/samvera/hyrax into flexible…
orangewolf Jun 25, 2024
94a08d7
Merge branch 'flexible_metadata' of https://github.com/samvera/hyrax …
orangewolf Jun 25, 2024
e455106
sigh
orangewolf Jun 25, 2024
e4c6ff0
Merge branch 'main' into flexible_metadata
kirkkwang Jun 25, 2024
c052bbb
Merge branch 'main' into flexible_metadata
orangewolf Jun 26, 2024
857f579
Load the indexer module for flexible metadata
laritakr Jun 27, 2024
7f8525d
Merge branch 'main' into flexible_metadata
kirkkwang Jun 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dassie/.env
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ HYRAX_ANALYTICS=false
HYRAX_ANALYTICS_PROVIDER=google
HYRAX_DERIVATIVES_PATH=/app/samvera/hyrax-webapp/derivatives/
HYRAX_ENGINE_PATH=/app/samvera/hyrax-engine
HYRAX_FLEXIBLE=true
HYRAX_UPLOAD_PATH=/app/samvera/hyrax-webapp/uploads/
HYRAX_VALKYRIE=true
IN_DOCKER=true
Expand Down
4 changes: 2 additions & 2 deletions .dassie/app/models/collection_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class CollectionResource < Hyrax::PcdmCollection
# * add Valkyrie attributes to this class
# * update form and indexer to process the attributes
#
include Hyrax::Schema(:basic_metadata)
include Hyrax::Schema(:collection_resource)
include Hyrax::Schema(:basic_metadata) unless ENV.fetch('HYRAX_FLEXIBLE', false)
include Hyrax::Schema(:collection_resource) unless ENV.fetch('HYRAX_FLEXIBLE', false)

Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Collection)
end
4 changes: 2 additions & 2 deletions .dassie/app/models/generic_work_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# Generated via
# `rails generate hyrax:work_resource GenericWorkResource`
class GenericWorkResource < Hyrax::Work
include Hyrax::Schema(:basic_metadata)
include Hyrax::Schema(:generic_work_resource)
include Hyrax::Schema(:basic_metadata) unless ENV.fetch('HYRAX_FLEXIBLE', false)
include Hyrax::Schema(:generic_work_resource) unless ENV.fetch('HYRAX_FLEXIBLE', false)

Hyrax::ValkyrieLazyMigration.migrating(self, from: GenericWork)
end
4 changes: 2 additions & 2 deletions .dassie/app/models/monograph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
# Generated via
# `rails generate hyrax:work_resource Monograph`
class Monograph < Hyrax::Work
include Hyrax::Schema(:basic_metadata)
include Hyrax::Schema(:monograph)
include Hyrax::Schema(:basic_metadata) unless ENV.fetch('HYRAX_FLEXIBLE', false)
include Hyrax::Schema(:monograph) unless ENV.fetch('HYRAX_FLEXIBLE', false)
end
10 changes: 10 additions & 0 deletions .dassie/db/migrate/20240606205215_create_hyrax_flexible_schemas.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateHyraxFlexibleSchemas < ActiveRecord::Migration[6.1]
def change
create_table :hyrax_flexible_schemas do |t|
t.string :version, index: { unique: true }
t.text :profile

t.timestamps
end
end
end
9 changes: 8 additions & 1 deletion .dassie/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2024_05_06_070809) do
ActiveRecord::Schema.define(version: 2024_06_06_205215) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -172,6 +172,13 @@
t.datetime "updated_at", null: false
end

create_table "hyrax_flexible_schemas", force: :cascade do |t|
t.string "version"
t.text "profile"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end

create_table "job_io_wrappers", force: :cascade do |t|
t.bigint "user_id"
t.bigint "uploaded_file_id"
Expand Down
84 changes: 84 additions & 0 deletions app/models/concerns/hyrax/flexibility.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module Hyrax
module Flexibility
extend ActiveSupport::Concern
included do
attribute :schema_version, Valkyrie::Types::String
end

class_methods do
## Override dry-struct 1.6.0 to enable redefining schemas on the fly
def attributes(new_schema)
keys = new_schema.keys.map { |k| k.to_s.chomp("?").to_sym }
schema Hyrax::Resource.schema.schema(new_schema)

define_accessors(keys)

@attribute_names = nil

direct_descendants = descendants&.select { |d| d.superclass == self }
direct_descendants&.each do |d|
inherited_attrs = new_schema.reject { |k, _| d.has_attribute?(k.to_s.chomp("?").to_sym) }
d.attributes(inherited_attrs)
end

new_schema.each_key do |key|
key = key.to_s.chomp('?')
next if instance_methods.include?("#{key}=".to_sym)

class_eval(<<-RUBY)
def #{key}=(value)
set_value("#{key}".to_sym, value)
end
RUBY
end

self
end

## Override dry-struct 1.6.0 to filter attributes after schema reload happens
def new(attributes = default_attributes, safe = false, &block) # rubocop:disable Style/OptionalBooleanParameter
if attributes.is_a?(Struct)
if equal?(attributes.class)
attributes
else
# This implicit coercion is arguable but makes sense overall
# in cases there you pass child struct to the base struct constructor
# User.new(super_user)
#
# We may deprecate this behavior in future forcing people to be explicit
new(attributes.to_h, safe, &block)
end
else
load(attributes, safe)
end
rescue Dry::Types::CoercionError => e
raise Dry::Error, "[#{self}.new] #{e}", e.backtrace
end

## Read the schema from the database and load the correct schemas for the instance in to the class
def load(attributes, safe = false)
attributes[:schema_version] ||= Hyrax::FlexibleSchema.order('created_at DESC').pick(:version)
struct = allocate
schema_version = attributes[:schema_version]
struct.singleton_class.attributes(Hyrax::Schema(self, schema_version:).attributes)
clean_attributes = safe ? struct.singleton_class.schema.call_safe(attributes) { |output = attributes| return yield output } : struct.singleton_class.schema.call_unsafe(attributes)
struct.__send__(:initialize, clean_attributes)
struct
end
end

# Override set_value from valkyrie 3.1.1 to enable dynamic schema loading
def set_value(key, value)
@attributes[key.to_sym] = self.singleton_class.schema.key(key.to_sym).type.call(value)
end

# Override inspect from dry-struct 1.6.0 to enable dynamic schema loading
def inspect
klass = self.singleton_class
attrs = klass.attribute_names.map { |key| " #{key}=#{@attributes[key].inspect}" }.join
"#<#{klass.name || klass.inspect}#{attrs}>"
end
end
end
2 changes: 1 addition & 1 deletion app/models/hyrax/administrative_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ module Hyrax
# @see Valkyrie query adapter's #find_inverse_references_by
#
class AdministrativeSet < Hyrax::Resource
include Hyrax::Schema(:core_metadata)
include Hyrax::Schema(:core_metadata) unless ENV.fetch('HYRAX_FLEXIBLE', false)

attribute :alternative_title, Valkyrie::Types::Set.of(Valkyrie::Types::String)
attribute :creator, Valkyrie::Types::Set.of(Valkyrie::Types::String)
Expand Down
4 changes: 2 additions & 2 deletions app/models/hyrax/file_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ module Hyrax
# @see Hyrax::CustomQueries::Navigators::ParentWorkNavigator#find_parent_work
# @see https://wiki.duraspace.org/display/samvera/Hydra%3A%3AWorks+Shared+Modeling
class FileSet < Hyrax::Resource
include Hyrax::Schema(:core_metadata)
include Hyrax::Schema(:file_set_metadata)
include Hyrax::Schema(:core_metadata) unless ENV.fetch('HYRAX_FLEXIBLE', false)
include Hyrax::Schema(:file_set_metadata) unless ENV.fetch('HYRAX_FLEXIBLE', false)

def self.model_name(name_class: Hyrax::Name)
@_model_name ||= name_class.new(self, nil, 'FileSet')
Expand Down
40 changes: 40 additions & 0 deletions app/models/hyrax/flexible_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class Hyrax::FlexibleSchema < ApplicationRecord
serialize :profile, coder: YAML

def attributes_for(class_name)
class_names[class_name]
end

def class_names
return @class_names if @class_names
@class_names = {}
profile['classes'].keys.each do |class_name|
@class_names[class_name] = {}
end
profile['properties'].each do |key, values|
values['available_on']['class'].each do |property_class|
# map some m3 items to what Hyrax expects
values = values_map(values)
@class_names[property_class][key] = values
end
end
@class_names
end

def values_map(values)
orangewolf marked this conversation as resolved.
Show resolved Hide resolved
values['type'] = lookup_type(value['range'])
values['predicate'] = value['property_uri']
values['index_keys'] = values['indexing']
values['multiple'] = values['multi_value']
values
end

def lookup_type(range)
case range
when "http://www.w3.org/2001/XMLSchema#dateTime"
'date_time'
else
range.split('#').last.underscore
end
end
end
2 changes: 1 addition & 1 deletion app/models/hyrax/pcdm_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ module Hyrax
# @see Hyrax::CustomQueries::Navigators::CollectionMembers#find_members_of
#
class PcdmCollection < Hyrax::Resource
include Hyrax::Schema(:core_metadata)
include Hyrax::Schema(:core_metadata) unless ENV.fetch('HYRAX_FLEXIBLE', false)

attribute :collection_type_gid, Valkyrie::Types::String
attribute :member_ids, Valkyrie::Types::Array.of(Valkyrie::Types::ID).meta(ordered: true)
Expand Down
1 change: 1 addition & 0 deletions app/models/hyrax/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module Hyrax
# implementations).
#
class Resource < Valkyrie::Resource
include Hyrax::Flexibility if ENV.fetch('HYRAX_FLEXIBLE', false)
include Hyrax::Naming
include Hyrax::WithEvents

Expand Down
2 changes: 1 addition & 1 deletion app/models/hyrax/work.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ module Hyrax
# @see https://wiki.lyrasis.org/display/samvera/Hydra::Works+Shared+Modeling
# for a historical perspective.
class Work < Hyrax::Resource
include Hyrax::Schema(:core_metadata)
include Hyrax::Schema(:core_metadata) unless ENV.fetch('HYRAX_FLEXIBLE', false)

attribute :admin_set_id, Valkyrie::Types::ID
attribute :member_ids, Valkyrie::Types::Array.of(Valkyrie::Types::ID).meta(ordered: true)
Expand Down
22 changes: 22 additions & 0 deletions app/services/hyrax/m3_schema_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Hyrax
##
# @api private
#
# Read m3 profiles from the database
#
# @see config/metadata/m3_profile.yaml for an example configuration
class M3SchemaLoader < Hyrax::SchemaLoader
private

##
# @param [#to_s] schema_name
# @return [Enumerable<AttributeDefinition]
def definitions(schema_name, version)
Hyrax::FlexibleSchema.find_by(version: version).attributes_for(schema_name).map do |name, config|
AttributeDefinition.new(name, config)
end
end
end
end
130 changes: 130 additions & 0 deletions app/services/hyrax/schema_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# frozen_string_literal: true
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code was moved as is from simple_schema_loader


module Hyrax
##
# @api private
#
# This is a simple yaml config-driven schema loader
#
# @see config/metadata/basic_metadata.yaml for an example configuration
class SchemaLoader
##
# @param [Symbol] schema
#
# @return [Hash<Symbol, Dry::Types::Type>] a map from attribute names to
# types
def attributes_for(schema:, version: 1)
definitions(schema).each_with_object({}) do |definition, hash|
hash[definition.name] = definition.type.meta(definition.config)
end
end

##
# @param [Symbol] schema
#
# @return [Hash{Symbol => Hash{Symbol => Object}}]
def form_definitions_for(schema:, version: 1)
definitions(schema).each_with_object({}) do |definition, hash|
next if definition.form_options.empty?

hash[definition.name] = definition.form_options
end
end

##
# @param [Symbol] schema
#
# @return [{Symbol => Symbol}] a map from index keys to attribute names
def index_rules_for(schema:, version: 1)
definitions(schema).each_with_object({}) do |definition, hash|
definition.index_keys.each do |key|
hash[key] = definition.name
end
end
end

##
# @api private
class AttributeDefinition
##
# @!attr_reader :config
# @return [Hash<String, Object>]
# @!attr_reader :name
# @return [#to_sym]
attr_reader :config, :name

##
# @param [#to_sym] name
# @param [Hash<String, Object>] config
def initialize(name, config)
@config = config
@name = name.to_sym
end

##
# @return [Hash{Symbol => Object}]
def form_options
config.fetch('form', {}).symbolize_keys
end

##
# @return [Enumerable<Symbol>]
def index_keys
config.fetch('index_keys', []).map(&:to_sym)
end

##
# @return [Dry::Types::Type]
def type
collection_type = if config['multiple']
Valkyrie::Types::Array.constructor { |v| Array(v).select(&:present?) }
else
Identity
end
collection_type.of(type_for(config['type']))
end

##
# @api private
#
# This class acts as a Valkyrie/Dry::Types collection with typed members,
# but instead of wrapping the given type with itself as the collection type
# (as in `Valkyrie::Types::Array.of(MyType)`), it returns the given type.
#
# @example
# Identity.of(Valkyrie::Types::String) # => Valkyrie::Types::String
#
class Identity
##
# @param [Dry::Types::Type]
# @return [Dry::Types::Type] the type passed in
def self.of(type)
type
end
end

private

##
# Maps a configuration string value to a `Valkyrie::Type`.
#
# @param [String]
# @return [Dry::Types::Type]
def type_for(type)
case type
when 'id'
Valkyrie::Types::ID
when 'uri'
Valkyrie::Types::URI
when 'date_time'
Valkyrie::Types::DateTime
else
"Valkyrie::Types::#{type.capitalize}".constantize
end
end
end

class UndefinedSchemaError < ArgumentError; end

end
end
Loading