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

Add support for nested and object attribute types #26

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions lib/rom/elasticsearch/constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module ROM
module Elasticsearch
InvalidAttributeError = Class.new(StandardError)
end
end
2 changes: 2 additions & 0 deletions lib/rom/elasticsearch/relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require "rom/elasticsearch/relation/loaded"
require "rom/elasticsearch/types"
require "rom/elasticsearch/schema"
require "rom/elasticsearch/schema/dsl"
require "rom/elasticsearch/attribute"

module ROM
Expand Down Expand Up @@ -113,6 +114,7 @@ class Relation < ROM::Relation
# end
defines :multi_index_types

schema_dsl Elasticsearch::Schema::DSL
schema_class Elasticsearch::Schema
schema_attr_class Elasticsearch::Attribute

Expand Down
46 changes: 46 additions & 0 deletions lib/rom/elasticsearch/schema/dsl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require "rom/schema/dsl"
require "dry/types"

module ROM
module Elasticsearch
class Schema
class DSL < ROM::Schema::DSL
# Defines a relation attribute with its type and options.
#
# Optionally, accepts a block to define nested attributes
# for Elasticsearch indices.
#
# @see Relation.schema
#
# @api public
def attribute(name, type_or_options, options = EMPTY_HASH, &block)
unless block.nil?
unless type_or_options.is_a?(::Dry::Types::Hash)
raise ROM::Elasticsearch::InvalidAttributeError,
"You can only specify an attribute block on an object or nested field! " \
"Attribute #{name} is a #{type_or_options.name}"
end

nested_schema = self.class.new(
relation,
schema_class: schema_class,
attr_class: attr_class,
inferrer: inferrer,
&block
).call

type_or_options = type_or_options.meta(properties: nested_schema.to_properties)
end

super(
name,
type_or_options,
options
)
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/rom/elasticsearch/types.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "rom/types"
require "rom/elasticsearch/schema"

module ROM
module Elasticsearch
Expand Down Expand Up @@ -30,6 +31,24 @@ def self.Keyword(meta = {})
def self.Text(meta = {})
String.meta(type: "text", **meta)
end

# Define a nested attribute type
#
# @return [Dry::Types::Type]
#
# @api public
def self.Nested(meta = {})
Hash.meta(type: "nested", **meta)
end

# Define an object attribute type
#
# @return [Dry::Types::Type]
#
# @api public
def self.Object(meta = {})
Hash.meta(properties: {}, **meta)
end
end
end
end
55 changes: 44 additions & 11 deletions spec/integration/rom/elasticsearch/relation/schema_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,56 @@

include_context "setup"

before do
conf.relation(:users) do
schema(:users) do
attribute :id, ROM::Elasticsearch::Types::ID
attribute :name, ROM::Elasticsearch::Types.Text, read: ROM::Types.Constructor(Symbol, &:to_sym)
context "read/write types" do
before do
conf.relation(:users) do
schema(:users) do
attribute :id, ROM::Elasticsearch::Types::ID
attribute :name, ROM::Elasticsearch::Types.Text, read: ROM::Types.Constructor(Symbol, &:to_sym)
end
end
end

it "defines read/write types" do
relation.create_index

relation.command(:create).call(id: 1, name: "Jane")

user = relation.get(1).one

expect(user[:id]).to be(1)
expect(user[:name]).to be(:Jane)
end
end

it "defines read/write types" do
relation.create_index
context "nested fields" do
before do
conf.relation(:users) do
schema(:users) do
attribute :id, ROM::Elasticsearch::Types::ID
attribute :name, ROM::Elasticsearch::Types.Text
attribute :profile, ROM::Elasticsearch::Types.Nested do
attribute :email, ROM::Elasticsearch::Types.Text
end
end
end
end

relation.command(:create).call(id: 1, name: "Jane")
it "persists and reads nested types" do
relation.create_index

user = relation.get(1).one
relation.command(:create).call(
id: 1,
name: "Jane",
profile: {
email: "[email protected]"
}
)

expect(user[:id]).to be(1)
expect(user[:name]).to be(:Jane)
user = relation.get(1).one

expect(user[:id]).to be(1)
expect(user[:profile]).to eql({"email" => "[email protected]"})
end
end
end
4 changes: 2 additions & 2 deletions spec/shared/unit/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
before do
conf.relation(:users) do
schema(:users) do
attribute :id, ROM::Types::Integer
attribute :name, ROM::Types::String
attribute :id, ROM::Elasticsearch::Types::ID
attribute :name, ROM::Elasticsearch::Types.Text

primary_key :id
end
Expand Down
61 changes: 61 additions & 0 deletions spec/unit/rom/elasticsearch/relation/create_index_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,66 @@
})
end
end

context "with a nested attribute type" do
before do
conf.relation(:users) do
schema do
attribute :id, ROM::Elasticsearch::Types::ID
attribute :name, ROM::Elasticsearch::Types.Keyword
attribute :profile, ROM::Elasticsearch::Types.Nested do
attribute :email, ROM::Elasticsearch::Types.Text
end
end
end
end

it "creates an index" do
relation.create_index

expect(gateway.index?(:users)).to be(true)

expect(relation.dataset.mappings)
.to eql("properties" => {
"name" => {"type" => "keyword"},
"profile" => {
"type" => "nested",
"properties" => {
"email" => {"type" => "text"}
}
}
})
end
end

context "with an object attribute type" do
before do
conf.relation(:users) do
schema do
attribute :id, ROM::Elasticsearch::Types::ID
attribute :name, ROM::Elasticsearch::Types.Keyword
attribute :profile, ROM::Elasticsearch::Types.Object do
attribute :email, ROM::Elasticsearch::Types.Text
end
end
end
end

it "creates an index" do
relation.create_index

expect(gateway.index?(:users)).to be(true)

expect(relation.dataset.mappings)
.to eql("properties" => {
"name" => {"type" => "keyword"},
"profile" => {
"properties" => {
"email" => {"type" => "text"}
}
}
})
end
end
end
end