Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion lib/rom/components/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def self.inherited(klass)
option :config, type: Types.Instance(Dry::Configurable::Config)

# @!attribute [r] gateway
# @return [Proc] Optional dataset evaluation block
# @return [Proc] Optional component evaluation block
option :block, type: Types.Interface(:to_proc), optional: true

# @api public
Expand Down
53 changes: 53 additions & 0 deletions lib/rom/components/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require "rom/components/dsl/dataset"
require "rom/components/dsl/schema"
require "rom/components/dsl/relation"
require "rom/components/dsl/view"
require "rom/components/dsl/association"
require "rom/components/dsl/command"
require "rom/components/dsl/mapper"
Expand Down Expand Up @@ -71,6 +72,58 @@ def relation(id, dataset: id, **options, &block)
__dsl__(DSL::Relation, id: id, dataset: dataset, **options, &block)
end

# Define a relation view with a specific schema
#
# This method should only be used in cases where a given adapter doesn't
# support automatic schema projection at run-time.
#
# @overload view(name, schema, &block)
# @example View with the canonical schema
# class Users < ROM::Relation[:sql]
# view(:listing, schema) do
# order(:name)
# end
# end
#
# @example View with a projected schema
# class Users < ROM::Relation[:sql]
# view(:listing, schema.project(:id, :name)) do
# order(:name)
# end
# end
#
# @overload view(name, &block)
# @example View with the canonical schema and arguments
# class Users < ROM::Relation[:sql]
# view(:by_name) do |name|
# where(name: name)
# end
# end
#
# @example View with projected schema and arguments
# class Users < ROM::Relation[:sql]
# view(:by_name) do
# schema { project(:id, :name) }
# relation { |name| where(name: name) }
# end
# end
#
# @example View with a schema extended with foreign attributes
# class Users < ROM::Relation[:sql]
# view(:index) do
# schema { append(relations[:tasks][:title]) }
# relation { |name| where(name: name) }
# end
# end
#
# @return [Symbol] view method name
#
# @api public
def view(id, *args, &block)
__dsl__(DSL::View, id: id, args: args, &block)
id
end

# Define associations for a relation
#
# @example
Expand Down
5 changes: 3 additions & 2 deletions lib/rom/components/dsl/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def call
plugin.enable(self) unless plugin.enabled?
end

instance_eval(&block) if block
# Evaluate block only if it's not a schema defined by Relation.view DSL
instance_eval(&block) if block && !config.view

# Apply plugin defaults
plugins.each do |plugin|
Expand All @@ -59,7 +60,7 @@ def call

configure

components.add(key, config: config)
components.add(key, config: config, block: config.view ? block : nil)
end

# @api private
Expand Down
82 changes: 82 additions & 0 deletions lib/rom/components/dsl/view.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

require_relative "core"

module ROM
module Components
module DSL
# @api private
class View < Core
key :views

# @api private
attr_reader :schema_block

# @api private
attr_reader :relation_block

# @see Components::DSL#view
#
# @api public
def schema(&block)
@schema_block = block
self
end

# @see Components::DSL#view
#
# @api public
def relation(&block)
@relation_block = block
self
end

# @api private
def call
# Nest view under relation ns
config.join!({namespace: relation_id}, :right)

if args.empty? && block.arity.positive?
raise ArgumentError, "schema attribute names must be provided as the second argument"
end

# Capture schema and relation blocks if there are no args
# otherwise assume args is a list of attributes to project
if args.empty? && block
instance_eval(&block)
else
schema { schema.project(*args.first) }
end

provider.schema(
id: config.id,
namespace: relation_id,
relation: relation_id,
view: true,
&schema_block
)

components.add(
key,
config: config,
relation_id: relation_id,
# Default to the block because we assume the schema was set based on args
relation_block: relation_block || block
)
end

private

# @api private
def args
config.args
end

# @api private
def relation_id
provider.config.component.id
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/rom/components/relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class Relation < Core
def build
constant.use(:registry_reader, relations: resolver.relation_ids)

# Define view methods if there are any registered view components for this relation
local_components.views(relation_id: id).each do |view|
view.define(constant)
end

trigger("relations.class.ready", relation: constant, adapter: adapter)

apply_plugins
Expand Down
55 changes: 55 additions & 0 deletions lib/rom/components/view.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require_relative "core"

