class LinkedRails::Middleware::LinkedDataParams

Public Class Methods

new(app) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 6
def initialize(app)
  @app = app
end

Public Instance Methods

call(env) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 10
def call(env)
  req = Rack::Request.new(env)
  params_from_query(req)
  params_from_graph(req)

  @app.call(env)
end

Private Instance Methods

add_param(hash, key, value) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 20
def add_param(hash, key, value) # rubocop:disable Metrics/MethodLength
  case hash[key]
  when nil
    hash[key] = value
  when Hash
    hash[key].merge!(value)
  when Array
    hash[key].append(value)
  else
    hash[key] = [hash[key], value]
  end
  hash
end
add_param_from_query(data, target_class, key, value) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 59
def add_param_from_query(data, target_class, key, value)
  data[target_class.predicate_mapping[RDF::URI(key)].key] = value
end
associated_class_from_params(reflection, graph, subject) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 34
def associated_class_from_params(reflection, graph, subject)
  return reflection.klass unless reflection.polymorphic?

  query = graph.query(subject: subject, predicate: Vocab.rdfv[:type])
  if query.empty?
    raise("No type given for '#{subject}' referenced by polymorphic association '#{reflection.name}'")
  end

  iri_to_class(query.first.object)
end
attributes_from_iri(object, association, reflection) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 173
def attributes_from_iri(object, association, reflection)
  if reflection.options[:through]
    key = reflection.has_one? ? "#{association}_id" : "#{association.to_s.singularize}_ids"
  elsif reflection.belongs_to?
    key = reflection.foreign_key
  end
  return unless key

  resource = LinkedRails.iri_mapper.resource_from_iri(object, nil)
  value = resource&.send(reflection.association_primary_key)

  [key, value] if value
end
blob_attribute(base_params, value) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 45
def blob_attribute(base_params, value)
  base_params["<#{value}>"] if value.starts_with?(Vocab.ll['blobs/'])
end
convert_query_params(request, target_class) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 49
def convert_query_params(request, target_class)
  keys = target_class.predicate_mapping.keys.map(&:to_s)
  class_key = target_class.to_s.underscore
  data = request.params[class_key] || {}
  request.params.slice(*keys.map(&:to_s)).each do |key, value|
    add_param_from_query(data, target_class, key, value)
  end
  request.update_param(class_key, data) if data.present?
end
enum_attribute(klass, key, value) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 63
def enum_attribute(klass, key, value)
  opts = RDF::Serializers.serializer_for(klass).try(:enum_options, key)
  return if opts.blank?

  opts.detect { |_k, options| options.iri == value }&.first&.to_s
end
graph_from_request(request) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 70
def graph_from_request(request)
  request_graph = request.delete_param("<#{Vocab.ll[:graph].value}>")
  return if request_graph.blank?

  RDF::Graph.load(
    request_graph[:tempfile].path,
    content_type: request_graph[:type],
    canonicalize: true,
    intern: false
  )
end
iri_to_class(iri) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 82
def iri_to_class(iri)
  iri.to_s.split(LinkedRails.app_vocab.to_s).pop&.classify&.safe_constantize ||
    ApplicationRecord.descendants.detect { |klass| klass.iri == iri }
end
logger() click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 87
def logger
  Rails.logger
end
nested_attributes(base_params, graph, subject, klass, association, collection) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 91
def nested_attributes(base_params, graph, subject, klass, association, collection) # rubocop:disable Metrics/ParameterLists
  nested_resources =
    if graph.query([subject, Vocab.rdfv[:first], nil]).present?
      nested_attributes_from_list(base_params, graph, subject, klass)
    else
      parsed = parse_nested_resource(base_params, graph, subject, klass)
      collection ? {rand(1_000_000_000).to_s => parsed} : parsed
    end
  ["#{association}_attributes", nested_resources]
end
nested_attributes_from_list(base_params, graph, subject, klass) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 102
def nested_attributes_from_list(base_params, graph, subject, klass)
  Hash[
    RDF::List.new(subject: subject, graph: graph)
      .map { |nested| [rand(1_000_000_000).to_s, parse_nested_resource(base_params, graph, nested, klass)] }
  ]
