From d094df3e96bfb064f3f6ca5f7d6e483b43fc2ff9 Mon Sep 17 00:00:00 2001 From: AMHOL Date: Thu, 25 Jan 2018 11:43:00 +0800 Subject: [PATCH] Add Nazrin::DataAccessor::Struct --- lib/nazrin/data_accessor.rb | 37 +++++--- lib/nazrin/data_accessor/active_record.rb | 10 +- lib/nazrin/data_accessor/mongoid.rb | 14 ++- lib/nazrin/data_accessor/struct.rb | 68 +++++++++++++ lib/nazrin/searchable/config.rb | 2 + nazrin.gemspec | 1 + spec/nazrin/data_accessor/struct_spec.rb | 110 ++++++++++++++++++++++ spec/nazrin/struct/searchable_spec.rb | 75 +++++++++++++++ 8 files changed, 296 insertions(+), 21 deletions(-) create mode 100644 lib/nazrin/data_accessor/struct.rb create mode 100644 spec/nazrin/data_accessor/struct_spec.rb create mode 100644 spec/nazrin/struct/searchable_spec.rb diff --git a/lib/nazrin/data_accessor.rb b/lib/nazrin/data_accessor.rb index 0730b33..97f22e7 100644 --- a/lib/nazrin/data_accessor.rb +++ b/lib/nazrin/data_accessor.rb @@ -26,7 +26,11 @@ def register_accessor(clazz) def accessor_for(clazz) return nil if clazz.name.nil? || clazz.name.empty? - if defined?(::ActiveRecord::Base) && clazz.ancestors.include?(::ActiveRecord::Base) + + if clazz.respond_to?(:nazrin_searchable_config) && clazz.nazrin_searchable_config.domain_name + require 'nazrin/data_accessor/struct' + return Nazrin::DataAccessor::Struct[clazz.nazrin_searchable_config] + elsif defined?(::ActiveRecord::Base) && clazz.ancestors.include?(::ActiveRecord::Base) require 'nazrin/data_accessor/active_record' return Nazrin::DataAccessor::ActiveRecord elsif defined?(::Mongoid::Document) && clazz.ancestors.include?(::Mongoid::Document) @@ -45,26 +49,29 @@ def accessors end end + attr_reader :model + attr_reader :options + def initialize(model, options) @model = model @options = options end def results(client) - @client = client - - res = @client.search - collection = load_all(res.data.hits.hit.map(&:id)) + res = client.search + collection = load_all(data_from_response(res)) + start = client.parameters[:start] + size = client.parameters[:size] - if @client.parameters[:size] && @client.parameters[:start] + if size && start total_count = res.data.hits.found Nazrin::PaginationGenerator.generate( collection, - current_page: current_page, - per_page: @client.parameters[:size], + current_page: current_page(start, size), + per_page: size, total_count: total_count, - last_page: last_page(total_count)) + last_page: last_page(size, total_count)) else collection end @@ -74,14 +81,18 @@ def load_all raise NotImplementedError end + def data_from_response + raise NotImplementedError + end + private - def last_page(total_count) - (total_count / @client.parameters[:size].to_f).ceil + def last_page(size, total_count) + (total_count / size.to_f).ceil end - def current_page - (@client.parameters[:start] / @client.parameters[:size].to_f).ceil + 1 + def current_page(start, size) + (start / size.to_f).ceil + 1 end end end diff --git a/lib/nazrin/data_accessor/active_record.rb b/lib/nazrin/data_accessor/active_record.rb index 6a5aad9..4f2ca7f 100644 --- a/lib/nazrin/data_accessor/active_record.rb +++ b/lib/nazrin/data_accessor/active_record.rb @@ -4,16 +4,20 @@ class ActiveRecord < Nazrin::DataAccessor # load from activerecord def load_all(ids) records_table = {} - @options.each do |k, v| - @model = @model.send(k, v) + options.each do |k, v| + model = model.send(k, v) end - @model.where(id: ids).each do |record| + model.where(id: ids).each do |record| records_table[record.id] = record end ids.map do |id| records_table.select { |k, _| k == id.to_i }[id.to_i] end.reject(&:nil?) end + + def data_from_response(res) + res.data.hits.hit.map(&:id) + end end end end diff --git a/lib/nazrin/data_accessor/mongoid.rb b/lib/nazrin/data_accessor/mongoid.rb index 9e4240a..3434424 100644 --- a/lib/nazrin/data_accessor/mongoid.rb +++ b/lib/nazrin/data_accessor/mongoid.rb @@ -3,20 +3,24 @@ class DataAccessor class Mongoid < Nazrin::DataAccessor def load_all(ids) documents_table = {} - @options.each do |k, v| - @model = if v.nil? - @model.send(k) + options.each do |k, v| + model = if v.nil? + model.send(k) else - @model.send(k, v) + model.send(k, v) end end - @model.where('_id' => { '$in' => ids }).each do |document| + model.where('_id' => { '$in' => ids }).each do |document| documents_table[document._id.to_s] = document end ids.map do |id| documents_table[id] end.reject(&:nil?) end + + def data_from_response(res) + res.data.hits.hit.map(&:id) + end end end end diff --git a/lib/nazrin/data_accessor/struct.rb b/lib/nazrin/data_accessor/struct.rb new file mode 100644 index 0000000..64295f8 --- /dev/null +++ b/lib/nazrin/data_accessor/struct.rb @@ -0,0 +1,68 @@ +require 'aws-sdk-cloudsearch' + +module Nazrin + class DataAccessor + class Struct < Nazrin::DataAccessor + class MissingDomainNameConfigError < StandardError; end + + class << self + attr_reader :config + + def [](config) + Class.new(self).tap do |clazz| + clazz.instance_variable_set(:@config, config) + end + end + + def transform_attributes(attributes) + attributes.each_with_object({}) do |(name, value), hash| + type = field_types[name] + + if type.end_with?('array') + hash[name] = value + else + hash[name] = value.first + end + end + end + + def field_types + return @field_types if defined?(@field_types) + + response = cloudsearch_client.describe_index_fields( + domain_name: config.domain_name + ) + + @field_types = response.index_fields.each_with_object({}) do |field, fields| + name = field.options[:index_field_name] + type = field.options[:index_field_type] + + fields[name] = type + end + end + + private + + def cloudsearch_client + @cloudsearch_client ||= Aws::CloudSearch::Client.new( + access_key_id: config.access_key_id, + secret_access_key: config.secret_access_key, + logger: config.logger + ) + end + end + + def load_all(data) + data.map do |attributes| + model.new(attributes) + end + end + + def data_from_response(res) + res.data[:hits][:hit].map do |hit| + self.class.transform_attributes(hit[:fields]) + end + end + end + end +end diff --git a/lib/nazrin/searchable/config.rb b/lib/nazrin/searchable/config.rb index 9263e61..65f1146 100644 --- a/lib/nazrin/searchable/config.rb +++ b/lib/nazrin/searchable/config.rb @@ -1,6 +1,8 @@ module Nazrin module Searchable class Configuration + attr_accessor :domain_name + %i( search_endpoint document_endpoint diff --git a/nazrin.gemspec b/nazrin.gemspec index 23204e9..75a4460 100644 --- a/nazrin.gemspec +++ b/nazrin.gemspec @@ -18,6 +18,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_dependency 'aws-sdk-core', '~> 3' + spec.add_dependency 'aws-sdk-cloudsearch', '~> 1.0' spec.add_dependency 'aws-sdk-cloudsearchdomain', '~> 1.0' spec.add_dependency 'activesupport', '>= 4.0.0' diff --git a/spec/nazrin/data_accessor/struct_spec.rb b/spec/nazrin/data_accessor/struct_spec.rb new file mode 100644 index 0000000..a087353 --- /dev/null +++ b/spec/nazrin/data_accessor/struct_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe Nazrin::DataAccessor::Struct do + let(:clazz) do + Class.new do + def self.name; 'CustomStruct'; end + include Nazrin::Searchable + + attr_reader :attributes + + searchable_configure do |config| + config.domain_name = 'my-domain-name' + end + + def initialize(attributes) + @attributes = attributes + end + end + end + let(:data_accessor) { Nazrin::DataAccessor.for(clazz) } + + it do + expect(data_accessor).to be < Nazrin::DataAccessor::Struct + end + + describe '.field_types' do + let(:cs_client) do + instance_double(Aws::CloudSearch::Client) + end + let(:index_fields_response) { double(index_fields: index_fields) } + let(:index_fields) do + [id_field, name_field, tags_field] + end + let(:id_field) do + double( + options: { + index_field_name: 'id', + index_field_type: 'int' + } + ) + end + let(:name_field) do + double( + options: { + index_field_name: 'name', + index_field_type: 'text' + } + ) + end + let(:tags_field) do + double( + options: { + index_field_name: 'tags', + index_field_type: 'literal-array' + } + ) + end + + before do + allow(Aws::CloudSearch::Client).to receive(:new).and_return( + cs_client + ) + allow(cs_client).to receive(:describe_index_fields).and_return( + index_fields_response + ) + end + + it do + expect(Aws::CloudSearch::Client).to receive(:new).with( + access_key_id: Nazrin.config.access_key_id, + secret_access_key: Nazrin.config.secret_access_key, + logger: Nazrin.config.logger + ) + expect(cs_client).to receive(:describe_index_fields).with( + domain_name: 'my-domain-name' + ) + expect(data_accessor.field_types).to eq( + 'id' => 'int', + 'name' => 'text', + 'tags' => 'literal-array' + ) + end + end + + describe '.transform_attributes' do + let(:attributes) do + { + 'id' => ['1'], + 'name' => ['Michael'], + 'tags' => ['one', 'two', 'three'] + } + end + + before do + allow(data_accessor).to receive(:field_types).and_return( + 'id' => 'int', + 'name' => 'text', + 'tags' => 'literal-array' + ) + end + + it do + expect(data_accessor.transform_attributes(attributes)).to eq( + 'id' => '1', + 'name' => 'Michael', + 'tags' => ['one', 'two', 'three'] + ) + end + end +end diff --git a/spec/nazrin/struct/searchable_spec.rb b/spec/nazrin/struct/searchable_spec.rb new file mode 100644 index 0000000..b35a1ee --- /dev/null +++ b/spec/nazrin/struct/searchable_spec.rb @@ -0,0 +1,75 @@ +describe Nazrin::Searchable do + let(:clazz) do + Class.new do + def self.name; 'CustomStruct'; end + include Nazrin::Searchable + + attr_reader :attributes + + searchable_configure do |config| + config.domain_name = 'my-domain-name' + end + + def initialize(attributes) + @attributes = attributes + end + end + end + let(:data_accessor) { Nazrin::DataAccessor.for(clazz) } + + describe '#search' do + let(:result) do + clazz.search.query_parser('structured').query('matchall').execute + end + let(:fake_response) do + double( + data: { + hits: { + hit: [ + { + fields: { + 'id' => ['1'], + 'name' => ['Michael'], + 'tags' => ['one', 'two', 'three'] + } + }, + { + fields: { + 'id' => ['2'], + 'name' => ['Florence'], + 'tags' => ['four', 'five'] + } + } + ] + } + } + ) + end + before do + allow(data_accessor).to receive(:field_types).and_return( + 'id' => 'int', + 'name' => 'text', + 'tags' => 'literal-array' + ) + allow_any_instance_of(Nazrin::SearchClient).to receive(:search).and_return( + fake_response + ) + end + + it { expect(result.length).to eq(2) } + it do + expect(result[0].attributes).to eq( + 'id' => '1', + 'name' => 'Michael', + 'tags' => ['one', 'two', 'three'] + ) + end + it do + expect(result[1].attributes).to eq( + 'id' => '2', + 'name' => 'Florence', + 'tags' => ['four', 'five'] + ) + end + end +end