#!/usr/bin/env ruby # frozen_string_literal: true require 'connection_pool' require 'pg' require 'sinatra' require 'sinatra/json' $DB_POOL = ConnectionPool.new size: 5, timeout: 5 do PG.connect( host: 'localhost', dbname: 'leqsikoni', user: 'leqsikoni', password: 'password', ).tap do |conn| conn.type_map_for_results = PG::BasicTypeMapForResults.new conn end end before do headers 'Access-Control-Allow-Origin' => '*' end get '/words/:id' do Context.call db_pool: $DB_POOL, locale: params[:locale] do |context| word_id = Integer params[:id] word_info = context.word_info(word_id) json({ primary_form: word_info[:primary_form], part_of_speech: context.part_of_speech_name(word_id), commentary: word_info[:commentary], translations: context.translations(word_id), inflections: context.inflections(word_id), examples: context.examples(word_id), }) end end class Context private_class_method :new def self.call(db_pool:, **kwargs, &block) new(**kwargs).send :call, db_pool: db_pool, &block end def initialize(locale:) self.locale = locale end private attr_reader :locale, :locale_id def call(db_pool:) db_pool.with do |db_conn| @locale_id = db_conn.exec_params( ( <<~SQL SELECT id FROM languages WHERE code = $1 ORDER BY (CASE WHEN code != 'en' THEN 1 ELSE 2 END) ASC LIMIT 1 SQL ), [locale], ).values.first&.first @db_conn = db_conn result = yield self @db_conn = nil result end end def locale=(locale) @locale = String(locale).strip.freeze end public ## # @return [Hash{Symbol => String}, nil] # def word_info(word_id) word_id = Integer word_id row = @db_conn.exec_params( 'SELECT primary_form, commentary FROM words WHERE id = $1', [word_id], ).to_a.first primary_form = String(row['primary_form']).strip.freeze primary_form = nil if primary_form.empty? commentary = String(row['commentary']).strip.freeze commentary = nil if commentary.empty? { primary_form: primary_form, commentary: commentary, }.freeze end ## # @return [String, nil] # def part_of_speech_name(word_id) word_id = Integer word_id column = @db_conn.exec_params( ( <<~SQL SELECT part_names.value FROM words INNER JOIN parts ON words.part_id = parts.id INNER JOIN part_names ON part_names.part_id = parts.id WHERE words.id = $1 AND ( part_names.name_lang_id = $2 OR $2 IS NULL ) LIMIT 1 SQL ), [word_id, locale_id], ).values.first&.first str = String(column).strip.freeze str unless str.empty? end def translations(word_id) word_id = Integer word_id values = @db_conn.exec_params( ( <<~SQL SELECT translation_texts.value, translation_texts.commentary FROM words INNER JOIN translations ON translations.word_id = words.id INNER JOIN translation_texts ON translation_texts.translation_id = translations.id WHERE word_id = $1 AND translation_texts.lang_id = $2 ORDER BY translations.index ASC SQL ), [word_id, locale_id], ).to_a values.map.with_index do |row, index| commentary = String(row['commentary']).strip.freeze commentary = nil if commentary.empty? { index: index + 1, translation: String(row['value']).strip.freeze, commentary: commentary, }.freeze end.freeze end def inflections(word_id) word_id = Integer word_id result = @db_conn.exec_params( ( <<~SQL SELECT inflections.descr, word_forms.value, word_forms.other_word_id FROM words INNER JOIN parts ON parts.id = words.part_id INNER JOIN inflections ON inflections.part_id = parts.id FULL OUTER JOIN word_forms ON word_forms.inflection_id = inflections.id AND word_forms.word_id = $1 WHERE words.id = $1 ORDER BY inflections.index SQL ), [word_id], ).to_a result.map do |row| descr = String(row['descr']).strip.freeze value = String(row['value']).strip.freeze value = nil if value.empty? unless row['other_word_id'].nil? other_word_id = Integer(row['other_word_id']) end { descr: descr, value: value, other_word_id: other_word_id, } end.freeze end def examples(word_id) word_id = Integer word_id left_lang_id = 5 # Georgian right_lang_id = 2 # Russian lang_order = left_lang_id < right_lang_id ? 'ASC' : 'DESC' result = @db_conn.exec_params( ( <<~SQL SELECT foo.example_id, array_agg(foo.id ORDER BY foo.language_id #{lang_order})::text[] AS ids, array_agg(foo.value ORDER BY foo.language_id #{lang_order})::text[] AS values FROM example_texts foo INNER JOIN ( SELECT example_id, language_id, MIN(index) as min_index FROM example_texts WHERE example_id = ANY( SELECT example_texts.example_id FROM words INNER JOIN word_forms ON word_forms.word_id = words.id INNER JOIN word_form_example_texts ON word_form_example_texts.word_form_id = word_forms.id INNER JOIN example_texts ON example_texts.id = word_form_example_texts.example_text_id WHERE words.id = $1 GROUP BY example_texts.example_id ) AND (language_id = $2 OR language_id = $3) GROUP BY example_id, language_id ) bar ON foo.example_id = bar.example_id AND foo.language_id = bar.language_id AND foo.index = bar.min_index GROUP BY foo.example_id SQL ), [word_id, left_lang_id, right_lang_id], ).map do |row| { example_id: Integer(row['example_id']), left_id: Integer(row['ids'].first), right_id: Integer(row['ids'].last), left: String(row['values'].first).strip.freeze, right: String(row['values'].last).strip.freeze, highlights: [], }.freeze end.freeze highlights = @db_conn.exec_params( ( <<~SQL SELECT example_texts.id AS example_text_id, array_agg( word_form_example_texts.pos ORDER BY word_form_example_texts.word_form_id ) AS positions, array_agg( word_forms.len ORDER BY word_forms.id ) AS lengths FROM example_texts INNER JOIN word_form_example_texts ON word_form_example_texts.example_text_id = example_texts.id INNER JOIN word_forms ON word_form_example_texts.word_form_id = word_forms.id WHERE language_id = $1::bigint AND example_texts.example_id = ANY($2::bigint[]) GROUP BY example_texts.id SQL ), [ left_lang_id, PG::TextEncoder::Array.new.encode(result.map { |h| h[:example_id] }), ], ) .map do |row| example_text_id = Integer(row['example_text_id']) positions = row['positions'].map { |pos| Integer pos }.freeze lengths = row['lengths'].map { |len| Integer len }.freeze { example_text_id: example_text_id, data: positions.zip(lengths).map do |pos, len| { pos: pos, len: len }.freeze end.sort_by { |h| h[:pos] }.freeze, } end.to_a highlights.each do |highlight| result .find { |item| item[:left_id] == highlight[:example_text_id] } &.tap { |item| item[:highlights].replace highlight[:data] } end result.each { |h| h[:highlights].freeze } end end