end
params_from_graph(request) click to toggle source

Converts a serialized graph from a multipart request body to a nested attributes hash.

The graph sent to the server should be sent under the `ll:graph` form name. The entrypoint for the graph is the `ll:targetResource` subject, which is assumed to be the resource intended to be targeted by the request (i.e. the resource to be created, updated, or deleted).

@return [Hash] A hash of attributes, empty if no statements were given.

# File lib/linked_rails/middleware/linked_data_params.rb, line 118
def params_from_graph(request)
  graph = graph_from_request(request)

  return unless graph

  request.update_param(:body_graph, graph)
  target_class = target_class_from_path(request)
  return if target_class.blank?

  update_actor_param(request, graph)
  update_target_params(request, graph, target_class)
end
params_from_query(request) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 131
def params_from_query(request)
  target_class = target_class_from_path(request) if request.params.present?
  return unless target_class.try(:predicate_mapping)

  convert_query_params(request, target_class)
end
parse_nested_resource(base_params, graph, subject, klass) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 138
def parse_nested_resource(base_params, graph, subject, klass)
  resource = parse_resource(base_params, graph, subject, klass)
  resource[:id] ||= LinkedRails.iri_mapper.opts_from_iri(subject)[:params][:id] if subject.iri?
  resource
end
parse_resource(base_params, graph, subject, klass) click to toggle source

Recursively parses a resource from graph

# File lib/linked_rails/middleware/linked_data_params.rb, line 145
def parse_resource(base_params, graph, subject, klass)
  graph
    .query([subject])
    .map { |statement| parse_statement(base_params, graph, statement, klass) }
    .compact
    .reduce({}) { |h, (k, v)| add_param(h, k, v) }
end
parse_statement(base_params, graph, statement, klass) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 153
def parse_statement(base_params, graph, statement, klass)
  field = serializer_field(klass, statement.predicate)
  if field.is_a?(FastJsonapi::Attribute)
    parsed_attribute(base_params, klass, field.key, statement.object.value)
  elsif field.is_a?(FastJsonapi::Relationship)
    parsed_association(base_params, graph, statement.object, klass, field.association || field.key)
  end
end
parsed_association(base_params, graph, object, klass, association) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 162
def parsed_association(base_params, graph, object, klass, association)
  reflection = klass.reflect_on_association(association) || raise("#{association} not found for #{klass}")

  if graph.has_subject?(object)
    association_klass = associated_class_from_params(reflection, graph, object)
    nested_attributes(base_params, graph, object, association_klass, association, reflection.collection?)
  elsif object.iri?
    attributes_from_iri(object, association, reflection)
  end
end
parsed_attribute(base_params, klass, key, value) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 187
def parsed_attribute(base_params, klass, key, value)
  [key, blob_attribute(base_params, value) || enum_attribute(klass, key, value) || value]
end
serializer_field(klass, predicate) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 191
def serializer_field(klass, predicate)
  field = klass.try(:predicate_mapping).try(:[], predicate)
  logger.info("#{predicate} not found for #{klass || 'nil'}") if field.blank?
  field
end
target_class_from_path(request) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 197
def target_class_from_path(request) # rubocop:disable Metrics/AbcSize
  opts = LinkedRails.iri_mapper.opts_from_iri(
    request.base_url + request.env['REQUEST_URI'],
    method: request.request_method
  )

  logger.info("No class found for #{request.base_url + request.env['REQUEST_URI']}") unless opts[:class]

  opts[:class]
end
update_actor_param(request, graph) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 208
def update_actor_param(request, graph)
  actor = graph.query([Vocab.ll[:targetResource], Vocab.schema.creator]).first
  return if actor.blank?

  request.update_param(:actor_iri, actor.object)
  graph.delete(actor)
end
update_target_params(request, graph, target_class) click to toggle source
# File lib/linked_rails/middleware/linked_data_params.rb, line 216
def update_target_params(request, graph, target_class)
  key = target_class.to_s.demodulize.underscore
  from_body = parse_resource(request.params, graph, Vocab.ll[:targetResource], target_class)

  request.update_param(key, from_body.merge(request.params[key] || {}))
end