module ROM
module Components
# @api public
class View < Core
# @!attribute [r] relation_id
# @return [Symbol] Relation runtime identifier
# @api private
option :relation_id

# @!attribute [r] relation_block
# @return [Proc] Block used for view method definition
# @api private
option :relation_block

# @return [ROM::Relation]
#
# @api private
def build
resolver.relations[relation_id].public_send(config.id)
end

# @return [Symbol]
#
# @api private
def define(constant)
_name = config.id
_relation_block = relation_block

if relation_block&.arity&.positive?
constant.class_eval do
auto_curry_guard do
define_method(_name, &_relation_block)

auto_curry(_name) do
schemas[_name].(self)
end
end
end
else
constant.class_eval do
define_method(_name) do
schemas[_name].(instance_eval(&_relation_block))
end
end
end

_name
end
end
end
end
1 change: 1 addition & 0 deletions lib/rom/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module ROM
datasets
schemas
relations
views
associations
mappers
commands
Expand Down
5 changes: 4 additions & 1 deletion lib/rom/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require_relative "components/dataset"
require_relative "components/schema"
require_relative "components/relation"
require_relative "components/view"
require_relative "components/association"
require_relative "components/command"
require_relative "components/mapper"
Expand All @@ -23,7 +24,8 @@
# @api public
module ROM
extend Global
extend self

module_function

# Global component setup
#
Expand Down Expand Up @@ -69,6 +71,7 @@ def plugins(*args, &block)
register :dataset, Components::Dataset
register :schema, Components::Schema
register :relation, Components::Relation
register :view, Components::View
register :association, Components::Association
register :command, Components::Command
register :mapper, Components::Mapper
Expand Down
2 changes: 1 addition & 1 deletion lib/rom/relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ module ROM
#
# @api public
class Relation
extend ROM::Provider(:dataset, :schema, :association, type: :relation)
extend ROM::Provider(:dataset, :schema, :view, :association, type: :relation)
extend Initializer
extend ClassInterface

Expand Down
93 changes: 0 additions & 93 deletions lib/rom/relation/class_interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

require "rom/constants"
require "rom/relation/name"
require "rom/relation/view_dsl"
require "rom/schema"

module ROM
Expand All @@ -35,98 +34,6 @@ def [](adapter)
raise AdapterNotPresentError.new(adapter, :relation)
end

# Define a relation view with a specific schema
#
# This method should only be used in cases where a given adapter doesn't
# support automatic schema projection at run-time.
#
# **It's not needed in rom-sql**
#
# @overload view(name, schema, &block)
# @example View with the canonical schema
# class Users < ROM::Relation[:sql]
# view(:listing, schema) do
# order(:name)
# end
# end
#
# @example View with a projected schema
# class Users < ROM::Relation[:sql]
# view(:listing, schema.project(:id, :name)) do
# order(:name)
# end
# end
#
# @overload view(name, &block)
# @example View with the canonical schema and arguments
# class Users < ROM::Relation[:sql]
# view(:by_name) do |name|
# where(name: name)
# end
# end
#
# @example View with projected schema and arguments
# class Users < ROM::Relation[:sql]
# view(:by_name) do
# schema { project(:id, :name) }
# relation { |name| where(name: name) }
# end
# end
#
# @example View with a schema extended with foreign attributes
# class Users < ROM::Relation[:sql]
# view(:index) do
# schema { append(relations[:tasks][:title]) }
# relation { |name| where(name: name) }
# end
# end
#
# @return [Symbol] view method name
#
# @api public
#
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/PerceivedComplexity
def view(*args, &block)
if args.size == 1 && block.arity.positive?
raise ArgumentError, "schema attribute names must be provided as the second argument"
end

name, schema_block, relation_block =
if args.size == 1
ViewDSL.new(*args, &block).call
else
[*args, block]
end

block =
if args.size == 2
-> _ { schema.project(*args[1]) }
else
schema_block
end

schema(id: name, relation: config.component.id, view: true, &block)

if relation_block.arity.positive?
auto_curry_guard do
define_method(name, &relation_block)

auto_curry(name) do
schemas[name].(self)
end
end
else
define_method(name) do
schemas[name].(instance_exec(&relation_block))
end
end

name
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/PerceivedComplexity

# Dynamically define a method that will forward to the dataset and wrap
# response in the relation itself
#
Expand Down
Loading