diff --git a/lib/rom/elasticsearch/constants.rb b/lib/rom/elasticsearch/constants.rb new file mode 100644 index 0000000..a40b089 --- /dev/null +++ b/lib/rom/elasticsearch/constants.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ROM + module Elasticsearch + InvalidAttributeError = Class.new(StandardError) + end +end diff --git a/lib/rom/elasticsearch/relation.rb b/lib/rom/elasticsearch/relation.rb index cf6bcac..be1c951 100644 --- a/lib/rom/elasticsearch/relation.rb +++ b/lib/rom/elasticsearch/relation.rb @@ -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 @@ -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 diff --git a/lib/rom/elasticsearch/schema/dsl.rb b/lib/rom/elasticsearch/schema/dsl.rb new file mode 100644 index 0000000..b6c2270 --- /dev/null +++ b/lib/rom/elasticsearch/schema/dsl.rb @@ -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 diff --git a/lib/rom/elasticsearch/types.rb b/lib/rom/elasticsearch/types.rb index f78b0f6..19f7a35 100644 --- a/lib/rom/elasticsearch/types.rb +++ b/lib/rom/elasticsearch/types.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "rom/types" +require "rom/elasticsearch/schema" module ROM module Elasticsearch @@ -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 diff --git a/spec/integration/rom/elasticsearch/relation/schema_spec.rb b/spec/integration/rom/elasticsearch/relation/schema_spec.rb index 35de61f..91fd10c 100644 --- a/spec/integration/rom/elasticsearch/relation/schema_spec.rb +++ b/spec/integration/rom/elasticsearch/relation/schema_spec.rb @@ -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: "jane@sample.com" + } + ) - 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" => "jane@sample.com"}) + end end end diff --git a/spec/shared/unit/users.rb b/spec/shared/unit/users.rb index 47d4e07..4533148 100644 --- a/spec/shared/unit/users.rb +++ b/spec/shared/unit/users.rb @@ -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 diff --git a/spec/unit/rom/elasticsearch/relation/create_index_spec.rb b/spec/unit/rom/elasticsearch/relation/create_index_spec.rb index 1aed537..582b580 100644 --- a/spec/unit/rom/elasticsearch/relation/create_index_spec.rb +++ b/spec/unit/rom/elasticsearch/relation/create_index_spec.rb @@ -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