diff --git a/.gitignore b/.gitignore index 2f761fe..85ed304 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ coverage config/settings.local.yml config/settings/*.local.yml config/environments/*.local.yml +.idea diff --git a/Gemfile b/Gemfile index fc76bca..aaad069 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ gem 'puma', '>= 6.5.0' # Базы данных gem 'pg' +gem 'mongoid' # Многопоточное выполнение gem 'parallel' @@ -29,6 +30,10 @@ gem 'activeadmin' gem 'activeadmin_addons' gem 'devise' +gem 'alba' +gem 'kaminari' +gem 'kaminari-mongoid' + group :development, :test do gem 'bundler-audit' gem 'capybara' diff --git a/Gemfile.lock b/Gemfile.lock index d66b198..d6edac5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,6 +95,8 @@ GEM uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + alba (3.5.0) + ostruct (~> 0.6) arbre (1.7.0) activesupport (>= 3.0.0) ruby2_keywords (>= 0.0.2) @@ -104,6 +106,7 @@ GEM benchmark (0.4.0) bigdecimal (3.1.8) bindex (0.8.1) + bson (5.0.2) builder (3.3.0) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) @@ -210,6 +213,9 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + kaminari-mongoid (1.0.2) + kaminari-core (~> 1.0) + mongoid language_server-protocol (3.17.0.3) logger (1.6.0) loofah (2.23.1) @@ -226,6 +232,12 @@ GEM mini_mime (1.1.5) mini_portile2 (2.8.8) minitest (5.25.1) + mongo (2.21.0) + bson (>= 4.14.1, < 6.0.0) + mongoid (9.0.6) + activemodel (>= 5.1, < 8.1, != 7.0.0) + concurrent-ruby (>= 1.0.5, < 2.0) + mongo (>= 2.18.0, < 3.0.0) net-imap (0.5.6) date net-protocol @@ -270,7 +282,7 @@ GEM puma (6.5.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.10) + rack (3.1.11) rack-session (2.0.0) rack (>= 3.0.0) rack-test (2.1.0) @@ -416,7 +428,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) - uri (1.0.2) + uri (1.0.3) useragent (0.16.10) warden (1.2.9) rack (>= 2.0.9) @@ -448,6 +460,7 @@ DEPENDENCIES activeadmin activeadmin_addons activerecord-import + alba bundler-audit capybara config @@ -460,6 +473,9 @@ DEPENDENCIES factory_bot_rails fasterer ffaker + kaminari + kaminari-mongoid + mongoid parallel pg pry-byebug diff --git a/app/controllers/books_controller.rb b/app/controllers/books_controller.rb new file mode 100644 index 0000000..a796942 --- /dev/null +++ b/app/controllers/books_controller.rb @@ -0,0 +1,15 @@ +class BooksController < ApplicationController + def index + query = Mongo::Book.all.page(params[:page]).per(Settings.app.items_per_page) + + render json: { + books: query, + pagination: { + page: query.current_page, + per_page: query.limit_value, + page_count: query.total_pages, + total: query.total_count + } + } + end +end diff --git a/app/controllers/concerns/pagination.rb b/app/controllers/concerns/pagination.rb new file mode 100644 index 0000000..302aba1 --- /dev/null +++ b/app/controllers/concerns/pagination.rb @@ -0,0 +1,18 @@ +module Pagination + extend ActiveSupport::Concern + include Pagy::Backend + + def paginate_json(scope:, key:, serializer:) + pagy, records = pagy(scope) + + { + key => serializer.new(records), + pagination: { + page: pagy.page, + per_page: pagy.limit, + page_count: pagy.pages, + total: pagy.count + } + } + end +end diff --git a/app/models/mongo/author.rb b/app/models/mongo/author.rb new file mode 100644 index 0000000..8a67219 --- /dev/null +++ b/app/models/mongo/author.rb @@ -0,0 +1,11 @@ +module Mongo + class Author + include Mongoid::Document + + field :id, type: String + field :first_name, type: String + field :last_name, type: String + field :middle_name, type: String + field :original, type: String + end +end diff --git a/app/models/mongo/book.rb b/app/models/mongo/book.rb new file mode 100644 index 0000000..01b33fc --- /dev/null +++ b/app/models/mongo/book.rb @@ -0,0 +1,23 @@ +module Mongo + class Book + include Mongoid::Document + include Mongoid::Timestamps + + field :title, type: String + field :series, type: String + field :serno, type: String + field :libid, type: Integer + field :size, type: Integer + field :filename, type: Integer + field :del, type: Boolean + field :ext, type: String + field :published_at, type: Date + field :insno, type: String + field :language, type: String + + embeds_one :folder + embeds_many :authors + embeds_many :genres + embeds_many :keywords + end +end diff --git a/app/models/mongo/folder.rb b/app/models/mongo/folder.rb new file mode 100644 index 0000000..b7a1908 --- /dev/null +++ b/app/models/mongo/folder.rb @@ -0,0 +1,8 @@ +module Mongo + class Folder + include Mongoid::Document + + field :id, type: String + field :name, type: String + end +end diff --git a/app/models/mongo/genre.rb b/app/models/mongo/genre.rb new file mode 100644 index 0000000..73a4d95 --- /dev/null +++ b/app/models/mongo/genre.rb @@ -0,0 +1,9 @@ +module Mongo + class Genre + include Mongoid::Document + + field :id, type: String + field :slug, type: String + field :name, type: String + end +end diff --git a/app/models/mongo/keyword.rb b/app/models/mongo/keyword.rb new file mode 100644 index 0000000..c3793d3 --- /dev/null +++ b/app/models/mongo/keyword.rb @@ -0,0 +1,8 @@ +module Mongo + class Keyword + include Mongoid::Document + + field :id, type: String + field :name, type: String + end +end diff --git a/app/serializers/author_serializer.rb b/app/serializers/author_serializer.rb new file mode 100644 index 0000000..b68aaa3 --- /dev/null +++ b/app/serializers/author_serializer.rb @@ -0,0 +1,5 @@ +class AuthorSerializer + include Alba::Resource + + attributes :id, :first_name, :last_name, :middle_name, :original +end diff --git a/app/serializers/book_serializer.rb b/app/serializers/book_serializer.rb new file mode 100644 index 0000000..23b15cd --- /dev/null +++ b/app/serializers/book_serializer.rb @@ -0,0 +1,15 @@ +class BookSerializer + include Alba::Resource + + attributes :id, :title, :series, :serno, :libid, :size, :filename, + :del, :ext, :published_at, :insno + + one :folder, resource: FolderSerializer + many :authors, resource: AuthorSerializer + many :genres, resource: GenreSerializer + many :keywords, resource: KeywordSerializer + + attribute :language do |book| + book.language.slug + end +end diff --git a/app/serializers/folder_serializer.rb b/app/serializers/folder_serializer.rb new file mode 100644 index 0000000..9295e0f --- /dev/null +++ b/app/serializers/folder_serializer.rb @@ -0,0 +1,5 @@ +class FolderSerializer + include Alba::Resource + + attributes :id, :name +end diff --git a/app/serializers/genre_serializer.rb b/app/serializers/genre_serializer.rb new file mode 100644 index 0000000..f61b639 --- /dev/null +++ b/app/serializers/genre_serializer.rb @@ -0,0 +1,5 @@ +class GenreSerializer + include Alba::Resource + + attributes :id, :slug, :name +end diff --git a/app/serializers/keyword_serializer.rb b/app/serializers/keyword_serializer.rb new file mode 100644 index 0000000..eeb9acd --- /dev/null +++ b/app/serializers/keyword_serializer.rb @@ -0,0 +1,5 @@ +class KeywordSerializer + include Alba::Resource + + attributes :id, :name +end diff --git a/config/database.yml b/config/database.yml index 48f30ee..4277af8 100644 --- a/config/database.yml +++ b/config/database.yml @@ -17,6 +17,7 @@ development: test: <<: *default + <<: *postgre database: library_test production: diff --git a/config/initializers/mongoid.rb b/config/initializers/mongoid.rb new file mode 100644 index 0000000..6371d1a --- /dev/null +++ b/config/initializers/mongoid.rb @@ -0,0 +1,5 @@ +# rubocop:todo all +Mongoid.configure do + target_version = "9.0" + config.load_defaults target_version +end diff --git a/config/mongoid.yml b/config/mongoid.yml new file mode 100644 index 0000000..1cc6ecd --- /dev/null +++ b/config/mongoid.yml @@ -0,0 +1,6 @@ +development: + clients: + default: + database: library_development + hosts: + - <%= ENV.fetch('MONGO_HOST') { 'localhost:27017' } %> diff --git a/config/routes.rb b/config/routes.rb index ae608d2..75e2b42 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,7 @@ # # delete 'logout', to: 'sessions#destroy' # # end # end + get 'books(/:page)', to: 'books#index', defaults: { page: 1 }, constraints: { page: /\d+/ } # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. diff --git a/docker-compose.yml b/docker-compose.yml index 9c195af..d0fe4c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,14 @@ services: POSTGRES_PASSWORD: 'postgres' volumes: - db:/var/lib/postgresql/data + mongodb: + image: mongo:6.0 + container_name: mongodb + restart: always + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db web: tty: true stdin_open: true @@ -33,5 +41,7 @@ services: POSTGRES_HOST: 'db' POSTGRES_USER: 'postgres' POSTGRES_PASSWORD: 'postgres' + MONGO_HOST: 'mongodb:27017' volumes: db: + mongodb_data: diff --git a/docs/kafkatry-ruby/.gitattributes b/docs/kafkatry-ruby/.gitattributes new file mode 100644 index 0000000..5168571 --- /dev/null +++ b/docs/kafkatry-ruby/.gitattributes @@ -0,0 +1,10 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark the yarn lockfile as having been generated. +yarn.lock linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored diff --git a/docs/kafkatry-ruby/.gitignore b/docs/kafkatry-ruby/.gitignore new file mode 100644 index 0000000..9f45ac7 --- /dev/null +++ b/docs/kafkatry-ruby/.gitignore @@ -0,0 +1,31 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle +/.env + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key +/config/credentials.yml.enc + +/.idea +*.local.yml +coverage diff --git a/docs/kafkatry-ruby/.rubocop.yml b/docs/kafkatry-ruby/.rubocop.yml new file mode 100644 index 0000000..05cfcba --- /dev/null +++ b/docs/kafkatry-ruby/.rubocop.yml @@ -0,0 +1,70 @@ +require: + - rubocop-performance + - rubocop-rails + - rubocop-rspec + +Rails: + Enabled: true + +Rails/FilePath: + EnforcedStyle: arguments + +Style/Documentation: + Enabled: false + +Layout/LineLength: + AutoCorrect: true + Max: 100 + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/RescueStandardError: + Enabled: false + +Style/AsciiComments: + Enabled: false + +Metrics/BlockLength: + Exclude: + - 'Rakefile' + - '**/*.rake' + - 'spec/**/*' + +Rails/HttpPositionalArguments: + Exclude: + - 'spec/**/*' + +AllCops: + Exclude: + - 'tmp/**/*' + - 'db/**/*' + - 'bin/**/*' + - 'lib/tasks/sneakers.rake' + - 'vendor/bundle/**/*' + NewCops: enable + +# if else by tab +Layout/EndAlignment: + EnforcedStyleAlignWith: variable +# кол-во expect в блоке it или specify +RSpec/MultipleExpectations: + Max: 5 +# кол-во строк в блоке it или specify +RSpec/ExampleLength: + Max: 10 +# использование allow_any_instance_of +RSpec/AnyInstance: + Enabled: false +# use have_received +RSpec/MessageSpies: + EnforcedStyle: have_received +# кол-во вложенных контекстов +RSpec/NestedGroups: + Max: 5 +Style/RedundantFetchBlock: + Enabled: false +Rails/UnknownEnv: + Enabled: false +Style/HashSyntax: + Enabled: false diff --git a/docs/kafkatry-ruby/.ruby-version b/docs/kafkatry-ruby/.ruby-version new file mode 100644 index 0000000..c877459 --- /dev/null +++ b/docs/kafkatry-ruby/.ruby-version @@ -0,0 +1 @@ +ruby-3.1.3 diff --git a/docs/kafkatry-ruby/.tool-versions b/docs/kafkatry-ruby/.tool-versions new file mode 100644 index 0000000..520a168 --- /dev/null +++ b/docs/kafkatry-ruby/.tool-versions @@ -0,0 +1 @@ +ruby 3.1.3 diff --git a/docs/kafkatry-ruby/Dockerfile b/docs/kafkatry-ruby/Dockerfile new file mode 100644 index 0000000..ded51f8 --- /dev/null +++ b/docs/kafkatry-ruby/Dockerfile @@ -0,0 +1,29 @@ +FROM ruby:3.1.3-alpine3.17 AS builder + +RUN mkdir -p /var/cache/apk && ln -s /var/cache/apk /etc/apk/cache + +RUN --mount=type=cache,uid=1000,target=/var/cache/apk \ + apk update && apk add make gcc postgresql-dev musl-dev bash g++ jq nodejs + +FROM builder AS gems + +RUN gem install bundler:2.4.3 rubygems-bundler +RUN gem regenerate_binstubs + +COPY Gemfile Gemfile.lock ./ + +RUN bundle config set app_config .bundle +RUN bundle config set path /.cache/bundle +RUN --mount=type=cache,uid=1000,target=/.cache/bundle \ + bundle install --jobs $(nproc) && \ + mkdir -p vendor && \ + cp -ar .cache/bundle vendor/bundle +RUN bundle config set path vendor/bundle + +FROM builder + +WORKDIR /app +COPY . /app + +COPY --from=gems /usr/local/bundle/ /usr/local/bundle/ +COPY --from=gems /vendor/bundle /app/vendor/bundle diff --git a/docs/kafkatry-ruby/Gemfile b/docs/kafkatry-ruby/Gemfile new file mode 100644 index 0000000..0110e70 --- /dev/null +++ b/docs/kafkatry-ruby/Gemfile @@ -0,0 +1,41 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '3.1.3' + +gem 'pg', '~> 1.1' +gem 'puma', '~> 5.0' +gem 'rails', '~> 6.1.7', '>= 6.1.7.2' +gem 'sass-rails', '>= 6' + +# Administration +gem 'activeadmin' +gem 'activeadmin_addons' +gem 'coffee-rails', '~> 5.0.0' +gem 'devise' + +# Queue broker +gem 'karafka', '~> 2.0' + +gem "tzinfo-data" + +group :development, :test do + gem 'byebug' + gem 'rubocop', require: false + gem 'rubocop-performance' + gem 'rubocop-rails' + gem 'rubocop-rspec' +end + +group :development do + gem 'annotate', require: false + gem 'better_errors' + gem 'listen', '~> 3.3' + gem 'web-console', '>= 4.1.0' +end + +group :test do + gem 'capybara', '>= 3.26' + gem 'selenium-webdriver', '>= 4.0.0.rc1' + gem 'webdrivers' +end diff --git a/docs/kafkatry-ruby/Gemfile.lock b/docs/kafkatry-ruby/Gemfile.lock new file mode 100644 index 0000000..220dbf1 --- /dev/null +++ b/docs/kafkatry-ruby/Gemfile.lock @@ -0,0 +1,372 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.1.7.2) + actionpack (= 6.1.7.2) + activesupport (= 6.1.7.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.1.7.2) + actionpack (= 6.1.7.2) + activejob (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) + mail (>= 2.7.1) + actionmailer (6.1.7.2) + actionpack (= 6.1.7.2) + actionview (= 6.1.7.2) + activejob (= 6.1.7.2) + activesupport (= 6.1.7.2) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.1.7.2) + actionview (= 6.1.7.2) + activesupport (= 6.1.7.2) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.7.2) + actionpack (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) + nokogiri (>= 1.8.5) + actionview (6.1.7.2) + activesupport (= 6.1.7.2) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_material (1.5.2) + activeadmin (2.13.1) + arbre (~> 1.2, >= 1.2.1) + formtastic (>= 3.1, < 5.0) + formtastic_i18n (~> 0.4) + inherited_resources (~> 1.7) + jquery-rails (~> 4.2) + kaminari (~> 1.0, >= 1.2.1) + railties (>= 6.1, < 7.1) + ransack (>= 2.1.1, < 4) + activeadmin_addons (1.9.0) + active_material + railties + require_all + sassc + sassc-rails + xdan-datetimepicker-rails (~> 2.5.1) + activejob (6.1.7.2) + activesupport (= 6.1.7.2) + globalid (>= 0.3.6) + activemodel (6.1.7.2) + activesupport (= 6.1.7.2) + activerecord (6.1.7.2) + activemodel (= 6.1.7.2) + activesupport (= 6.1.7.2) + activestorage (6.1.7.2) + actionpack (= 6.1.7.2) + activejob (= 6.1.7.2) + activerecord (= 6.1.7.2) + activesupport (= 6.1.7.2) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (6.1.7.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) + rake (>= 10.4, < 14.0) + arbre (1.5.0) + activesupport (>= 3.0.0, < 7.1) + ruby2_keywords (>= 0.0.2, < 1.0) + ast (2.4.2) + bcrypt (3.1.18) + better_errors (2.9.1) + coderay (>= 1.0.0) + erubi (>= 1.0.0) + rack (>= 0.9.0) + bindex (0.8.1) + builder (3.2.4) + byebug (11.1.3) + capybara (3.38.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + coderay (1.1.3) + coffee-rails (5.0.0) + coffee-script (>= 2.2.0) + railties (>= 5.2.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + concurrent-ruby (1.2.0) + crass (1.0.6) + date (3.3.3) + devise (4.8.1) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + erubi (1.12.0) + execjs (2.8.1) + ffi (1.15.5) + formtastic (4.0.0) + actionpack (>= 5.2.0) + formtastic_i18n (0.7.0) + globalid (1.1.0) + activesupport (>= 5.0) + has_scope (0.8.1) + actionpack (>= 5.2) + activesupport (>= 5.2) + i18n (1.12.0) + concurrent-ruby (~> 1.0) + inherited_resources (1.13.1) + actionpack (>= 5.2, < 7.1) + has_scope (~> 0.6) + railties (>= 5.2, < 7.1) + responders (>= 2, < 4) + jquery-rails (4.5.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (2.6.3) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + karafka (2.0.31) + karafka-core (>= 2.0.11, < 3.0.0) + thor (>= 0.20) + waterdrop (>= 2.4.10, < 3.0.0) + zeitwerk (~> 2.3) + karafka-core (2.0.11) + concurrent-ruby (>= 1.1) + karafka-rdkafka (>= 0.12.1) + karafka-rdkafka (0.12.1) + ffi (~> 1.15) + mini_portile2 (~> 2.6) + rake (> 12) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.19.1) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.2) + matrix (0.4.2) + method_source (1.0.0) + mini_mime (1.1.2) + mini_portile2 (2.8.1) + minitest (5.17.0) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol + nio4r (2.7.4) + nokogiri (1.14.1-arm64-darwin) + racc (~> 1.4) + nokogiri (1.14.1-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.14.1-x86_64-linux) + racc (~> 1.4) + orm_adapter (0.5.0) + parallel (1.22.1) + parser (3.2.1.0) + ast (~> 2.4.1) + pg (1.4.5) + public_suffix (5.0.1) + puma (5.6.5) + nio4r (~> 2.0) + racc (1.6.2) + rack (2.2.6.2) + rack-test (2.0.2) + rack (>= 1.3) + rails (6.1.7.2) + actioncable (= 6.1.7.2) + actionmailbox (= 6.1.7.2) + actionmailer (= 6.1.7.2) + actionpack (= 6.1.7.2) + actiontext (= 6.1.7.2) + actionview (= 6.1.7.2) + activejob (= 6.1.7.2) + activemodel (= 6.1.7.2) + activerecord (= 6.1.7.2) + activestorage (= 6.1.7.2) + activesupport (= 6.1.7.2) + bundler (>= 1.15.0) + railties (= 6.1.7.2) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) + railties (6.1.7.2) + actionpack (= 6.1.7.2) + activesupport (= 6.1.7.2) + method_source + rake (>= 12.2) + thor (~> 1.0) + rainbow (3.1.1) + rake (13.0.6) + ransack (3.2.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) + i18n + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + regexp_parser (2.7.0) + require_all (3.0.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.2.5) + rubocop (1.45.1) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.24.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.26.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.17.0) + rubocop (~> 1.41) + rubocop-performance (1.16.0) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.17.4) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-rspec (2.18.1) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + selenium-webdriver (4.8.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + sprockets (4.2.0) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + thor (1.2.1) + tilt (2.0.11) + timeout (0.3.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + tzinfo-data (1.2022.7) + tzinfo (>= 1.0.0) + unicode-display_width (2.4.2) + warden (1.2.9) + rack (>= 2.0.9) + waterdrop (2.4.10) + karafka-core (>= 2.0.9, < 3.0.0) + zeitwerk (~> 2.3) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webdrivers (5.2.0) + nokogiri (~> 1.6) + rubyzip (>= 1.3.0) + selenium-webdriver (~> 4.0) + websocket (1.2.9) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xdan-datetimepicker-rails (2.5.4) + jquery-rails + rails (>= 3.2.16) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.6.7) + +PLATFORMS + arm64-darwin-22 + x86_64-darwin-22 + x86_64-darwin-24 + x86_64-linux-musl + +DEPENDENCIES + activeadmin + activeadmin_addons + annotate + better_errors + byebug + capybara (>= 3.26) + coffee-rails (~> 5.0.0) + devise + karafka (~> 2.0) + listen (~> 3.3) + nio4r (>= 2.5.9) + pg (~> 1.1) + puma (~> 5.0) + rails (~> 6.1.7, >= 6.1.7.2) + rubocop + rubocop-performance + rubocop-rails + rubocop-rspec + sass-rails (>= 6) + selenium-webdriver (>= 4.0.0.rc1) + tzinfo-data + web-console (>= 4.1.0) + webdrivers + +RUBY VERSION + ruby 3.1.3p185 + +BUNDLED WITH + 2.4.3 diff --git a/docs/kafkatry-ruby/Makefile b/docs/kafkatry-ruby/Makefile new file mode 100644 index 0000000..7643a41 --- /dev/null +++ b/docs/kafkatry-ruby/Makefile @@ -0,0 +1,17 @@ +build: + APP_HOST=$(or ${APP_HOST},${APP_HOST}, "localhost") docker compose build + +db_setup: + APP_HOST=$(or ${APP_HOST},${APP_HOST}, "localhost") docker compose run --rm server bundle exec rails db:create db:migrate + +up: + APP_HOST=$(or ${APP_HOST},${APP_HOST}, "localhost") docker compose up + +server_bash: + APP_HOST=$(or ${APP_HOST},${APP_HOST}, "localhost") docker compose run --rm server bash + +rails_console: + APP_HOST=$(or ${APP_HOST},${APP_HOST}, "localhost") docker compose run --rm server bundle exec rails c + +send_message: + APP_HOST=$(or ${APP_HOST},${APP_HOST}, "localhost") docker compose run --rm server bundle exec rake karafka:producer:produce_sync diff --git a/docs/kafkatry-ruby/README.md b/docs/kafkatry-ruby/README.md new file mode 100644 index 0000000..88cd499 --- /dev/null +++ b/docs/kafkatry-ruby/README.md @@ -0,0 +1,49 @@ +## Использование Kafka в качестве брокера сообщений + +Для межсервисного взаимодействия планируется использовать брокер сообщений Kafka. + +## Подготовка + +```bash +make build +make db_setup +``` + +## Запуск + +Для успешной работы необходимо подготовить кластер Kafka, запустить RoR-приложение и обработчик Kafka-сообщений + +### Запуск кластера kafka + +```bash +make up +``` + +Для того, чтобы понимать, что происходит внутри kafka, можно использовать redpanda-панель, которая будет доступна по пути http://localhost:8080/ + +Система администрирования приложения находится по ссылке http://localhost:3000/admin/ + +Отправить сообщение: + +```bash +make send_message +``` + +или из консоли rails + +```ruby +docker compose bundle exec -it server bundle exec rails c +> Karafka.producer.produce_sync(topic: 'example', payload: { 'ping' => 'pong' }.to_json) +``` + +## План работ + +* регистрация kafka-сообщений в модели EventsMessage +* сервис-объект на отправку сообщений +* сервис-объект на регистрацию сообщений +* кнопка в active_admin на отправку уведомлений +* нужно подружить karafka с ActiveJob +* гем dotenv +* гем config +* тесты +* probe-роуты для Kubernetes diff --git a/docs/kafkatry-ruby/Rakefile b/docs/kafkatry-ruby/Rakefile new file mode 100644 index 0000000..e85f913 --- /dev/null +++ b/docs/kafkatry-ruby/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/docs/kafkatry-ruby/app/admin/kafka_messages.rb b/docs/kafkatry-ruby/app/admin/kafka_messages.rb new file mode 100644 index 0000000..a4737bd --- /dev/null +++ b/docs/kafkatry-ruby/app/admin/kafka_messages.rb @@ -0,0 +1,57 @@ +# rubocop:disable Metrics/BlockLength +ActiveAdmin.register KafkaMessage do + menu priority: 1, label: proc { I18n.t('active_admin.kafka_messages') } + + actions :index, :show, :resend + + action_item(:edit, only: :show) do + button_to I18n.t('active_admin.resend'), resend_admin_kafka_message_path(kafka_message) + end + + config.sort_order = 'created_at_desc' + + filter :entity_id + filter :action, as: :select, + collection: proc { KafkaMessage.select(:action).distinct.pluck(:action).sort } + filter :success + filter :direction, as: :select, collection: proc { %w[income outcome] } + + controller do + def scoped_collection + end_of_association_chain.select( + :id, + :direction, + :action, + :created_at, + :updated_at + ) + end + end + + index do + column :entity_id + column :action + column :direction + column :created_at + actions + end + + show do + attributes_table do + row :id + row :action + row :direction + row :created_at + row :updated_at + row :data do |message| + div class: 'b-json__show', 'data-json': message.data.to_json + end + end + end + + # member_action :resend, method: :post do + # resource.resend! + # redirect_to resource_path, notice: I18n.t('active_admin.resended') + # end +end +# rubocop:enable Metrics/BlockLength diff --git a/docs/kafkatry-ruby/app/assets/config/manifest.js b/docs/kafkatry-ruby/app/assets/config/manifest.js new file mode 100644 index 0000000..5918193 --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css diff --git a/docs/kafkatry-ruby/app/assets/images/favicon.ico b/docs/kafkatry-ruby/app/assets/images/favicon.ico new file mode 100644 index 0000000..29fba17 Binary files /dev/null and b/docs/kafkatry-ruby/app/assets/images/favicon.ico differ diff --git a/docs/kafkatry-ruby/app/assets/javascripts/active_admin.js.coffee b/docs/kafkatry-ruby/app/assets/javascripts/active_admin.js.coffee new file mode 100644 index 0000000..e6a8b3b --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/javascripts/active_admin.js.coffee @@ -0,0 +1,7 @@ +#= require active_admin/base +#= require active_material +#= require activeadmin_addons/all +#= require rabbit_messages +#= require jsonb +#= require jquery.json-viewer +#= require jquery.suggestions.min diff --git a/docs/kafkatry-ruby/app/assets/javascripts/application.js b/docs/kafkatry-ruby/app/assets/javascripts/application.js new file mode 100644 index 0000000..5a9680a --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/javascripts/application.js @@ -0,0 +1,15 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's +// vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require rails-ujs +// require turbolinks +//= require_tree . diff --git a/docs/kafkatry-ruby/app/assets/javascripts/jquery.json-viewer.js b/docs/kafkatry-ruby/app/assets/javascripts/jquery.json-viewer.js new file mode 100644 index 0000000..3ecbc3f --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/javascripts/jquery.json-viewer.js @@ -0,0 +1,148 @@ +/** + * jQuery json-viewer + * @author: Alexandre Bodelot + */ +(function($){ + + /** + * Check if arg is either an array with at least 1 element, or a dict with at least 1 key + * @return boolean + */ + function isCollapsable(arg) { + return arg instanceof Object && Object.keys(arg).length > 0; + } + + /** + * Check if a string represents a valid url + * @return boolean + */ + function isUrl(string) { + var regexp = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; + return regexp.test(string); + } + + /** + * Transform a json object into html representation + * @return string + */ + function json2html(json, options) { + var html = ''; + if (typeof json === 'string') { + /* Escape tags */ + json = json.replace(/&/g, '&').replace(//g, '>'); + if (isUrl(json)) + html += '' + json + ''; + else + html += '"' + json + '"'; + } + else if (typeof json === 'number') { + html += '' + json + ''; + } + else if (typeof json === 'boolean') { + html += '' + json + ''; + } + else if (json === null) { + html += 'null'; + } + else if (json instanceof Array) { + if (json.length > 0) { + html += '[
    '; + for (var i = 0; i < json.length; ++i) { + html += '
  1. '; + /* Add toggle button if item is collapsable */ + if (isCollapsable(json[i])) { + html += ''; + } + html += json2html(json[i], options); + /* Add comma if item is not last */ + if (i < json.length - 1) { + html += ','; + } + html += '
  2. '; + } + html += '
]'; + } + else { + html += '[]'; + } + } + else if (typeof json === 'object') { + var key_count = Object.keys(json).length; + if (key_count > 0) { + html += '{}'; + } + else { + html += '{}'; + } + } + return html; + } + + /** + * jQuery plugin method + * @param json: a javascript object + * @param options: an optional options hash + */ + $.fn.jsonViewer = function(json, options) { + options = options || {}; + + /* jQuery chaining */ + return this.each(function() { + + /* Transform to HTML */ + var html = json2html(json, options); + if (isCollapsable(json)) + html = '' + html; + + /* Insert HTML in target DOM element */ + $(this).html(html); + + /* Bind click on toggle buttons */ + $(this).off('click'); + $(this).on('click', 'a.json-toggle', function() { + var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array'); + target.toggle(); + if (target.is(':visible')) { + target.siblings('.json-placeholder').remove(); + } + else { + var count = target.children('li').length; + var placeholder = count + (count > 1 ? ' items' : ' item'); + target.after('' + placeholder + ''); + } + return false; + }); + + /* Simulate click on toggle button when placeholder is clicked */ + $(this).on('click', 'a.json-placeholder', function() { + $(this).siblings('a.json-toggle').click(); + return false; + }); + + if (options.collapsed == true) { + /* Trigger click to collapse all nodes */ + $(this).find('a.json-toggle').click(); + } + }); + }; +})(jQuery); diff --git a/docs/kafkatry-ruby/app/assets/javascripts/jquery.suggestions.min.js b/docs/kafkatry-ruby/app/assets/javascripts/jquery.suggestions.min.js new file mode 100644 index 0000000..99fabcb --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/javascripts/jquery.suggestions.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],t):t(e.jQuery)}(this,function(e){"use strict";function t(e,t){return function(n,i){var s,o=[];return t(i)&&(s=k.splitTokens(k.split(n,e)),m.each(i,function(t,i){var r=t.value;if(k.stringEncloses(n,r))return!1;var a=k.splitTokens(k.split(r,e));0===m.minus(s,a).length&&o.push(i)})),1===o.length?o[0]:-1}}function n(e,t){var n=e.data&&e.data[t];return n&&new RegExp("^"+k.escapeRegExChars(n)+"(["+w+"]|$)","i").test(e.value)}function i(e,t){var n=//;return n.test(t)&&!n.test(e)?t:e}function s(e,t,n,s,o){var r=this;return i(r.highlightMatches(e,n,s,o),r.highlightMatches(t,n,s,o))}function o(e){this.urlSuffix=e.toLowerCase(),this.noSuggestionsHint="Неизвестное значение",this.matchers=[F.matchByNormalizedQuery(),F.matchByWords()]}function r(t,n){var i=this;i.element=t,i.el=e(t),i.suggestions=[],i.badQueries=[],i.selectedIndex=-1,i.currentValue=i.element.value,i.intervalId=0,i.cachedResponse={},i.enrichmentCache={},i.currentRequest=null,i.inputPhase=e.Deferred(),i.fetchPhase=e.Deferred(),i.enrichPhase=e.Deferred(),i.onChangeTimeout=null,i.triggering={},i.$wrapper=null,i.options=e.extend({},L,n),i.classes=x,i.disabled=!1,i.selection=null,i.$viewport=e(window),i.$body=e(document.body),i.type=null,i.status={},i.setupElement(),i.initializer=e.Deferred(),i.el.is(":visible")?i.initializer.resolve():i.deferInitialization(),i.initializer.done(e.proxy(i.initialize,i))}function a(){V.each(K,function(e){e.abort()}),K={}}function u(){J=null,L.geoLocation=X}function l(t){return e.map(t,function(e){var t=V.escapeHtml(e.text);return t&&e.matched&&(t=""+t+""),t}).join("")}function c(t,n){var i=t.split(", ");return 1===i.length?t:e.map(i,function(e){return''+e+""}).join(", ")}function d(t,n){var i=!1;return e.each(t,function(e,t){if(i=t.value==n.value&&t!=n)return!1}),i}function f(e,t){var n=t.selection,i=n&&n.data&&t.bounds;return i&&m.each(t.bounds.all,function(t,s){return i=n.data[t]===e.data[t]}),i}function p(e){var t=e.replace(/^(\d{2})(\d*?)(0+)$/g,"$1$2"),n=t.length,i=-1;return n<=2?i=2:n>2&&n<=5?i=5:n>5&&n<=8?i=8:n>8&&n<=11?i=11:n>11&&n<=15?i=15:n>15&&(i=19),k.padEnd(t,i,"0")}function g(e){this.plan=e.status.plan;var t=e.getContainer();this.element=de.selectByClass(x.promo,t)}function h(){new g(this).show()}e=e&&e.hasOwnProperty("default")?e.default:e;var y={isArray:function(e){return Array.isArray(e)},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isEmptyObject:function(e){return 0===Object.keys(e).length&&e.constructor===Object},isPlainObject:function(e){return void 0!==e&&"object"==typeof e&&null!==e&&!e.nodeType&&e!==e.window&&!(e.constructor&&!Object.prototype.hasOwnProperty.call(e.constructor.prototype,"isPrototypeOf"))}},m={compact:function(e){return e.filter(function(e){return!!e})},each:function(e,t){if(Array.isArray(e))return void e.some(function(e,n){return!1===t(e,n)});Object.keys(e).some(function(n){var i=e[n];return!1===t(i,n)})},intersect:function(e,t){var n=[];return Array.isArray(e)&&Array.isArray(t)?e.filter(function(e){return-1!==t.indexOf(e)}):n},minus:function(e,t){return t&&0!==t.length?e.filter(function(e){return-1===t.indexOf(e)}):e},makeArray:function(e){return y.isArray(e)?Array.prototype.slice.call(e):[e]},minusWithPartialMatching:function(e,t){return t&&0!==t.length?e.filter(function(e){return!t.some(function(t){return 0===t.indexOf(e)})}):e},slice:function(e,t){return Array.prototype.slice.call(e,t)}},_={delay:function(e,t){return setTimeout(e,t||0)}},v={areSame:function e(t,n){var i=!0;return typeof t==typeof n&&("object"==typeof t&&null!=t&&null!=n?(m.each(t,function(t,s){return i=e(t,n[s])}),i):t===n)},assign:function(e,t){if("function"==typeof Object.assign)return Object.assign.apply(null,arguments);if(null==e)throw new TypeError("Cannot convert undefined or null to object");for(var n=Object(e),i=1;i№",C=new RegExp("["+w+"]+","g"),E=new RegExp("[\\-\\+\\\\\\?!@#$%^&]+","g"),k={escapeHtml:function(e){var t={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return e&&m.each(t,function(t,n){e=e.replace(new RegExp(n,"g"),t)}),e},escapeRegExChars:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},formatToken:function(e){return e&&e.toLowerCase().replace(/[ёЁ]/g,"е")},getWordExtractorRegExp:function(){return new RegExp("([^"+w+"]*)(["+w+"]*)","g")},normalize:function(e,t){return k.split(e,t).join(" ")},padEnd:function(e,t,n){return String.prototype.padEnd?e.padEnd(t,n):(t>>=0,n=String(void 0!==n?n:" "),e.length>t?String(e):(t-=e.length,t>n.length&&(n+=n.repeat(t/n.length)),String(e)+n.slice(0,t)))},split:function(e,t){var n=e.toLowerCase().replace("ё","е").replace(/(\d+)([а-я]{2,})/g,"$1 $2").replace(/([а-я]+)(\d+)/g,"$1 $2"),i=m.compact(n.split(C));if(!i.length)return[];var s=i.pop(),o=m.minus(i,t);return o.push(s),o},splitTokens:function(e){var t=[];return m.each(e,function(e,n){var i=e.split(E);t=t.concat(m.compact(i))}),t},stringEncloses:function(e,t){return e.length>t.length&&-1!==e.toLowerCase().indexOf(t.toLowerCase())},tokenize:function(e,t){var n=m.compact(k.formatToken(e).split(C)),i=m.minus(n,t),s=m.minus(n,i);return n=k.withSubTokens(i.concat(s))},withSubTokens:function(e){var t=[];return m.each(e,function(e,n){var i=e.split(E);t.push(e),i.length>1&&(t=t.concat(m.compact(i)))}),t}},B={Deferred:function(){return e.Deferred()},ajax:function(t){return e.ajax(t)},extend:function(){return e.extend.apply(null,arguments)},isJqObject:function(t){return t instanceof e},param:function(t){return e.param(t)},proxy:function(t,n){return e.proxy(t,n)},select:function(t){return e(t)},supportsCors:function(){return e.support.cors}},T={getDefaultType:function(){return B.supportsCors()?"POST":"GET"},getDefaultContentType:function(){return B.supportsCors()?"application/json":"application/x-www-form-urlencoded"},fixURLProtocol:function(e){return B.supportsCors()?e:e.replace(/^https?:/,location.protocol)},addUrlParams:function(e,t){return e+(/\?/.test(e)?"&":"?")+B.param(t)},serialize:function(e){return B.supportsCors()?JSON.stringify(e,function(e,t){return null===t?void 0:t}):(e=v.compact(e),B.param(e,!0))}},j=function(){var e=0;return function(t){return(t||"")+ ++e}}(),V={escapeRegExChars:k.escapeRegExChars,escapeHtml:k.escapeHtml,formatToken:k.formatToken,normalize:k.normalize,reWordExtractor:k.getWordExtractorRegExp,stringEncloses:k.stringEncloses,addUrlParams:T.addUrlParams,getDefaultContentType:T.getDefaultContentType,getDefaultType:T.getDefaultType,fixURLProtocol:T.fixURLProtocol,serialize:T.serialize,arrayMinus:m.minus,arrayMinusWithPartialMatching:m.minusWithPartialMatching,arraysIntersection:m.intersect,compact:m.compact,each:m.each,makeArray:m.makeArray,slice:m.slice,delay:_.delay,areSame:v.areSame,compactObject:v.compact,getDeepValue:v.getDeepValue,fieldsNotEmpty:v.fieldsAreNotEmpty,indexBy:v.indexObjectsById,isArray:y.isArray,isEmptyObject:y.isEmptyObject,isFunction:y.isFunction,isPlainObject:y.isPlainObject,uniqueId:j},L={$helpers:null,autoSelectFirst:!1,containerClass:"suggestions-suggestions",count:5,deferRequestBy:100,enrichmentEnabled:!0,formatResult:null,formatSelected:null,headers:null,hint:"Выберите вариант или продолжите ввод",initializeInterval:100,language:null,minChars:1,mobileWidth:600,noCache:!1,noSuggestionsHint:null,onInvalidateSelection:null,onSearchComplete:e.noop,onSearchError:e.noop,onSearchStart:e.noop,onSelect:null,onSelectNothing:null,onSuggestionsFetch:null,paramName:"query",params:{},preventBadQueries:!1,requestMode:"suggest",scrollOnFocus:!1,serviceUrl:"https://suggestions.dadata.ru/suggestions/api/4_1/rs",tabDisabled:!1,timeout:3e3,triggerSelectOnBlur:!0,triggerSelectOnEnter:!0,triggerSelectOnSpace:!1,type:null,url:null},O=function(e){return function(t){if(0===t.length)return!1;if(1===t.length)return!0;var n=e(t[0].value);return 0===t.filter(function(t){return 0!==e(t.value).indexOf(n)}).length}}(function(e){return e}),F={matchByNormalizedQuery:function(e){return function(t,n){var i=k.normalize(t,e),s=[];return m.each(n,function(n,o){var r=n.value.toLowerCase();return!k.stringEncloses(t,r)&&(!(r.indexOf(i)>0)&&void(i===k.normalize(r,e)&&s.push(o)))}),1===s.length?s[0]:-1}},matchByWords:function(e){return t(e,O)},matchByWordsAddress:function(e){return t(e,O)},matchByFields:function(e){return function(t,n){var i=k.splitTokens(k.split(t)),s=[];return 1===n.length&&(e&&m.each(e,function(e,t){var i=v.getDeepValue(n[0],t),o=i&&k.splitTokens(k.split(i,e));o&&o.length&&(s=s.concat(o))}),0===m.minusWithPartialMatching(i,s).length)?0:-1}}},P=["ао","аобл","дом","респ","а/я","аал","автодорога","аллея","арбан","аул","б-р","берег","бугор","вал","вл","волость","въезд","высел","г","городок","гск","д","двлд","днп","дор","дп","ж/д_будка","ж/д_казарм","ж/д_оп","ж/д_платф","ж/д_пост","ж/д_рзд","ж/д_ст","жилзона","жилрайон","жт","заезд","заимка","зона","к","казарма","канал","кв","кв-л","км","кольцо","комн","кордон","коса","кп","край","линия","лпх","м","массив","местность","мкр","мост","н/п","наб","нп","обл","округ","остров","оф","п","п/о","п/р","п/ст","парк","пгт","пер","переезд","пл","пл-ка","платф","погост","полустанок","починок","пр-кт","проезд","промзона","просек","просека","проселок","проток","протока","проулок","р-н","рзд","россия","рп","ряды","с","с/а","с/мо","с/о","с/п","с/с","сад","сквер","сл","снт","спуск","ст","ст-ца","стр","тер","тракт","туп","у","ул","уч-к","ф/х","ферма","х","ш","бульвар","владение","выселки","гаражно-строительный","город","деревня","домовладение","дорога","квартал","километр","комната","корпус","литер","леспромхоз","местечко","микрорайон","набережная","область","переулок","платформа","площадка","площадь","поселение","поселок","проспект","разъезд","район","республика","село","сельсовет","слобода","сооружение","станица","станция","строение","территория","тупик","улица","улус","участок","хутор","шоссе"],D=[{id:"kladr_id",fields:["kladr_id"],forBounds:!1,forLocations:!0},{id:"postal_code",fields:["postal_code"],forBounds:!1,forLocations:!0},{id:"country_iso_code",fields:["country_iso_code"],forBounds:!1,forLocations:!0},{id:"country",fields:["country"],forBounds:!0,forLocations:!0,kladrFormat:{digits:0,zeros:13},fiasType:"country_iso_code"},{id:"region_iso_code",fields:["region_iso_code"],forBounds:!1,forLocations:!0},{id:"region_fias_id",fields:["region_fias_id"],forBounds:!1,forLocations:!0},{id:"region_type_full",fields:["region_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:2,zeros:11},fiasType:"region_fias_id"},{id:"region",fields:["region","region_type","region_type_full","region_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:2,zeros:11},fiasType:"region_fias_id"},{id:"area_fias_id",fields:["area_fias_id"],forBounds:!1,forLocations:!0},{id:"area_type_full",fields:["area_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:5,zeros:8},fiasType:"area_fias_id"},{id:"area",fields:["area","area_type","area_type_full","area_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:5,zeros:8},fiasType:"area_fias_id"},{id:"city_fias_id",fields:["city_fias_id"],forBounds:!1,forLocations:!0},{id:"city_type_full",fields:["city_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:8,zeros:5},fiasType:"city_fias_id"},{id:"city",fields:["city","city_type","city_type_full","city_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:8,zeros:5},fiasType:"city_fias_id"},{id:"city_district_fias_id",fields:["city_district_fias_id"],forBounds:!1,forLocations:!0},{id:"city_district_type_full",fields:["city_district_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"city_district_fias_id"},{id:"city_district",fields:["city_district","city_district_type","city_district_type_full","city_district_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"city_district_fias_id"},{id:"settlement_fias_id",fields:["settlement_fias_id"],forBounds:!1,forLocations:!0},{id:"settlement_type_full",fields:["settlement_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"settlement_fias_id"},{id:"settlement",fields:["settlement","settlement_type","settlement_type_full","settlement_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"settlement_fias_id"},{id:"street_fias_id",fields:["street_fias_id"],forBounds:!1,forLocations:!0},{id:"street_type_full",fields:["street_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"street_fias_id"},{id:"street",fields:["street","street_type","street_type_full","street_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"street_fias_id"},{id:"house",fields:["house","house_type","house_type_full","block","block_type"],forBounds:!0,forLocations:!1,kladrFormat:{digits:19}}],I={urlSuffix:"address",noSuggestionsHint:"Неизвестный адрес",matchers:[F.matchByNormalizedQuery(P),F.matchByWordsAddress(P)],dataComponents:D,dataComponentsById:v.indexObjectsById(D,"id","index"),unformattableTokens:P,enrichmentEnabled:!0,enrichmentMethod:"suggest",enrichmentParams:{count:1,locations:null,locations_boost:null,from_bound:null,to_bound:null},getEnrichmentQuery:function(e){return e.unrestricted_value},geoEnabled:!0,isDataComplete:function(e){var t=[this.bounds.to||"flat"],n=e.data;return!y.isPlainObject(n)||v.fieldsAreNotEmpty(n,t)},composeValue:function(e,t){var n=e.country,i=e.region_with_type||m.compact([e.region,e.region_type]).join(" ")||e.region_type_full,s=e.area_with_type||m.compact([e.area_type,e.area]).join(" ")||e.area_type_full,o=e.city_with_type||m.compact([e.city_type,e.city]).join(" ")||e.city_type_full,r=e.settlement_with_type||m.compact([e.settlement_type,e.settlement]).join(" ")||e.settlement_type_full,a=e.city_district_with_type||m.compact([e.city_district_type,e.city_district]).join(" ")||e.city_district_type_full,u=e.street_with_type||m.compact([e.street_type,e.street]).join(" ")||e.street_type_full,l=m.compact([e.house_type,e.house,e.block_type,e.block]).join(" "),c=m.compact([e.flat_type,e.flat]).join(" "),d=e.postal_box&&"а/я "+e.postal_box;return i===o&&(i=""),t&&t.saveCityDistrict||(t&&t.excludeCityDistrict?a="":a&&!e.city_district_fias_id&&(a="")),m.compact([n,i,s,o,a,r,u,l,c,d]).join(", ")},formatResult:function(){var e=[],t=!1;return D.forEach(function(n){t&&e.push(n.id),"city_district"===n.id&&(t=!0)}),function(t,n,i,s){var o,r,a,u=this,l=i.data&&i.data.city_district_with_type,c=s&&s.unformattableTokens,d=i.data&&i.data.history_values;return d&&d.length>0&&(o=k.tokenize(n,c),r=this.type.findUnusedTokens(o,t),(a=this.type.getFormattedHistoryValues(r,d))&&(t+=a)),t=u.highlightMatches(t,n,i,s),t=u.wrapFormattedValue(t,i),l&&(!u.bounds.own.length||u.bounds.own.indexOf("street")>=0)&&!y.isEmptyObject(u.copyDataComponents(i.data,e))&&(t+='
'+u.highlightMatches(l,n,i)+"
"),t}}(),findUnusedTokens:function(e,t){return e.filter(function(e){return-1===t.indexOf(e)})},getFormattedHistoryValues:function(e,t){var n=[],i="";return t.forEach(function(t){m.each(e,function(e){if(t.toLowerCase().indexOf(e)>=0)return n.push(t),!1})}),n.length>0&&(i=" (бывш. "+n.join(", ")+")"),i},getSuggestionValue:function(e,t){var n=null;return t.hasSameValues?n=e.options.restrict_value?this.getValueWithinConstraints(e,t.suggestion):e.bounds.own.length?this.getValueWithinBounds(e,t.suggestion):t.suggestion.unrestricted_value:t.hasBeenEnriched&&e.options.restrict_value&&(n=this.getValueWithinConstraints(e,t.suggestion,{excludeCityDistrict:!0})),n},getValueWithinConstraints:function(e,t,n){return this.composeValue(e.getUnrestrictedData(t.data),n)},getValueWithinBounds:function(e,t,n){var i=e.copyDataComponents(t.data,e.bounds.own.concat(["city_district_fias_id"]));return this.composeValue(i,n)}},q=[{id:"kladr_id",fields:["kladr_id"],forBounds:!1,forLocations:!0},{id:"region_fias_id",fields:["region_fias_id"],forBounds:!1,forLocations:!0},{id:"region_type_full",fields:["region_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:2,zeros:11},fiasType:"region_fias_id"},{id:"region",fields:["region","region_type","region_type_full","region_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:2,zeros:11},fiasType:"region_fias_id"},{id:"area_fias_id",fields:["area_fias_id"],forBounds:!1,forLocations:!0},{id:"area_type_full",fields:["area_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:5,zeros:8},fiasType:"area_fias_id"},{id:"area",fields:["area","area_type","area_type_full","area_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:5,zeros:8},fiasType:"area_fias_id"},{id:"city_fias_id",fields:["city_fias_id"],forBounds:!1,forLocations:!0},{id:"city_type_full",fields:["city_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:8,zeros:5},fiasType:"city_fias_id"},{id:"city",fields:["city","city_type","city_type_full","city_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:8,zeros:5},fiasType:"city_fias_id"},{id:"city_district_fias_id",fields:["city_district_fias_id"],forBounds:!1,forLocations:!0},{id:"city_district_type_full",fields:["city_district_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"city_district_fias_id"},{id:"city_district",fields:["city_district","city_district_type","city_district_type_full","city_district_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"city_district_fias_id"},{id:"settlement_fias_id",fields:["settlement_fias_id"],forBounds:!1,forLocations:!0},{id:"settlement_type_full",fields:["settlement_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"settlement_fias_id"},{id:"settlement",fields:["settlement","settlement_type","settlement_type_full","settlement_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"settlement_fias_id"},{id:"planning_structure_fias_id",fields:["planning_structure_fias_id"],forBounds:!1,forLocations:!0},{id:"planning_structure_type_full",fields:["planning_structure_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"planning_structure_fias_id"},{id:"planning_structure",fields:["planning_structure","planning_structure_type","planning_structure_type_full","planning_structure_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"planning_structure_fias_id"},{id:"street_fias_id",fields:["street_fias_id"],forBounds:!1,forLocations:!0},{id:"street_type_full",fields:["street_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"street_fias_id"},{id:"street",fields:["street","street_type","street_type_full","street_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"street_fias_id"},{id:"house",fields:["house","house_type","block","building_type","building"],forBounds:!0,forLocations:!1,kladrFormat:{digits:19}}],z={urlSuffix:"fias",noSuggestionsHint:"Неизвестный адрес",matchers:[F.matchByNormalizedQuery(P),F.matchByWordsAddress(P)],dataComponents:q,dataComponentsById:v.indexObjectsById(q,"id","index"),unformattableTokens:P,isDataComplete:function(e){var t=[this.bounds.to||"house"],n=e.data;return!y.isPlainObject(n)||v.fieldsAreNotEmpty(n,t)},composeValue:function(e,t){var n=e.country,i=e.region_with_type||m.compact([e.region,e.region_type]).join(" ")||e.region_type_full,s=e.area_with_type||m.compact([e.area_type,e.area]).join(" ")||e.area_type_full,o=e.city_with_type||m.compact([e.city_type,e.city]).join(" ")||e.city_type_full,r=e.settlement_with_type||m.compact([e.settlement_type,e.settlement]).join(" ")||e.settlement_type_full,a=e.city_district_with_type||m.compact([e.city_district_type,e.city_district]).join(" ")||e.city_district_type_full,u=e.planning_structure_with_type||m.compact([e.planning_structure_type,e.planning_structure]).join(" ")||e.planning_structure_type_full,l=e.street_with_type||m.compact([e.street_type,e.street]).join(" ")||e.street_type_full,c=m.compact([e.house_type,e.house,e.block_type,e.block]).join(" "),d=m.compact([e.flat_type,e.flat]).join(" "),f=e.postal_box&&"а/я "+e.postal_box;return i===o&&(i=""),t&&t.saveCityDistrict||(t&&t.excludeCityDistrict?a="":a&&!e.city_district_fias_id&&(a="")),m.compact([n,i,s,o,a,r,u,l,c,d,f]).join(", ")},formatResult:function(){return function(e,t,n,i){var s=this;return e=s.highlightMatches(e,t,n,i),e=s.wrapFormattedValue(e,n)}}(),getSuggestionValue:I.getSuggestionValue,getValueWithinConstraints:I.getValueWithinConstraints,getValueWithinBounds:I.getValueWithinBounds},R={urlSuffix:"fio",noSuggestionsHint:!1,matchers:[F.matchByNormalizedQuery(),F.matchByWords()],fieldNames:{surname:"фамилия",name:"имя",patronymic:"отчество"},isDataComplete:function(e){var t,i=this,s=i.options.params,o=e.data;return y.isFunction(s)&&(s=s.call(i.element,e.value)),s&&s.parts?t=s.parts.map(function(e){return e.toLowerCase()}):(t=["surname","name"],n(e,"surname")&&t.push("patronymic")),v.fieldsAreNotEmpty(o,t)},composeValue:function(e){return m.compact([e.surname,e.name,e.patronymic]).join(" ")}},A={LEGAL:[2,2,5,1],INDIVIDUAL:[2,2,6,2]},$={urlSuffix:"party",noSuggestionsHint:"Неизвестная организация",matchers:[F.matchByFields({value:null,"data.address.value":P,"data.inn":null,"data.ogrn":null})],dataComponents:D,enrichmentEnabled:!0,enrichmentMethod:"findById",enrichmentParams:{count:1,locations_boost:null},getEnrichmentQuery:function(e){return e.data.hid},geoEnabled:!0,formatResult:function(e,t,n,o){var r=this,a=r.type.formatResultInn.call(r,n,t),u=r.highlightMatches(v.getDeepValue(n.data,"ogrn"),t,n),l=i(a,u),c=r.highlightMatches(v.getDeepValue(n.data,"management.name"),t,n),d=v.getDeepValue(n.data,"address.value")||"";return r.isMobile&&((o||(o={})).maxLength=50),e=s.call(r,e,v.getDeepValue(n.data,"name.latin"),t,n,o),e=r.wrapFormattedValue(e,n),d&&(d=d.replace(/^(\d{6}|Россия),\s+/i,""),d=r.isMobile?d.replace(new RegExp("^([^"+w+"]+["+w+"]+[^"+w+"]+).*"),"$1"):r.highlightMatches(d,t,n,{unformattableTokens:P})),(l||d||c)&&(e+='
'+(l||"")+""+(i(d,c)||"")+"
"),e},formatResultInn:function(e,t){var n,i,s=this,o=e.data&&e.data.inn,r=A[e.data&&e.data.type],a=/\d/;if(o)return i=s.highlightMatches(o,t,e),r&&(i=i.split(""),n=r.map(function(e){for(var t,n="";e&&(t=i.shift());)n+=t,a.test(t)&&e--;return n}),i=n.join('')+i.join("")),i}},N={urlSuffix:"email",noSuggestionsHint:!1,matchers:[F.matchByNormalizedQuery()],isQueryRequestable:function(e){return this.options.suggest_local||e.indexOf("@")>=0}},M={urlSuffix:"bank",noSuggestionsHint:"Неизвестный банк",matchers:[F.matchByFields({value:null,"data.bic":null,"data.swift":null})],dataComponents:D,geoEnabled:!0,formatResult:function(e,t,n,i){var s=this,o=s.highlightMatches(v.getDeepValue(n.data,"bic"),t,n),r=v.getDeepValue(n.data,"address.value")||"";return e=s.highlightMatches(e,t,n,i),e=s.wrapFormattedValue(e,n),r&&(r=r.replace(/^\d{6}( РОССИЯ)?, /i,""),r=s.isMobile?r.replace(new RegExp("^([^"+w+"]+["+w+"]+[^"+w+"]+).*"),"$1"):s.highlightMatches(r,t,n,{unformattableTokens:P})),(o||r)&&(e+='
'+o+""+r+"
"),e},formatSelected:function(e){return v.getDeepValue(e,"data.name.payment")||null}},W={NAME:R,ADDRESS:I,FIAS:z,PARTY:$,EMAIL:N,BANK:M};W.get=function(e){return W.hasOwnProperty(e)?W[e]:new o(e)},B.extend(L,{suggest_local:!0});var U={chains:{},on:function(e,t){return this.get(e).push(t),this},get:function(e){var t=this.chains;return t[e]||(t[e]=[])}},H={suggest:{defaultParams:{type:V.getDefaultType(),dataType:"json",contentType:V.getDefaultContentType()},addTypeInUrl:!0},"iplocate/address":{defaultParams:{type:"GET",dataType:"json"},addTypeInUrl:!1},status:{defaultParams:{type:"GET",dataType:"json"},addTypeInUrl:!0},findById:{defaultParams:{type:V.getDefaultType(),dataType:"json",contentType:V.getDefaultContentType()},addTypeInUrl:!0}},Q={suggest:{method:"suggest",userSelect:!0,updateValue:!0,enrichmentEnabled:!0},findById:{method:"findById",userSelect:!1,updateValue:!1,enrichmentEnabled:!1}};r.prototype={initialize:function(){var e=this;e.uniqueId=V.uniqueId("i"),e.createWrapper(),e.notify("initialize"),e.bindWindowEvents(),e.setOptions(),e.inferIsMobile(),e.notify("ready")},deferInitialization:function(){var e,t=this,n="mouseover focus keydown",i=function(){t.initializer.resolve(),t.enable()};t.initializer.always(function(){t.el.off(n,i),clearInterval(e)}),t.disabled=!0,t.el.on(n,i),e=setInterval(function(){t.el.is(":visible")&&i()},t.options.initializeInterval)},isInitialized:function(){return"resolved"===this.initializer.state()},dispose:function(){var e=this;e.initializer.reject(),e.notify("dispose"),e.el.removeData("suggestions").removeClass("suggestions-input"),e.unbindWindowEvents(),e.removeWrapper(),e.el.trigger("suggestions-dispose")},notify:function(t){var n=this,i=V.slice(arguments,1);return e.map(U.get(t),function(e){return e.apply(n,i)})},createWrapper:function(){var t=this;t.$wrapper=e('
'),t.el.after(t.$wrapper),t.$wrapper.on("mousedown"+S,e.proxy(t.onMousedown,t))},removeWrapper:function(){var t=this;t.$wrapper&&t.$wrapper.remove(),e(t.options.$helpers).off(S)},onMousedown:function(t){var n=this;t.preventDefault(),n.cancelBlur=!0,V.delay(function(){delete n.cancelBlur}),0==e(t.target).closest(".ui-menu-item").length&&V.delay(function(){e(document).one("mousedown",function(t){var i=n.el.add(n.$wrapper).add(n.options.$helpers);n.options.floating&&(i=i.add(n.$container)),i=i.filter(function(){return this===t.target||e.contains(this,t.target)}),i.length||n.hide()})})},bindWindowEvents:function(){var t=e.proxy(this.inferIsMobile,this);this.$viewport.on("resize"+S+this.uniqueId,t)},unbindWindowEvents:function(){this.$viewport.off("resize"+S+this.uniqueId)},scrollToTop:function(){var t=this,n=t.options.scrollOnFocus;!0===n&&(n=t.el),n instanceof e&&n.length>0&&e("body,html").animate({scrollTop:n.offset().top},"fast")},setOptions:function(t){var n=this;e.extend(n.options,t),n.type=W.get(n.options.type),e.each({requestMode:Q},function(t,i){if(n[t]=i[n.options[t]],!n[t])throw n.disable(),"`"+t+"` option is incorrect! Must be one of: "+e.map(i,function(e,t){return'"'+t+'"'}).join(", ")}),e(n.options.$helpers).off(S).on("mousedown"+S,e.proxy(n.onMousedown,n)),n.isInitialized()&&n.notify("setOptions")},inferIsMobile:function(){this.isMobile=this.$viewport.width()<=this.options.mobileWidth},clearCache:function(){this.cachedResponse={},this.enrichmentCache={},this.badQueries=[]},clear:function(){var e=this,t=e.selection;e.isInitialized()&&(e.clearCache(),e.currentValue="",e.selection=null,e.hide(),e.suggestions=[],e.el.val(""),e.el.trigger("suggestions-clear"),e.notify("clear"),e.trigger("InvalidateSelection",t))},disable:function(){var e=this;e.disabled=!0,e.abortRequest(),e.visible&&e.hide()},enable:function(){this.disabled=!1},isUnavailable:function(){return this.disabled},update:function(){var e=this,t=e.el.val();e.isInitialized()&&(e.currentValue=t,e.isQueryRequestable(t)?e.updateSuggestions(t):e.hide())},setSuggestion:function(t){var n,i,s=this;e.isPlainObject(t)&&e.isPlainObject(t.data)&&(t=e.extend(!0,{},t),s.isUnavailable()&&s.initializer&&"pending"===s.initializer.state()&&(s.initializer.resolve(),s.enable()),s.bounds.own.length&&(s.checkValueBounds(t),n=s.copyDataComponents(t.data,s.bounds.all),t.data.kladr_id&&(n.kladr_id=s.getBoundedKladrId(t.data.kladr_id,s.bounds.all)),t.data=n),s.selection=t,s.suggestions=[t],i=s.getSuggestionValue(t)||"",s.currentValue=i,s.el.val(i),s.abortRequest(),s.el.trigger("suggestions-set"))},fixData:function(){var t=this,n=t.extendedCurrentValue(),i=t.el.val(),s=e.Deferred();s.done(function(e){t.selectSuggestion(e,0,i,{hasBeenEnriched:!0}),t.el.trigger("suggestions-fixdata",e)}).fail(function(){t.selection=null,t.el.trigger("suggestions-fixdata")}),t.isQueryRequestable(n)?(t.currentValue=n,t.getSuggestions(n,{count:1,from_bound:null,to_bound:null}).done(function(e){var t=e[0];t?s.resolve(t):s.reject()}).fail(function(){s.reject()})):s.reject()},extendedCurrentValue:function(){var t=this,n=t.getParentInstance(),i=n&&n.extendedCurrentValue(),s=e.trim(t.el.val());return V.compact([i,s]).join(" ")},getAjaxParams:function(t,n){var i=this,s=e.trim(i.options.token),o=e.trim(i.options.partner),a=i.options.serviceUrl,u=i.options.url,l=H[t],c=e.extend({timeout:i.options.timeout},l.defaultParams),d={};return u?a=u:(/\/$/.test(a)||(a+="/"),a+=t,l.addTypeInUrl&&(a+="/"+i.type.urlSuffix)),a=V.fixURLProtocol(a),e.support.cors?(s&&(d.Authorization="Token "+s),o&&(d["X-Partner"]=o),d["X-Version"]=r.version,c.headers||(c.headers={}),c.xhrFields||(c.xhrFields={}),e.extend(c.headers,i.options.headers,d),c.xhrFields.withCredentials=!1):(s&&(d.token=s),o&&(d.partner=o),d.version=r.version,a=V.addUrlParams(a,d)),c.url=a,e.extend(c,n)},isQueryRequestable:function(e){var t,n=this;return t=e.length>=n.options.minChars,t&&n.type.isQueryRequestable&&(t=n.type.isQueryRequestable.call(n,e)),t},constructRequestParams:function(t,n){var i=this,s=i.options,o=e.isFunction(s.params)?s.params.call(i.element,t):e.extend({},s.params);return i.type.constructRequestParams&&e.extend(o,i.type.constructRequestParams.call(i)),e.each(i.notify("requestParams"),function(t,n){e.extend(o,n)}),o[s.paramName]=t,e.isNumeric(s.count)&&s.count>0&&(o.count=s.count),s.language&&(o.language=s.language),e.extend(o,n)},updateSuggestions:function(e){var t=this;t.fetchPhase=t.getSuggestions(e).done(function(n){t.assignSuggestions(n,e)})},getSuggestions:function(t,n,i){var s,o=this,r=o.options,a=i&&i.noCallbacks,u=i&&i.useEnrichmentCache,l=i&&i.method||o.requestMode.method,c=o.constructRequestParams(t,n),d=e.param(c||{}),f=e.Deferred();return s=o.cachedResponse[d],s&&e.isArray(s.suggestions)?f.resolve(s.suggestions):o.isBadQuery(t)?f.reject():a||!1!==r.onSearchStart.call(o.element,c)?o.doGetSuggestions(c,l).done(function(e){o.processResponse(e)&&t==o.currentValue?(r.noCache||(u?o.enrichmentCache[t]=e.suggestions[0]:(o.enrichResponse(e,t),o.cachedResponse[d]=e,r.preventBadQueries&&0===e.suggestions.length&&o.badQueries.push(t))),f.resolve(e.suggestions)):f.reject(),a||r.onSearchComplete.call(o.element,t,e.suggestions)}).fail(function(e,n,i){f.reject(),a||"abort"===n||r.onSearchError.call(o.element,t,e,n,i)}):f.reject(),f},doGetSuggestions:function(t,n){var i=this,s=e.ajax(i.getAjaxParams(n,{data:V.serialize(t)}));return i.abortRequest(),i.currentRequest=s,i.notify("request"), + s.always(function(){i.currentRequest=null,i.notify("request")}),s},isBadQuery:function(t){if(!this.options.preventBadQueries)return!1;var n=!1;return e.each(this.badQueries,function(e,i){return!(n=0===t.indexOf(i))}),n},abortRequest:function(){var e=this;e.currentRequest&&e.currentRequest.abort()},processResponse:function(t){var n,i=this;return!(!t||!e.isArray(t.suggestions))&&(i.verifySuggestionsFormat(t.suggestions),i.setUnrestrictedValues(t.suggestions),e.isFunction(i.options.onSuggestionsFetch)&&(n=i.options.onSuggestionsFetch.call(i.element,t.suggestions),e.isArray(n)&&(t.suggestions=n)),!0)},verifySuggestionsFormat:function(t){"string"==typeof t[0]&&e.each(t,function(e,n){t[e]={value:n,data:null}})},getSuggestionValue:function(t,n){var i,s=this,o=s.options.formatSelected||s.type.formatSelected,r=n&&n.hasSameValues,a=n&&n.hasBeenEnriched,u=null;return e.isFunction(o)&&(i=o.call(s,t)),"string"!=typeof i&&(i=t.value,s.type.getSuggestionValue&&null!==(u=s.type.getSuggestionValue(s,{suggestion:t,hasSameValues:r,hasBeenEnriched:a}))&&(i=u)),i},hasSameValues:function(t){var n=!1;return e.each(this.suggestions,function(e,i){if(i.value===t.value&&i!==t)return n=!0,!1}),n},assignSuggestions:function(e,t){var n=this;n.suggestions=e,n.notify("assignSuggestions",t)},shouldRestrictValues:function(){var e=this;return e.options.restrict_value&&e.constraints&&1==Object.keys(e.constraints).length},setUnrestrictedValues:function(t){var n=this,i=n.shouldRestrictValues(),s=n.getFirstConstraintLabel();e.each(t,function(e,t){t.unrestricted_value||(t.unrestricted_value=i?s+", "+t.value:t.value)})},areSuggestionsSame:function(e,t){return e&&t&&e.value===t.value&&V.areSame(e.data,t.data)},getNoSuggestionsHint:function(){var e=this;return!1!==e.options.noSuggestionsHint&&(e.options.noSuggestionsHint||e.type.noSuggestionsHint)}};var Z={setupElement:function(){this.el.attr("autocomplete","off").attr("autocorrect","off").attr("autocapitalize","off").attr("spellcheck","false").addClass("suggestions-input").css("box-sizing","border-box")},bindElementEvents:function(){var t=this;t.el.on("keydown"+S,e.proxy(t.onElementKeyDown,t)),t.el.on(["keyup"+S,"cut"+S,"paste"+S,"input"+S].join(" "),e.proxy(t.onElementKeyUp,t)),t.el.on("blur"+S,e.proxy(t.onElementBlur,t)),t.el.on("focus"+S,e.proxy(t.onElementFocus,t))},unbindElementEvents:function(){this.el.off(S)},onElementBlur:function(){var e=this;if(e.cancelBlur)return void(e.cancelBlur=!1);e.options.triggerSelectOnBlur?e.isUnavailable()||e.selectCurrentValue({noSpace:!0}).always(function(){e.hide()}):e.hide(),e.fetchPhase.abort&&e.fetchPhase.abort()},onElementFocus:function(){var t=this;t.cancelFocus||V.delay(e.proxy(t.completeOnFocus,t)),t.cancelFocus=!1},onElementKeyDown:function(e){var t=this;if(!t.isUnavailable())if(t.visible){switch(e.which){case b.ESC:t.el.val(t.currentValue),t.hide(),t.abortRequest();break;case b.TAB:if(!1===t.options.tabDisabled)return;break;case b.ENTER:t.options.triggerSelectOnEnter&&t.selectCurrentValue();break;case b.SPACE:return void(t.options.triggerSelectOnSpace&&t.isCursorAtEnd()&&(e.preventDefault(),t.selectCurrentValue({continueSelecting:!0,dontEnrich:!0}).fail(function(){t.currentValue+=" ",t.el.val(t.currentValue),t.proceedChangedValue()})));case b.UP:t.moveUp();break;case b.DOWN:t.moveDown();break;default:return}e.stopImmediatePropagation(),e.preventDefault()}else switch(e.which){case b.DOWN:t.suggest();break;case b.ENTER:t.options.triggerSelectOnEnter&&t.triggerOnSelectNothing()}},onElementKeyUp:function(e){var t=this;if(!t.isUnavailable()){switch(e.which){case b.UP:case b.DOWN:case b.ENTER:return}clearTimeout(t.onChangeTimeout),t.inputPhase.reject(),t.currentValue!==t.el.val()&&t.proceedChangedValue()}},proceedChangedValue:function(){var t=this;t.abortRequest(),t.inputPhase=e.Deferred().done(e.proxy(t.onValueChange,t)),t.options.deferRequestBy>0?t.onChangeTimeout=V.delay(function(){t.inputPhase.resolve()},t.options.deferRequestBy):t.inputPhase.resolve()},onValueChange:function(){var e,t=this;t.selection&&(e=t.selection,t.selection=null,t.trigger("InvalidateSelection",e)),t.selectedIndex=-1,t.update(),t.notify("valueChange")},completeOnFocus:function(){var e=this;e.isUnavailable()||e.isElementFocused()&&(e.update(),e.isMobile&&(e.setCursorAtEnd(),e.scrollToTop()))},isElementFocused:function(){return document.activeElement===this.element},isElementDisabled:function(){return Boolean(this.element.getAttribute("disabled")||this.element.getAttribute("readonly"))},isCursorAtEnd:function(){var e,t,n=this,i=n.el.val().length;try{if("number"==typeof(e=n.element.selectionStart))return e===i}catch(e){}return!document.selection||(t=document.selection.createRange(),t.moveStart("character",-i),i===t.text.length)},setCursorAtEnd:function(){var e=this.element;try{e.selectionEnd=e.selectionStart=e.value.length,e.scrollLeft=e.scrollWidth}catch(t){e.value=e.value}}};e.extend(r.prototype,Z),U.on("initialize",Z.bindElementEvents).on("dispose",Z.unbindElementEvents);var K={};a();var G={checkStatus:function(){function e(e){V.isFunction(t.options.onSearchError)&&t.options.onSearchError.call(t.element,null,s,"error",e)}var t=this,n=t.options.token&&t.options.token.trim()||"",i=t.options.type+n,s=K[i];s||(s=K[i]=B.ajax(t.getAjaxParams("status"))),s.done(function(n,i,s){if(n.search){var o=s.getResponseHeader("X-Plan");n.plan=o,B.extend(t.status,n)}else e("Service Unavailable")}).fail(function(){e(s.statusText)})}};r.resetTokens=a,B.extend(r.prototype,G),U.on("setOptions",G.checkStatus);var J,X=!0,Y={checkLocation:function(){var t=this,n=t.options.geoLocation;t.type.geoEnabled&&n&&(t.geoLocation=e.Deferred(),e.isPlainObject(n)||e.isArray(n)?t.geoLocation.resolve(n):(J||(J=e.ajax(t.getAjaxParams("iplocate/address"))),J.done(function(e){var n=e&&e.location&&e.location.data;n&&n.kladr_id?t.geoLocation.resolve({kladr_id:n.kladr_id}):t.geoLocation.reject()}).fail(function(){t.geoLocation.reject()})))},getGeoLocation:function(){return this.geoLocation},constructParams:function(){var t=this,n={};return t.geoLocation&&e.isFunction(t.geoLocation.promise)&&"resolved"==t.geoLocation.state()&&t.geoLocation.done(function(t){n.locations_boost=e.makeArray(t)}),n}};"GET"!=V.getDefaultType()&&(e.extend(L,{geoLocation:X}),e.extend(r,{resetLocation:u}),e.extend(r.prototype,{getGeoLocation:Y.getGeoLocation}),U.on("setOptions",Y.checkLocation).on("requestParams",Y.constructParams));var ee={enrichSuggestion:function(t,n){var i=this,s=e.Deferred();if(!i.options.enrichmentEnabled||!i.type.enrichmentEnabled||!i.requestMode.enrichmentEnabled||n&&n.dontEnrich)return s.resolve(t);if(t.data&&null!=t.data.qc)return s.resolve(t);i.disableDropdown();var o=i.type.getEnrichmentQuery(t),r=i.type.enrichmentParams,a={noCallbacks:!0,useEnrichmentCache:!0,method:i.type.enrichmentMethod};return i.currentValue=o,i.enrichPhase=i.getSuggestions(o,r,a).always(function(){i.enableDropdown()}).done(function(e){var n=e&&e[0];s.resolve(n||t,!!n)}).fail(function(){s.resolve(t)}),s},enrichResponse:function(t,n){var i=this,s=i.enrichmentCache[n];s&&e.each(t.suggestions,function(e,i){if(i.value===n)return t.suggestions[e]=s,!1})}};e.extend(r.prototype,ee);var te={width:"auto",floating:!1},ne={createContainer:function(){var t=this,n="."+t.classes.suggestion,i=t.options,s=e("
").addClass(i.containerClass).css({display:"none"});t.$container=s,s.on("click"+S,n,e.proxy(t.onSuggestionClick,t))},showContainer:function(){this.$container.appendTo(this.options.floating?this.$body:this.$wrapper)},getContainer:function(){return this.$container.get(0)},removeContainer:function(){var e=this;e.options.floating&&e.$container.remove()},setContainerOptions:function(){var t=this;t.$container.off("mousedown.suggestions"),t.options.floating&&t.$container.on("mousedown.suggestions",e.proxy(t.onMousedown,t))},onSuggestionClick:function(t){var n,i=this,s=e(t.target);if(!i.dropdownDisabled){for(i.cancelFocus=!0,i.el.focus();s.length&&!(n=s.attr("data-index"));)s=s.closest("."+i.classes.suggestion);n&&!isNaN(n)&&i.select(+n)}},getSuggestionsItems:function(){return this.$container.children("."+this.classes.suggestion)},toggleDropdownEnabling:function(e){this.dropdownDisabled=!e,this.$container.attr("disabled",!e)},disableDropdown:function(){this.toggleDropdownEnabling(!1)},enableDropdown:function(){this.toggleDropdownEnabling(!0)},hasSuggestionsToChoose:function(){var t=this;return t.suggestions.length>1||1===t.suggestions.length&&(!t.selection||e.trim(t.suggestions[0].value)!==e.trim(t.selection.value))},suggest:function(){var t=this,n=t.options,i=[];if(t.requestMode.userSelect){if(t.hasSuggestionsToChoose())n.hint&&t.suggestions.length&&i.push('
'+n.hint+"
"),t.selectedIndex=-1,t.suggestions.forEach(function(e,n){e==t.selection&&(t.selectedIndex=n),t.buildSuggestionHtml(e,n,i)});else{if(t.suggestions.length)return void t.hide();var s=t.getNoSuggestionsHint();if(!s)return void t.hide();i.push('
'+s+"
")}i.push('
'),i.push("
"),t.$container.html(i.join("")),n.autoSelectFirst&&-1===t.selectedIndex&&(t.selectedIndex=0),-1!==t.selectedIndex&&t.getSuggestionsItems().eq(t.selectedIndex).addClass(t.classes.selected),e.isFunction(n.beforeRender)&&n.beforeRender.call(t.element,t.$container),t.$container.show(),t.visible=!0}},buildSuggestionHtml:function(e,t,n){n.push('
');var i=this.options.formatResult||this.type.formatResult||this.formatResult;n.push(i.call(this,e.value,this.currentValue,e,{unformattableTokens:this.type.unformattableTokens}));var s=this.makeSuggestionLabel(this.suggestions,e);s&&n.push(''+V.escapeHtml(s)+""),n.push("
")},wrapFormattedValue:function(e,t){var n=this,i=V.getDeepValue(t.data,"state.status");return'"+e+""},formatResult:function(e,t,n,i){var s=this;return e=s.highlightMatches(e,t,n,i),s.wrapFormattedValue(e,n)},highlightMatches:function(t,n,i,s){var o,r,a,u,d,f,p,g=this,h=[],y=s&&s.unformattableTokens,m=s&&s.maxLength,_=V.reWordExtractor();if(!t)return"";for(o=k.tokenize(n,y),r=e.map(o,function(e){return new RegExp("^((.*)([\\-\\+\\\\\\?!@#$%^&]+))?("+V.escapeRegExChars(e)+")([^\\-\\+\\\\\\?!@#$%^&]*[\\-\\+\\\\\\?!@#$%^&]*)","i")});(a=_.exec(t))&&a[0];)u=a[1],h.push({text:u,hasUpperCase:u.toLowerCase()!==u,formatted:V.formatToken(u),matchable:!0}),a[2]&&h.push({text:a[2]});for(d=0;dn&&(h.splice(s,0,{text:f.text.substr(n),formatted:f.formatted.substr(n),matchable:!0}),f.text=f.text.substr(0,n),f.formatted=f.formatted.substr(0,n)),i.after&&(n=i.text.length,h.splice(s,0,{text:f.text.substr(n),formatted:f.formatted.substr(n)}),f.text=f.text.substr(0,n),f.formatted=f.formatted.substr(0,n)),f.matched=!0,!1});if(m){for(d=0;d=0;d++)f=h[d],(m-=f.text.length)<0&&(f.text=f.text.substr(0,f.text.length+m)+"...");h.length=d}return p=l(h),c(p,g.classes.nowrap)},makeSuggestionLabel:function(t,n){var i,s,o=this,r=o.type.fieldNames,a={},u=V.reWordExtractor(),l=[];if(r&&d(t,n)&&n.data&&(e.each(r,function(e){var t=n.data[e];t&&(a[e]=V.formatToken(t))}),!e.isEmptyObject(a))){for(;(i=u.exec(V.formatToken(n.value)))&&(s=i[1]);)e.each(a,function(e,t){if(t==s)return l.push(r[e]),delete a[e],!1});if(l.length)return l.join(", ")}},hide:function(){var e=this;e.visible=!1,e.selectedIndex=-1,e.$container.hide().empty()},activate:function(e){var t,n,i=this,s=i.classes.selected;return!i.dropdownDisabled&&(n=i.getSuggestionsItems(),n.removeClass(s),i.selectedIndex=e,-1!==i.selectedIndex&&n.length>i.selectedIndex)?(t=n.eq(i.selectedIndex),t.addClass(s),t):null},deactivate:function(e){var t=this;t.dropdownDisabled||(t.selectedIndex=-1,t.getSuggestionsItems().removeClass(t.classes.selected),e&&t.el.val(t.currentValue))},moveUp:function(){var e=this;if(!e.dropdownDisabled)return-1===e.selectedIndex?void(e.suggestions.length&&e.adjustScroll(e.suggestions.length-1)):0===e.selectedIndex?void e.deactivate(!0):void e.adjustScroll(e.selectedIndex-1)},moveDown:function(){var e=this;if(!e.dropdownDisabled)return e.selectedIndex===e.suggestions.length-1?void e.deactivate(!0):void e.adjustScroll(e.selectedIndex+1)},adjustScroll:function(e){var t,n,i,s=this,o=s.activate(e),r=s.$container.scrollTop();o&&o.length&&(t=o.position().top,t<0?s.$container.scrollTop(r+t):(n=t+o.outerHeight(),i=s.$container.innerHeight(),n>i&&s.$container.scrollTop(r-i+n)),s.el.val(s.suggestions[e].value))}};e.extend(L,te),e.extend(r.prototype,ne),U.on("initialize",ne.createContainer).on("dispose",ne.removeContainer).on("setOptions",ne.setContainerOptions).on("ready",ne.showContainer).on("assignSuggestions",ne.suggest);var ie={constraints:null,restrict_value:!1},se=["country_iso_code","region_iso_code","region_fias_id","area_fias_id","city_fias_id","city_district_fias_id","settlement_fias_id","planning_structure_fias_id","street_fias_id"],oe=function(e,t){var n,i,s=this,o={};s.instance=t,s.fields={},s.specificity=-1,y.isPlainObject(e)&&t.type.dataComponents&&m.each(t.type.dataComponents,function(t,n){var i=t.id;t.forLocations&&e[i]&&(s.fields[i]=e[i],s.specificity=n)}),n=Object.keys(s.fields),i=m.intersect(n,se),i.length?(m.each(i,function(e,t){o[e]=s.fields[e]}),s.fields=o,s.specificity=s.getFiasSpecificity(i)):s.fields.kladr_id&&(s.fields={kladr_id:s.fields.kladr_id},s.significantKladr=p(s.fields.kladr_id),s.specificity=s.getKladrSpecificity(s.significantKladr))};B.extend(oe.prototype,{getLabel:function(){return this.instance.type.composeValue(this.fields,{saveCityDistrict:!0})},getFields:function(){return this.fields},isValid:function(){return!y.isEmptyObject(this.fields)},getKladrSpecificity:function(e){var t=-1,n=e.length;return m.each(this.instance.type.dataComponents,function(e,i){e.kladrFormat&&n===e.kladrFormat.digits&&(t=i)}),t},getFiasSpecificity:function(e){var t=-1;return m.each(this.instance.type.dataComponents,function(n,i){n.fiasType&&e.indexOf(n.fiasType)>-1&&t0},getFields:function(){return this.locations.map(function(e){return e.getFields()})}});var ae={createConstraints:function(){this.constraints={}},setupConstraints:function(){var e,t=this,n=t.options.constraints;if(!n)return void t.unbindFromParent();B.isJqObject(n)||"string"==typeof n||"number"==typeof n.nodeType?(e=B.select(n),e.is(t.constraints)||(t.unbindFromParent(),e.is(t.el)||(t.constraints=e,t.bindToParent()))):(m.each(t.constraints,function(e,n){t.removeConstraint(n)}),m.each(m.makeArray(n),function(e,n){t.addConstraint(e)}))},filteredLocation:function(e){var t=[],n={};if(m.each(this.type.dataComponents,function(){this.forLocations&&t.push(this.id)}),y.isPlainObject(e)&&m.each(e,function(e,i){e&&t.indexOf(i)>=0&&(n[i]=e)}),!y.isEmptyObject(n))return n.kladr_id?{kladr_id:n.kladr_id}:n},addConstraint:function(e){var t=this;e=new re(e,t),e.isValid()&&(t.constraints[e.id]=e)},removeConstraint:function(e){delete this.constraints[e]},constructConstraintsParams:function(){for(var e,t,n=this,i=[],s=n.constraints,o={};B.isJqObject(s)&&(e=s.suggestions())&&!(t=v.getDeepValue(e,"selection.data"));)s=e.constraints;return B.isJqObject(s)?(t=new oe(t,e).getFields())&&(n.bounds.own.indexOf("city")>-1&&delete t.city_fias_id,o.locations=[t],o.restrict_value=!0):s&&(m.each(s,function(e,t){i=i.concat(e.getFields())}),i.length&&(o.locations=i,o.restrict_value=n.options.restrict_value)),o},getFirstConstraintLabel:function(){var e=this,t=y.isPlainObject(e.constraints)&&Object.keys(e.constraints)[0];return t?e.constraints[t].label:""},bindToParent:function(){var e=this;e.constraints.on(["suggestions-select."+e.uniqueId,"suggestions-invalidateselection."+e.uniqueId,"suggestions-clear."+e.uniqueId].join(" "),B.proxy(e.onParentSelectionChanged,e)).on("suggestions-dispose."+e.uniqueId,B.proxy(e.onParentDispose,e))},unbindFromParent:function(){var e=this,t=e.constraints;B.isJqObject(t)&&t.off("."+e.uniqueId)},onParentSelectionChanged:function(e,t,n){("suggestions-select"!==e.type||n)&&this.clear()},onParentDispose:function(e){this.unbindFromParent()},getParentInstance:function(){return B.isJqObject(this.constraints)&&this.constraints.suggestions()},shareWithParent:function(e){var t=this.getParentInstance();t&&t.type===this.type&&!f(e,t)&&(t.shareWithParent(e),t.setSuggestion(e))},getUnrestrictedData:function(e){var t=this,n=[],i={},s=-1;return m.each(t.constraints,function(t,n){m.each(t.locations,function(t,n){t.containsData(e)&&t.specificity>s&&(s=t.specificity)})}),s>=0?(e.region_kladr_id&&e.region_kladr_id===e.city_kladr_id&&n.push.apply(n,t.type.dataComponentsById.city.fields),m.each(t.type.dataComponents.slice(0,s+1),function(e,t){n.push.apply(n,e.fields)}),m.each(e,function(e,t){-1===n.indexOf(t)&&(i[t]=e)})):i=e,i}};B.extend(L,ie),B.extend(r.prototype,ae),"GET"!=T.getDefaultType()&&U.on("initialize",ae.createConstraints).on("setOptions",ae.setupConstraints).on("requestParams",ae.constructConstraintsParams).on("dispose",ae.unbindFromParent);var ue={proceedQuery:function(e){var t=this;e.length>=t.options.minChars?t.updateSuggestions(e):t.hide()},selectCurrentValue:function(e){var t=this,n=B.Deferred();return t.inputPhase.resolve(),t.fetchPhase.done(function(){var i;t.selection&&!t.visible?n.reject():(i=t.findSuggestionIndex(),t.select(i,e),-1===i?n.reject():n.resolve(i))}).fail(function(){n.reject()}),n},selectFoundSuggestion:function(){var e=this;e.requestMode.userSelect||e.select(0)},findSuggestionIndex:function(){var e,t=this,n=t.selectedIndex;return-1===n&&(e=t.el.val().trim())&&t.type.matchers.some(function(i){return-1!==(n=i(e,t.suggestions))}),n},select:function(e,t){var n,i=this,s=i.suggestions[e],o=t&&t.continueSelecting,r=i.currentValue;if(!i.triggering.Select){if(!s)return o||i.selection||i.triggerOnSelectNothing(),void i.onSelectComplete(o);n=i.hasSameValues(s),i.enrichSuggestion(s,t).done(function(s,o){var a=B.extend({hasBeenEnriched:o,hasSameValues:n},t);i.selectSuggestion(s,e,r,a)})}},selectSuggestion:function(e,t,n,i){var s=this,o=i.continueSelecting,r=!s.type.isDataComplete||s.type.isDataComplete.call(s,e),a=s.selection;s.triggering.Select||(s.type.alwaysContinueSelecting&&(o=!0),r&&(o=!1),i.hasBeenEnriched&&s.suggestions[t]&&(s.suggestions[t].data=e.data),s.requestMode.updateValue&&(s.checkValueBounds(e),s.currentValue=s.getSuggestionValue(e,i),!s.currentValue||i.noSpace||r||(s.currentValue+=" "),s.el.val(s.currentValue)),s.currentValue?(s.selection=e,s.areSuggestionsSame(e,a)||s.trigger("Select",e,s.currentValue!=n),s.requestMode.userSelect&&s.onSelectComplete(o)):(s.selection=null,s.triggerOnSelectNothing()),s.shareWithParent(e))},onSelectComplete:function(e){var t=this;e?(t.selectedIndex=-1,t.updateSuggestions(t.currentValue)):t.hide()},triggerOnSelectNothing:function(){var e=this;e.triggering.SelectNothing||e.trigger("SelectNothing",e.currentValue)},trigger:function(e){var t=this,n=V.slice(arguments,1),i=t.options["on"+e];t.triggering[e]=!0,V.isFunction(i)&&i.apply(t.element,n),t.el.trigger.call(t.el,"suggestions-"+e.toLowerCase(),n),t.triggering[e]=!1}};B.extend(r.prototype,ue),U.on("assignSuggestions",ue.selectFoundSuggestion);var le={bounds:null},ce={setupBounds:function(){this.bounds={from:null,to:null}},setBoundsOptions:function(){var t,n,i=this,s=[],o=e.trim(i.options.bounds).split("-"),r=o[0],a=o[o.length-1],u=[],l=[];i.type.dataComponents&&e.each(i.type.dataComponents,function(){this.forBounds&&s.push(this.id)}),-1===e.inArray(r,s)&&(r=null),n=e.inArray(a,s),-1!==n&&n!==s.length-1||(a=null),(r||a)&&(t=!r,e.each(s,function(e,n){if(n==r&&(t=!0),l.push(n),t&&u.push(n),n==a)return!1})),i.bounds.from=r,i.bounds.to=a,i.bounds.all=l,i.bounds.own=u},constructBoundsParams:function(){var e=this,t={};return e.bounds.from&&(t.from_bound={value:e.bounds.from}),e.bounds.to&&(t.to_bound={value:e.bounds.to}),t},checkValueBounds:function(e){var t,n=this;if(n.bounds.own.length&&n.type.composeValue){var i=n.bounds.own.slice(0);1===i.length&&"city_district"===i[0]&&i.push("city_district_fias_id"),t=n.copyDataComponents(e.data,i),e.value=n.type.composeValue(t)}},copyDataComponents:function(t,n){var i={},s=this.type.dataComponentsById;return s&&e.each(n,function(n,o){e.each(s[o].fields,function(e,n){null!=t[n]&&(i[n]=t[n])})}),i},getBoundedKladrId:function(t,n){var i,s=n[n.length-1];return e.each(this.type.dataComponents,function(e,t){if(t.id===s)return i=t.kladrFormat,!1}),t.substr(0,i.digits)+new Array((i.zeros||0)+1).join("0")}};e.extend(L,le),e.extend(r.prototype,ce),U.on("initialize",ce.setupBounds).on("setOptions",ce.setBoundsOptions).on("requestParams",ce.constructBoundsParams);var de={selectByClass:function(e,t){var n="."+e;return t?t.querySelector(n):document.querySelector(n)},addClass:function(e,t){var n=e.className.split(" ");-1===n.indexOf(t)&&n.push(t),e.className=n.join(" ")},setStyle:function(e,t,n){e.style[t]=n},listenTo:function(e,t,n,i){e.addEventListener(t,i,!1),n&&(eventsByNamespace[n]||(eventsByNamespace[n]=[]),eventsByNamespace[n].push({eventName:t,element:e,callback:i}))},stopListeningNamespace:function(e){var t=eventsByNamespace[e];t&&t.forEach(function(e){e.element.removeEventListener(e.eventName,e.callback,!1)})}};g.prototype.show=function(){"FREE"===this.plan&&this.element&&(this.setStyles(),this.setHtml())},g.prototype.setStyles=function(){this.element.style.display="block"},g.prototype.setHtml=function(){this.element.innerHTML='dadata-logo'},U.on("assignSuggestions",h),r.defaultOptions=L,r.version="20.3.0",e.Suggestions=r,e.fn.suggestions=function(t,n){return 0===arguments.length?this.first().data("suggestions"):this.each(function(){var i=e(this),s=i.data("suggestions");"string"==typeof t?s&&"function"==typeof s[t]&&s[t](n):(s&&s.dispose&&s.dispose(),s=new r(this,t),i.data("suggestions",s))})}}); \ No newline at end of file diff --git a/docs/kafkatry-ruby/app/assets/javascripts/jsonb.js b/docs/kafkatry-ruby/app/assets/javascripts/jsonb.js new file mode 100644 index 0000000..5351865 --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/javascripts/jsonb.js @@ -0,0 +1,8 @@ +$(function() { + let $elements = $("body.show .b-json__show"); + $elements.each(function(index, element) { + let $element= $(element); + let data = $element.data('json'); + $element.jsonViewer(data, {collapsed: true, withQuotes: true}); + }) +}); \ No newline at end of file diff --git a/docs/kafkatry-ruby/app/assets/javascripts/rabbit_messages.js b/docs/kafkatry-ruby/app/assets/javascripts/rabbit_messages.js new file mode 100644 index 0000000..b022a1b --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/javascripts/rabbit_messages.js @@ -0,0 +1,9 @@ +$(function() { + $('#index_table_rabbit_messages pre').hide(); + + $('#index_table_rabbit_messages .col-data a, #index_table_rabbit_messages .col-error_backtrace a').click(function () { + $(this).parent().find('pre').toggle(); + + return false; + }) +}) diff --git a/docs/kafkatry-ruby/app/assets/stylesheets/active_admin.scss b/docs/kafkatry-ruby/app/assets/stylesheets/active_admin.scss new file mode 100644 index 0000000..61ee609 --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/stylesheets/active_admin.scss @@ -0,0 +1,34 @@ +$am-theme-primary: #383548; +$am-theme-accent: #f76e6e; +$am-theme-backdrop: #646464; +$am-theme-paper: #c8c8c8; + +// SASS variable overrides must be declared before loading up Active Admin's styles. +// +// To view the variables that Active Admin provides, take a look at +// `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the +// Active Admin source. +// +// For example, to change the sidebar width: +// $sidebar-width: 242px; + +// Active Admin's got SASS! +@import "active_admin/mixins"; +//@import "active_admin/base"; +@import "activeadmin_addons/all"; +@import "activeadmin_addons/material"; +@import "jquery.json-viewer"; +@import "suggestions.min"; + +.status_tag { + &.current { background: #ffea44; } + &.error { background: #ff3300; } + &.success { background: #33cc33; } +} + +.b-images-image{ + img { + max-width: 100%; + } + max-width: 100px; +} diff --git a/docs/kafkatry-ruby/app/assets/stylesheets/application.css b/docs/kafkatry-ruby/app/assets/stylesheets/application.css new file mode 100644 index 0000000..d05ea0f --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/docs/kafkatry-ruby/app/assets/stylesheets/jquery.json-viewer.scss b/docs/kafkatry-ruby/app/assets/stylesheets/jquery.json-viewer.scss new file mode 100644 index 0000000..2c72ae5 --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/stylesheets/jquery.json-viewer.scss @@ -0,0 +1,47 @@ +/* Syntax highlighting for JSON objects */ +ul.json-dict, ol.json-array { + list-style-type: none; + margin: 0 0 0 1px; + border-left: 1px dotted #ccc; + padding-left: 2em; +} +.json-string { + color: #0B7500; +} +.json-literal { + color: #1A01CC; + font-weight: bold; +} + +/* Toggle button */ +a.json-toggle { + position: relative; + color: inherit; + text-decoration: none; +} +a.json-toggle:focus { + outline: none; +} +a.json-toggle:before { + color: #aaa; + content: "\25BC"; /* down arrow */ + position: absolute; + display: inline-block; + width: 1em; + left: -1em; +} +a.json-toggle.collapsed:before { + transform: rotate(-90deg); /* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */ + -ms-transform: rotate(-90deg); + -webkit-transform: rotate(-90deg); +} + +/* Collapsable placeholder links */ +a.json-placeholder { + color: #aaa; + padding: 0 1em; + text-decoration: none; +} +a.json-placeholder:hover { + text-decoration: underline; +} diff --git a/docs/kafkatry-ruby/app/assets/stylesheets/suggestions.min.css b/docs/kafkatry-ruby/app/assets/stylesheets/suggestions.min.css new file mode 100644 index 0000000..3f5956f --- /dev/null +++ b/docs/kafkatry-ruby/app/assets/stylesheets/suggestions.min.css @@ -0,0 +1 @@ +.suggestions-nowrap{white-space:nowrap}.suggestions-input{-ms-box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.suggestions-input::-ms-clear{display:none}.suggestions-wrapper{position:relative;margin:0;padding:0;vertical-align:top;-webkit-text-size-adjust:100%}.suggestions-suggestions{background:#fff;border:1px solid #999;-ms-box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:default;left:0;min-width:100%;position:absolute;z-index:9999;-webkit-text-size-adjust:100%}.suggestions-suggestions strong{font-weight:400;color:#39f}.suggestions-suggestions.suggestions-mobile{border-style:none}.suggestions-suggestions.suggestions-mobile .suggestions-suggestion{border-bottom:1px solid #ddd}.suggestions-suggestion{padding:4px 4px;overflow:hidden}.suggestions-suggestion:hover{background:#f7f7f7}.suggestions-selected{background:#f0f0f0}.suggestions-selected:hover{background:#f0f0f0}.suggestions-hint{padding:4px 4px;white-space:nowrap;overflow:hidden;color:#777;font-size:85%;line-height:20px}.suggestions-subtext{color:#777}.suggestions-subtext_inline{display:inline-block;min-width:6em;vertical-align:bottom;margin:0 .5em 0 0}.suggestions-subtext-delimiter{display:inline-block;width:2px}.suggestions-subtext_label{margin:0 0 0 .25em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:0 3px;background:#f5f5f5;font-size:85%}.suggestions-value[data-suggestion-status=LIQUIDATED]{position:relative}.suggestions-value[data-suggestion-status=LIQUIDATED]:after{position:absolute;left:0;right:0;top:50%;border-top:1px solid rgba(0,0,0,.4);content:""}.suggestions-promo{font-size:85%;display:none;color:#777;padding:4px;text-align:center}.suggestions-promo a{color:#777;display:block;filter:grayscale(100%);line-height:20px;text-decoration:none}.suggestions-promo a:hover{filter:grayscale(0)}.suggestions-promo svg{height:20px;vertical-align:bottom}@media screen and (min-width:600px){.suggestions-promo{position:absolute;top:0;right:0;text-align:left}} \ No newline at end of file diff --git a/docs/kafkatry-ruby/app/channels/application_cable/channel.rb b/docs/kafkatry-ruby/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/docs/kafkatry-ruby/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/docs/kafkatry-ruby/app/channels/application_cable/connection.rb b/docs/kafkatry-ruby/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/docs/kafkatry-ruby/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/docs/kafkatry-ruby/app/consumers/application_consumer.rb b/docs/kafkatry-ruby/app/consumers/application_consumer.rb new file mode 100644 index 0000000..2214b6c --- /dev/null +++ b/docs/kafkatry-ruby/app/consumers/application_consumer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Application consumer from which all Karafka consumers should inherit +# You can rename it if it would conflict with your current code base (in case you're integrating +# Karafka with other frameworks) +class ApplicationConsumer < Karafka::BaseConsumer +end diff --git a/docs/kafkatry-ruby/app/consumers/example_consumer.rb b/docs/kafkatry-ruby/app/consumers/example_consumer.rb new file mode 100644 index 0000000..9b82d9a --- /dev/null +++ b/docs/kafkatry-ruby/app/consumers/example_consumer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Example consumer that prints messages payloads +class ExampleConsumer < ApplicationConsumer + def consume + messages.each { |message| puts message.payload } + end + + # Run anything upon partition being revoked + # def revoked + # end + + # Define here any teardown things you want when Karafka server stops + # def shutdown + # end +end diff --git a/docs/kafkatry-ruby/app/controllers/application_controller.rb b/docs/kafkatry-ruby/app/controllers/application_controller.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/docs/kafkatry-ruby/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/docs/kafkatry-ruby/app/controllers/concerns/.keep b/docs/kafkatry-ruby/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/app/helpers/application_helper.rb b/docs/kafkatry-ruby/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/docs/kafkatry-ruby/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/docs/kafkatry-ruby/app/javascript/channels/consumer.js b/docs/kafkatry-ruby/app/javascript/channels/consumer.js new file mode 100644 index 0000000..8ec3aad --- /dev/null +++ b/docs/kafkatry-ruby/app/javascript/channels/consumer.js @@ -0,0 +1,6 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. + +import { createConsumer } from "@rails/actioncable" + +export default createConsumer() diff --git a/docs/kafkatry-ruby/app/javascript/channels/index.js b/docs/kafkatry-ruby/app/javascript/channels/index.js new file mode 100644 index 0000000..0cfcf74 --- /dev/null +++ b/docs/kafkatry-ruby/app/javascript/channels/index.js @@ -0,0 +1,5 @@ +// Load all the channels within this directory and all subdirectories. +// Channel files must be named *_channel.js. + +const channels = require.context('.', true, /_channel\.js$/) +channels.keys().forEach(channels) diff --git a/docs/kafkatry-ruby/app/javascript/packs/application.js b/docs/kafkatry-ruby/app/javascript/packs/application.js new file mode 100644 index 0000000..5929248 --- /dev/null +++ b/docs/kafkatry-ruby/app/javascript/packs/application.js @@ -0,0 +1,9 @@ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. + +import Rails from "@rails/ujs" +import "channels" + +Rails.start() diff --git a/docs/kafkatry-ruby/app/jobs/application_job.rb b/docs/kafkatry-ruby/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/docs/kafkatry-ruby/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/docs/kafkatry-ruby/app/models/application_record.rb b/docs/kafkatry-ruby/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/docs/kafkatry-ruby/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/docs/kafkatry-ruby/app/models/concerns/.keep b/docs/kafkatry-ruby/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/app/models/kafka_message.rb b/docs/kafkatry-ruby/app/models/kafka_message.rb new file mode 100644 index 0000000..76d37b8 --- /dev/null +++ b/docs/kafkatry-ruby/app/models/kafka_message.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: kafka_messages +# +# id :uuid not null, primary key +# action :string not null +# data :jsonb +# direction :string default("income") +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_kafka_messages_on_action (action) +# +class KafkaMessage < ApplicationRecord + INCOME_MESSAGE = 'income'.freeze + OUTCOME_MESSAGE = 'outcome'.freeze + + validates :action, presence: true +end diff --git a/docs/kafkatry-ruby/app/views/layouts/application.html.erb b/docs/kafkatry-ruby/app/views/layouts/application.html.erb new file mode 100644 index 0000000..573e05a --- /dev/null +++ b/docs/kafkatry-ruby/app/views/layouts/application.html.erb @@ -0,0 +1,16 @@ + + + + Kafkatry + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag 'application', media: 'all' %> + <%= javascript_pack_tag 'application' %> + + + + <%= yield %> + + diff --git a/docs/kafkatry-ruby/bin/bundle b/docs/kafkatry-ruby/bin/bundle new file mode 100755 index 0000000..ee73929 --- /dev/null +++ b/docs/kafkatry-ruby/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/docs/kafkatry-ruby/bin/rails b/docs/kafkatry-ruby/bin/rails new file mode 100755 index 0000000..6fb4e40 --- /dev/null +++ b/docs/kafkatry-ruby/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/docs/kafkatry-ruby/bin/rake b/docs/kafkatry-ruby/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/docs/kafkatry-ruby/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/docs/kafkatry-ruby/bin/setup b/docs/kafkatry-ruby/bin/setup new file mode 100755 index 0000000..90700ac --- /dev/null +++ b/docs/kafkatry-ruby/bin/setup @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies + system! 'bin/yarn' + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:prepare' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/docs/kafkatry-ruby/bin/yarn b/docs/kafkatry-ruby/bin/yarn new file mode 100755 index 0000000..9fab2c3 --- /dev/null +++ b/docs/kafkatry-ruby/bin/yarn @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + yarn = ENV["PATH"].split(File::PATH_SEPARATOR). + select { |dir| File.expand_path(dir) != __dir__ }. + product(["yarn", "yarn.cmd", "yarn.ps1"]). + map { |dir, file| File.expand_path(file, dir) }. + find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/docs/kafkatry-ruby/config.ru b/docs/kafkatry-ruby/config.ru new file mode 100644 index 0000000..ad1fbf2 --- /dev/null +++ b/docs/kafkatry-ruby/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application +Rails.application.load_server diff --git a/docs/kafkatry-ruby/config/application.rb b/docs/kafkatry-ruby/config/application.rb new file mode 100644 index 0000000..b73c7db --- /dev/null +++ b/docs/kafkatry-ruby/config/application.rb @@ -0,0 +1,29 @@ +require_relative 'boot' + +require 'rails' +# Pick the frameworks you want: +require 'active_model/railtie' +require 'active_job/railtie' +require 'active_record/railtie' +# require "active_storage/engine" +require 'action_controller/railtie' +# require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require 'action_view/railtie' +require 'action_cable/engine' +require 'sprockets/railtie' +require 'rails/test_unit/railtie' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Kafkatry + class Application < Rails::Application + config.load_defaults 6.1 + config.time_zone = 'Europe/Moscow' + config.i18n.default_locale = :ru + config.eager_load_paths << Rails.root.join('lib') + end +end diff --git a/docs/kafkatry-ruby/config/boot.rb b/docs/kafkatry-ruby/config/boot.rb new file mode 100644 index 0000000..30f5120 --- /dev/null +++ b/docs/kafkatry-ruby/config/boot.rb @@ -0,0 +1,3 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/docs/kafkatry-ruby/config/cable.yml b/docs/kafkatry-ruby/config/cable.yml new file mode 100644 index 0000000..cbbab57 --- /dev/null +++ b/docs/kafkatry-ruby/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: kafkatry_production diff --git a/docs/kafkatry-ruby/config/database.yml b/docs/kafkatry-ruby/config/database.yml new file mode 100644 index 0000000..2e4dbc1 --- /dev/null +++ b/docs/kafkatry-ruby/config/database.yml @@ -0,0 +1,89 @@ +# PostgreSQL. Versions 9.3 and up are supported. +# +# Install the pg driver: +# gem install pg +# On macOS with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On macOS with MacPorts: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem 'pg' +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # https://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + host: db + username: postgres + password: <%= ENV['KAFKATRY_DATABASE_PASSWORD'] %> + +development: + <<: *default + database: kafkatry_development + + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user running Rails. + #username: kafkatry + + # The password associated with the postgres role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: kafkatry_test + +# As with config/credentials.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password or a full connection URL as an environment +# variable when you boot the app. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%= ENV['MY_APP_DATABASE_URL'] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# +production: + <<: *default + database: kafkatry_production + username: kafkatry + password: <%= ENV['KAFKATRY_DATABASE_PASSWORD'] %> diff --git a/docs/kafkatry-ruby/config/environment.rb b/docs/kafkatry-ruby/config/environment.rb new file mode 100644 index 0000000..426333b --- /dev/null +++ b/docs/kafkatry-ruby/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/docs/kafkatry-ruby/config/environments/development.rb b/docs/kafkatry-ruby/config/environments/development.rb new file mode 100644 index 0000000..323ddb0 --- /dev/null +++ b/docs/kafkatry-ruby/config/environments/development.rb @@ -0,0 +1,71 @@ +require 'active_support/core_ext/integer/time' + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + app_host = ENV['APP_HOST'] + config.hosts << app_host if app_host.present? +end diff --git a/docs/kafkatry-ruby/config/environments/production.rb b/docs/kafkatry-ruby/config/environments/production.rb new file mode 100644 index 0000000..8064a66 --- /dev/null +++ b/docs/kafkatry-ruby/config/environments/production.rb @@ -0,0 +1,112 @@ +require 'active_support/core_ext/integer/time' + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info + + # Prepend all log lines with the following tags. + config.log_tags = [:request_id] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "kafkatry_production" + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Log disallowed deprecations. + config.active_support.disallowed_deprecation = :log + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = Logger::Formatter.new + + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV['RAILS_LOG_TO_STDOUT'].present? + logger = ActiveSupport::Logger.new($stdout) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = + # ActiveRecord::Middleware::DatabaseSelector::Resolver::Session +end diff --git a/docs/kafkatry-ruby/config/environments/test.rb b/docs/kafkatry-ruby/config/environments/test.rb new file mode 100644 index 0000000..0e6c806 --- /dev/null +++ b/docs/kafkatry-ruby/config/environments/test.rb @@ -0,0 +1,49 @@ +require 'active_support/core_ext/integer/time' + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true +end diff --git a/docs/kafkatry-ruby/config/initializers/active_admin.rb b/docs/kafkatry-ruby/config/initializers/active_admin.rb new file mode 100644 index 0000000..cec526f --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/active_admin.rb @@ -0,0 +1,302 @@ +ActiveAdmin.setup do |config| + # == Site Title + # + # Set the title that is displayed on the main layout + # for each of the active admin pages. + # + config.site_title = 'Административная панель' + # Set the link url for the title. For example, to take + # users to your main site. Defaults to no link. + # + # config.site_title_link = "/" + + # Set an optional image to be displayed for the header + # instead of a string (overrides :site_title) + # + # Note: Aim for an image that's 21px high so it fits in the header. + # + # config.site_title_image = "logo.png" + + # == Default Namespace + # + # Set the default namespace each administration resource + # will be added to. + # + # eg: + # config.default_namespace = :hello_world + # + # This will create resources in the HelloWorld module and + # will namespace routes to /hello_world/* + # + # To set no namespace by default, use: + # config.default_namespace = false + # + # Default: + # config.default_namespace = :admin + # + # You can customize the settings for each namespace by using + # a namespace block. For example, to change the site title + # within a namespace: + # + # config.namespace :admin do |admin| + # admin.site_title = "Custom Admin Title" + # end + # + # This will ONLY change the title for the admin section. Other + # namespaces will continue to use the main "site_title" configuration. + + # == User Authentication + # + # Active Admin will automatically call an authentication + # method in a before filter of all controller actions to + # ensure that there is a currently logged in admin user. + # + # This setting changes the method which Active Admin calls + # within the application controller. + # config.authentication_method = :authenticate_admin_user! + + # == User Authorization + # + # Active Admin will automatically call an authorization + # method in a before filter of all controller actions to + # method in a before filter of all controller actions to + # ensure that there is a user with proper rights. You can use + # CanCanAdapter or make your own. Please refer to documentation. + # config.authorization_adapter = ActiveAdmin::KanAdapter + + # In case you prefer Pundit over other solutions you can here pass + # the name of default policy class. This policy will be used in every + # case when Pundit is unable to find suitable policy. + # config.pundit_default_policy = 'ApplicationPolicy' + + # You can customize your CanCan Ability class name here. + # config.cancan_ability_class = "Ability" + + # You can specify a method to be called on unauthorized access. + # This is necessary in order to prevent a redirect loop which happens + # because, by default, user gets redirected to Dashboard. If user + # doesn't have access to Dashboard, he'll end up in a redirect loop. + # Method provided here should be defined in application_controller.rb. + # config.on_unauthorized_access = :access_denied + + # == Current User + # + # Active Admin will associate actions with the current + # user performing them. + # + # This setting changes the method which Active Admin calls + # (within the application controller) to return the currently logged in user. + # config.current_user_method = :current_admin_user + + # == Logging Out + # + # Active Admin displays a logout link on each screen. These + # settings configure the location and method used for the link. + # + # This setting changes the path where the link points to. If it's + # a string, the strings is used as the path. If it's a Symbol, we + # will call the method to return the path. + # + # Default: + # config.logout_link_path = :destroy_admin_user_session_path + + # This setting changes the http method used when rendering the + # link. For example :get, :delete, :put, etc.. + # + # Default: + # config.logout_link_method = :delete + + # == Root + # + # Set the action to call for the root path. You can set different + # roots for each namespace. + # + # Default: + config.root_to = 'kafka_messages#index' + + # == Admin Comments + # + # This allows your users to comment on any resource registered with Active Admin. + # + # You can completely disable comments: + config.comments = false + # + # You can change the name under which comments are registered: + # config.comments_registration_name = 'AdminComment' + # + # You can change the order for the comments and you can change the column + # to be used for ordering: + # config.comments_order = 'created_at ASC' + # + # You can disable the menu item for the comments index page: + config.comments_menu = false + # + # You can customize the comment menu: + # config.comments_menu = { parent: 'Admin', priority: 1 } + + # == Batch Actions + # + # Enable and disable Batch Actions + # + config.batch_actions = true + + # == Controller Filters + # + # You can add before, after and around filters to all of your + # Active Admin resources and pages from here. + # + # config.before_action :do_something_awesome + + # == Localize Date/Time Format + # + # Set the localize format to display dates and times. + # To understand how to localize your app with I18n, read more at + # https://github.com/svenfuchs/i18n/blob/master/lib%2Fi18n%2Fbackend%2Fbase.rb#L52 + # + config.localize_format = :long + + # == Setting a Favicon + # + config.favicon = 'favicon.ico' + + # == Meta Tags + # + # Add additional meta tags to the head element of active admin pages. + # + # Add tags to all pages logged in users see: + # config.meta_tags = { author: 'My Company' } + + # By default, sign up/sign in/recover password pages are excluded + # from showing up in search engine results by adding a robots meta + # tag. You can reset the hash of meta tags included in logged out + # pages: + # config.meta_tags_for_logged_out_pages = {} + + # == Removing Breadcrumbs + # + # Breadcrumbs are enabled by default. You can customize them for individual + # resources or you can disable them globally from here. + # + # config.breadcrumb = false + + # == Create Another Checkbox + # + # Create another checkbox is disabled by default. You can customize it for individual + # resources or you can enable them globally from here. + # + # config.create_another = true + + # == Register Stylesheets & Javascripts + # + # We recommend using the built in Active Admin layout and loading + # up your own stylesheets / javascripts to customize the look + # and feel. + # + # To load a stylesheet: + # config.register_stylesheet 'my_stylesheet.css' + # + # You can provide an options hash for more control, + # which is passed along to stylesheet_link_tag(): + # config.register_stylesheet 'my_print_stylesheet.css', media: :print + # + # To load a javascript file: + # config.register_javascript 'my_javascript.js' + + # == CSV options + # + # Set the CSV builder separator + # config.csv_options = { col_sep: ';' } + # + # Force the use of quotes + # config.csv_options = { force_quotes: true } + + # == Menu System + # + # You can add a navigation menu to be used in your application, or configure a provided menu + # + # To change the default utility navigation to show a link to your website & a logout btn + # + # config.namespace :admin do |admin| + # admin.build_menu :utility_navigation do |menu| + # menu.add( + # label: "My Great Website", + # url: "http://www.mygreatwebsite.com", + # html_options: { target: :blank } + # ) + # admin.add_logout_button_to_menu menu + # end + # end + # + # If you wanted to add a static menu item to the default menu provided: + # + # config.namespace :admin do |admin| + # admin.build_menu :default do |menu| + # menu.add( + # label: "My Great Website", + # url: "http://www.mygreatwebsite.com", + # html_options: { target: :blank } + # ) + # end + # end + + # == Download Links + # + # You can disable download links on resource listing pages, + # or customize the formats shown per namespace/globally + # + # To disable/customize for the :admin namespace: + # + # config.namespace :admin do |admin| + # + # # Disable the links entirely + # admin.download_links = false + # + # # Only show XML & PDF options + # admin.download_links = [:xml, :pdf] + # + # # Enable/disable the links based on block + # # (for example, with cancan) + # admin.download_links = proc { can?(:view_download_links) } + # + # end + + # == Pagination + # + # Pagination is enabled by default for all resources. + # You can control the default per page count for all resources here. + # + config.default_per_page = 20 + # + # You can control the max per page count too. + # + # config.max_per_page = 10_000 + + # == Filters + # + # By default the index screen includes a "Filters" sidebar on the right + # hand side with a filter for each attribute of the registered model. + # You can enable or disable them for all resources here. + # + # config.filters = true + # + # By default the filters include associations in a select, which means + # that every record will be loaded for each association. + # You can enabled or disable the inclusion + # of those filters by default here. + # + # config.include_default_association_filters = true + + # == Footer + # + # By default, the footer shows the current Active Admin version. You can + # override the content of the footer here. + # + # config.footer = 'my custom footer text' + + # == Sorting + # + # By default ActiveAdmin::OrderClause is used for sorting logic + # You can inherit it with own class and inject it for all resources + # + # config.order_clause = MyOrderClause +end diff --git a/docs/kafkatry-ruby/config/initializers/application_controller_renderer.rb b/docs/kafkatry-ruby/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..89d2efa --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/docs/kafkatry-ruby/config/initializers/assets.rb b/docs/kafkatry-ruby/config/initializers/assets.rb new file mode 100644 index 0000000..8b132d5 --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/assets.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path +# Add Yarn node_modules folder to the asset load path. +Rails.application.config.assets.paths << Rails.root.join('node_modules') +Rails.application.config.assets.paths += [ + Rails.root.join('vendor', 'assets').to_s +] +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/docs/kafkatry-ruby/config/initializers/backtrace_silencers.rb b/docs/kafkatry-ruby/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..6d78e13 --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/backtrace_silencers.rb @@ -0,0 +1,11 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using +# but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } + +# You can also remove all the silencers if you're trying to debug a problem +# that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, +# like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE'] diff --git a/docs/kafkatry-ruby/config/initializers/content_security_policy.rb b/docs/kafkatry-ruby/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..e8b33db --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/content_security_policy.rb @@ -0,0 +1,33 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # If you are using webpack-dev-server then specify webpack-dev-server host +# if Rails.env.development? +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" +# end + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = +# -> request { SecureRandom.base64(16) } + +# Set the nonce only to specific directives +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/docs/kafkatry-ruby/config/initializers/cookies_serializer.rb b/docs/kafkatry-ruby/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..5a6a32d --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/docs/kafkatry-ruby/config/initializers/devise.rb b/docs/kafkatry-ruby/config/initializers/devise.rb new file mode 100644 index 0000000..4f86c59 --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/devise.rb @@ -0,0 +1,294 @@ +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + config.authentication_keys = [:login] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:login] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:login] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 11. If + # using other algorithms, it sets how many times you want the password to be hashed. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 11 + + # Set up a pepper to generate the hashed password. + # config.pepper = '' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. Default is 0.days, meaning + # the user cannot access the website without confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + config.timeout_in = 3.hours + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + require './lib/devise/models/remote_authenticatable' + require './lib/devise/strategies/remote_authenticatable' + config.warden do |manager| + manager.strategies.add(:remote_authenticatable, Devise::Strategies::RemoteAuthenticatable) + manager.default_strategies(scope: :admin_user).unshift :remote_authenticatable + end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' +end +# Add routes +# new_admin_user_session GET /admin/login +# admin_user_session POST /admin/login +# destroy_admin_user_session DELETE|GET /admin/logout +Devise.add_module( + :remote_authenticatable, + strategy: true, + controller: :sessions, + route: { session: :routes } +) diff --git a/docs/kafkatry-ruby/config/initializers/filter_parameter_logging.rb b/docs/kafkatry-ruby/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..3e69694 --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += %i[ + passw secret token _key crypt salt certificate otp ssn +] diff --git a/docs/kafkatry-ruby/config/initializers/inflections.rb b/docs/kafkatry-ruby/config/initializers/inflections.rb new file mode 100644 index 0000000..ac033bf --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/docs/kafkatry-ruby/config/initializers/mime_types.rb b/docs/kafkatry-ruby/config/initializers/mime_types.rb new file mode 100644 index 0000000..dc18996 --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/docs/kafkatry-ruby/config/initializers/permissions_policy.rb b/docs/kafkatry-ruby/config/initializers/permissions_policy.rb new file mode 100644 index 0000000..00f64d7 --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/docs/kafkatry-ruby/config/initializers/wrap_parameters.rb b/docs/kafkatry-ruby/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..bbfc396 --- /dev/null +++ b/docs/kafkatry-ruby/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/docs/kafkatry-ruby/config/karafka.rb b/docs/kafkatry-ruby/config/karafka.rb new file mode 100644 index 0000000..570571e --- /dev/null +++ b/docs/kafkatry-ruby/config/karafka.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class KarafkaApp < Karafka::App + setup do |config| + config.kafka = { 'bootstrap.servers': 'redpanda:9092' } + config.client_id = 'kafkatry_ruby' + # Recreate consumers with each batch. This will allow Rails code reload to work in the + # development mode. Otherwise Karafka process would not be aware of code changes + config.consumer_persistence = !Rails.env.development? + end + + # Comment out this part if you are not using instrumentation and/or you are not + # interested in logging events for certain environments. Since instrumentation + # notifications add extra boilerplate, if you want to achieve max performance, + # listen to only what you really need for given environment. + Karafka.monitor.subscribe(Karafka::Instrumentation::LoggerListener.new) + # Karafka.monitor.subscribe(Karafka::Instrumentation::ProctitleListener.new) + + # This logger prints the producer development info using the Karafka logger. + # It is similar to the consumer logger listener but producer oriented. + Karafka.producer.monitor.subscribe( + WaterDrop::Instrumentation::LoggerListener.new(Karafka.logger) + ) + + routes.draw do + # Uncomment this if you use Karafka with ActiveJob + # You need to define the topic per each queue name you use + # active_job_topic :default + + topic :examples do + consumer ExampleConsumer + end + end +end diff --git a/docs/kafkatry-ruby/config/locales/en.yml b/docs/kafkatry-ruby/config/locales/en.yml new file mode 100644 index 0000000..cf9b342 --- /dev/null +++ b/docs/kafkatry-ruby/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/docs/kafkatry-ruby/config/locales/ru.yml b/docs/kafkatry-ruby/config/locales/ru.yml new file mode 100644 index 0000000..077756b --- /dev/null +++ b/docs/kafkatry-ruby/config/locales/ru.yml @@ -0,0 +1,5 @@ +ru: + active_admin: + kafka_messages: Kafka-сообщения + resend: Переотправить сообщение + resended: Сообщение переотправлено diff --git a/docs/kafkatry-ruby/config/puma.rb b/docs/kafkatry-ruby/config/puma.rb new file mode 100644 index 0000000..6b7e304 --- /dev/null +++ b/docs/kafkatry-ruby/config/puma.rb @@ -0,0 +1,43 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 } +min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch('PORT') { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch('RAILS_ENV') { 'development' } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/docs/kafkatry-ruby/config/routes.rb b/docs/kafkatry-ruby/config/routes.rb new file mode 100644 index 0000000..bf77772 --- /dev/null +++ b/docs/kafkatry-ruby/config/routes.rb @@ -0,0 +1,4 @@ +Rails.application.routes.draw do + ActiveAdmin.routes(self) + # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html +end diff --git a/docs/kafkatry-ruby/db/migrate/20230213171153_kafka_messages.rb b/docs/kafkatry-ruby/db/migrate/20230213171153_kafka_messages.rb new file mode 100644 index 0000000..40275a6 --- /dev/null +++ b/docs/kafkatry-ruby/db/migrate/20230213171153_kafka_messages.rb @@ -0,0 +1,21 @@ +class KafkaMessages < ActiveRecord::Migration[6.1] + def change + enable_extension 'pgcrypto' + + create_table( + :kafka_messages, + id: :uuid, + default: -> { 'gen_random_uuid()' }, + comment: 'Отобранные Kafka-сообщения', + force: :cascade + ) do |t| + t.string :action, null: false, comment: 'Тип/экшен сообщения' + t.jsonb :data, comment: 'Структура сообщения, которая будет передаваться следующему микросервису' + t.string :direction, default: 'income', comment: 'Направление сообщения' + + t.timestamps + end + + add_index :kafka_messages, :action + end +end diff --git a/docs/kafkatry-ruby/db/schema.rb b/docs/kafkatry-ruby/db/schema.rb new file mode 100644 index 0000000..d33979a --- /dev/null +++ b/docs/kafkatry-ruby/db/schema.rb @@ -0,0 +1,28 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2023_02_13_171153) do + + # These are extensions that must be enabled in order to support this database + enable_extension "pgcrypto" + enable_extension "plpgsql" + + create_table "kafka_messages", id: :uuid, default: -> { "gen_random_uuid()" }, comment: "Отобранные Kafka-сообщения", force: :cascade do |t| + t.string "action", null: false, comment: "Тип/экшен сообщения" + t.jsonb "data", comment: "Структура сообщения, которая будет передаваться следующему микросервису" + t.string "direction", default: "income", comment: "Направление сообщения" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["action"], name: "index_kafka_messages_on_action" + end + +end diff --git a/docs/kafkatry-ruby/db/seeds.rb b/docs/kafkatry-ruby/db/seeds.rb new file mode 100644 index 0000000..f3a0480 --- /dev/null +++ b/docs/kafkatry-ruby/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) diff --git a/docs/kafkatry-ruby/docker-compose.yml b/docs/kafkatry-ruby/docker-compose.yml new file mode 100644 index 0000000..808befd --- /dev/null +++ b/docs/kafkatry-ruby/docker-compose.yml @@ -0,0 +1,85 @@ +x-server-volumes: + &server-volumes + - .:/app + - vendor_bundle:/app/vendor/bundle + - ~/.bash_history:/root/.bash_history + - ~/.irbrc:/root/.irbrc + +x-base-service: &base-service + build: + context: . + dockerfile: Dockerfile + image: ruby-karafka + environment: + KARAFKA_BOOT_FILE: '/app/config/karafka.rb' + APP_HOST: ${APP_HOST} + KAFKATRY_DATABASE_PASSWORD: ${KAFKATRY_DATABASE_PASSWORD} + volumes: *server-volumes + stdin_open: true + tty: true + +services: + redpanda: + image: redpandadata/redpanda + command: + - redpanda start + - --smp 1 + - --overprovisioned + - --node-id 0 + - --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 + - --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092 + - --pandaproxy-addr 0.0.0.0:8082 + - --advertise-pandaproxy-addr redpanda:8082 + ports: + - 8081:8081 + - 8082:8082 + - 9092:9092 + - 29092:29092 + + console: + image: docker.redpanda.com/redpandadata/console + restart: on-failure + entrypoint: /bin/sh + command: -c "echo \"$$CONSOLE_CONFIG_FILE\" > /tmp/config.yml; /app/console" + environment: + CONFIG_FILEPATH: /tmp/config.yml + CONSOLE_CONFIG_FILE: | + kafka: + brokers: ["redpanda:29092"] + schemaRegistry: + enabled: true + urls: ["http://redpanda:8081"] + connect: + enabled: true + clusters: + - name: datagen + url: http://connect:8083 + ports: + - 8080:8080 + depends_on: + - redpanda + + server: + <<: *base-service + command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" + depends_on: + - db + ports: + - 3000:3000 + + karafka: + <<: *base-service + command: bundle exec karafka server + + db: + image: postgres:13.4 + environment: + POSTGRESQL_DATABASE: kafkatry_development + POSTGRESQL_USERNAME: postgres + POSTGRES_PASSWORD: ${KAFKATRY_DATABASE_PASSWORD} + ports: + - 5432:5432 + +volumes: + vendor_bundle: + driver: local diff --git a/docs/kafkatry-ruby/lib/assets/.keep b/docs/kafkatry-ruby/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/lib/devise/models/remote_authenticatable.rb b/docs/kafkatry-ruby/lib/devise/models/remote_authenticatable.rb new file mode 100644 index 0000000..e073d71 --- /dev/null +++ b/docs/kafkatry-ruby/lib/devise/models/remote_authenticatable.rb @@ -0,0 +1,64 @@ +module Devise + module Models + module RemoteAuthenticatable + extend ActiveSupport::Concern + + included do + def self.remote_authentication(authentication_hash) + response = check_remote_data(authentication_hash) + return nil unless response + + decoded_token = JsonWebToken.decode(response.dig('result', 'token')) + return nil if decoded_token.nil? + + AdminUser.new(decoded_token) + end + + def self.check_remote_data(authentication_hash) + inbox_client.auth(body: params_for_auth(authentication_hash)) + rescue MortgageClients::Http::UnauthorizedRequest + nil + end + + def self.inbox_client + @inbox_client ||= MortgageClients::Http::InboxAlfa.new( + Settings.hosts.inbox_alfa, verify: false + ) + end + + def self.params_for_auth(authentication_hash) + { + auth: { + login: authentication_hash[:login], + password: authentication_hash[:password] + } + } + end + end + + class_methods do + # rubocop:disable Style/RedundantSelf + def serialize_from_session(id, role, email, login, name) + resource = self.new + resource.id = id + resource.role = role + resource.email = email + resource.login = login + resource.name = name + resource + end + # rubocop:enable Style/RedundantSelf + + def serialize_into_session(record) + [ + record.id, + record.role, + record.email, + record.name, + record.login + ] + end + end + end + end +end diff --git a/docs/kafkatry-ruby/lib/devise/strategies/remote_authenticatable.rb b/docs/kafkatry-ruby/lib/devise/strategies/remote_authenticatable.rb new file mode 100644 index 0000000..3d24bbe --- /dev/null +++ b/docs/kafkatry-ruby/lib/devise/strategies/remote_authenticatable.rb @@ -0,0 +1,18 @@ +require 'devise/strategies/authenticatable' + +module Devise + module Strategies + class RemoteAuthenticatable < Authenticatable + def authenticate! + # authentication_hash doesn't include the password + auth_params = authentication_hash.merge(password: password) + # mapping.to is a wrapper over the resource model + user = mapping.to.remote_authentication(auth_params) + return fail! if user.nil? + return fail! unless user.role.in?(%w[admin bank_admin]) + + success!(user) + end + end + end +end diff --git a/docs/kafkatry-ruby/lib/tasks/.keep b/docs/kafkatry-ruby/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/lib/tasks/auto_annotate_models.rake b/docs/kafkatry-ruby/lib/tasks/auto_annotate_models.rake new file mode 100644 index 0000000..0ddf9f6 --- /dev/null +++ b/docs/kafkatry-ruby/lib/tasks/auto_annotate_models.rake @@ -0,0 +1,50 @@ +if Rails.env.development? + require 'annotate' + task :set_annotation_options do # rubocop:disable Rails/RakeEnvironment + Annotate.set_defaults( + 'routes' => 'false', + 'models' => 'true', + 'position_in_routes' => 'before', + 'position_in_class' => 'before', + 'position_in_test' => 'before', + 'position_in_fixture' => 'before', + 'position_in_factory' => 'before', + 'position_in_serializer' => 'before', + 'show_foreign_keys' => 'true', + 'show_complete_foreign_keys' => 'false', + 'show_indexes' => 'true', + 'simple_indexes' => 'false', + 'model_dir' => 'app/models', + 'root_dir' => '', + 'include_version' => 'false', + 'require' => '', + 'exclude_tests' => 'false', + 'exclude_fixtures' => 'false', + 'exclude_factories' => 'false', + 'exclude_serializers' => 'false', + 'exclude_scaffolds' => 'true', + 'exclude_controllers' => 'true', + 'exclude_helpers' => 'true', + 'exclude_sti_subclasses' => 'false', + 'ignore_model_sub_dir' => 'false', + 'ignore_columns' => nil, + 'ignore_routes' => nil, + 'ignore_unknown_models' => 'false', + 'hide_limit_column_types' => 'integer,boolean', + 'hide_default_column_types' => 'json,jsonb,hstore', + 'skip_on_db_migrate' => 'false', + 'format_bare' => 'true', + 'format_rdoc' => 'false', + 'format_markdown' => 'false', + 'sort' => 'false', + 'force' => 'false', + 'classified_sort' => 'true', + 'trace' => 'false', + 'wrapper_open' => nil, + 'wrapper_close' => nil, + 'with_comment' => false + ) + end + + Annotate.load_tasks +end diff --git a/docs/kafkatry-ruby/lib/tasks/send_message.rake b/docs/kafkatry-ruby/lib/tasks/send_message.rake new file mode 100644 index 0000000..d9d66d9 --- /dev/null +++ b/docs/kafkatry-ruby/lib/tasks/send_message.rake @@ -0,0 +1,8 @@ +namespace :karafka do + namespace :producer do + desc 'Отправка уведомления в топик example' + task produce_sync: :environment do + Karafka.producer.produce_sync(topic: 'example', payload: { 'ping' => "pong at #{Time.current.to_i}" }.to_json) + end + end +end diff --git a/docs/kafkatry-ruby/log/.keep b/docs/kafkatry-ruby/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/package.json b/docs/kafkatry-ruby/package.json new file mode 100644 index 0000000..efdb61d --- /dev/null +++ b/docs/kafkatry-ruby/package.json @@ -0,0 +1,9 @@ +{ + "name": "kafkatry", + "private": true, + "dependencies": { + "@rails/ujs": "^6.0.0", + "@rails/actioncable": "^6.0.0" + }, + "version": "0.1.0" +} diff --git a/docs/kafkatry-ruby/public/404.html b/docs/kafkatry-ruby/public/404.html new file mode 100644 index 0000000..2be3af2 --- /dev/null +++ b/docs/kafkatry-ruby/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/docs/kafkatry-ruby/public/422.html b/docs/kafkatry-ruby/public/422.html new file mode 100644 index 0000000..c08eac0 --- /dev/null +++ b/docs/kafkatry-ruby/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/docs/kafkatry-ruby/public/500.html b/docs/kafkatry-ruby/public/500.html new file mode 100644 index 0000000..78a030a --- /dev/null +++ b/docs/kafkatry-ruby/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/docs/kafkatry-ruby/public/apple-touch-icon-precomposed.png b/docs/kafkatry-ruby/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/public/apple-touch-icon.png b/docs/kafkatry-ruby/public/apple-touch-icon.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/public/favicon.ico b/docs/kafkatry-ruby/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/public/robots.txt b/docs/kafkatry-ruby/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/docs/kafkatry-ruby/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/docs/kafkatry-ruby/sample.env b/docs/kafkatry-ruby/sample.env new file mode 100644 index 0000000..3a4d9bc --- /dev/null +++ b/docs/kafkatry-ruby/sample.env @@ -0,0 +1,2 @@ +APP_HOST=localhost +KAFKATRY_DATABASE_PASSWORD=2yr-DYfrvXT4Cuf9AtjMsYA diff --git a/docs/kafkatry-ruby/test/application_system_test_case.rb b/docs/kafkatry-ruby/test/application_system_test_case.rb new file mode 100644 index 0000000..23701b4 --- /dev/null +++ b/docs/kafkatry-ruby/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require 'test_helper' + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/docs/kafkatry-ruby/test/channels/application_cable/connection_test.rb b/docs/kafkatry-ruby/test/channels/application_cable/connection_test.rb new file mode 100644 index 0000000..f925925 --- /dev/null +++ b/docs/kafkatry-ruby/test/channels/application_cable/connection_test.rb @@ -0,0 +1,13 @@ +require 'test_helper' + +module ApplicationCable + class ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end + end +end diff --git a/docs/kafkatry-ruby/test/controllers/.keep b/docs/kafkatry-ruby/test/controllers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/test/fixtures/files/.keep b/docs/kafkatry-ruby/test/fixtures/files/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/test/helpers/.keep b/docs/kafkatry-ruby/test/helpers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/test/integration/.keep b/docs/kafkatry-ruby/test/integration/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/test/models/.keep b/docs/kafkatry-ruby/test/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/test/system/.keep b/docs/kafkatry-ruby/test/system/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/test/test_helper.rb b/docs/kafkatry-ruby/test/test_helper.rb new file mode 100644 index 0000000..1b7300e --- /dev/null +++ b/docs/kafkatry-ruby/test/test_helper.rb @@ -0,0 +1,15 @@ +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +require 'rails/test_help' + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/docs/kafkatry-ruby/tmp/.keep b/docs/kafkatry-ruby/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/tmp/pids/.keep b/docs/kafkatry-ruby/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/kafkatry-ruby/vendor/.keep b/docs/kafkatry-ruby/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/library-template/.gitignore b/docs/library-template/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/docs/library-template/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/docs/library-template/readme.md b/docs/library-template/readme.md new file mode 100644 index 0000000..d140c53 --- /dev/null +++ b/docs/library-template/readme.md @@ -0,0 +1,37 @@ +# Микросервисное шасси + +Репозиторий содержит шаблон для генерации каркаса Rails приложения +Для того чтобы создать новое приложения используя шаблон нужно выполнить команду: + + rails new awesome-template -m library-template/template.rb + +Для конфигурирования приложения предусмотрены следующие флаги: + +- `--skip-rabbitmq` - без взаимодействия с RabbitMQ +- `--skip-active-admin` - без панели администрирования +- `--skip-db` - без базы данных + + rails new liked --database=postgresql --skip-gits --skip-action-mailbox --skip-action-text --skip-action-cable --skip-hotwire --skip-jbuilder --skip-test --skip-system-test --skip-bootsnap --skip-ci --skip-kamal --skip-solid --skip-javascript --skip-git -m library-template/template.rb + +# Компоненты шаблона + +- `Gemfile` +- `.gitignore` + +## Точки входа + +Приложение имеет две точки входа. Пользователи могут обращаться к системе администрирования через HTTP, для этого запускается веб-сервер puma + +```sh +bundle exec rails s +``` + +Кроме того, приложение слушает RabbitMQ-сообщения, для этого поднимается sneakers-обработчик + +```sh +bundle exec rails sneakers:run +``` + +## RabbitMQ + +Панель управления RabbitMQ можно найти по ссылке http://localhost:15672/ diff --git a/docs/library-template/template.rb b/docs/library-template/template.rb new file mode 100644 index 0000000..170b518 --- /dev/null +++ b/docs/library-template/template.rb @@ -0,0 +1,120 @@ +require 'pry' +require 'pry-byebug' +require 'pathname' + +class ::RailsTemplate < Thor::Group + include Thor::Actions + include Rails::Generators::Actions + + attr_accessor :options, :args + + # rabbitmq + # aasm + # file-detectors + ACTIONS = %w[ + active-admin + ].freeze + # db + + ACTIONS.each do |action| + define_method("skip_#{action.underscore}?") do + args.include? "--skip-#{action}" + end + end + + def configure_active_admin + return if skip_active_admin? + + directory 'app/admin' + copy_file 'app/models/rabbit_message.rb' + copy_file 'app/models/concerns/ransackable.rb' + end + + def configure_configs + copy_file '.rubocop.yml', force: true + copy_file '.rspec' + copy_file 'config/initializers/config.rb' + copy_file 'config/initializers/active_admin.rb' + copy_file 'config/initializers/sneakers.rb' + copy_file 'config/initializers/rabbitmq.rb' + copy_file 'config/settings.yml' + copy_file 'config/routes.rb', force: true + copy_file 'config/locales/ru.active_admin.yml', force: true + end + + def configure_controllers + copy_file 'app/controllers/application_controller.rb', force: true + end + + def configure_gemfile + remove_file 'Gemfile' + template 'Gemfile.erb', 'Gemfile' + copy_file 'Rakefile', force: true + end + + def configure_rabbit_mq + copy_file 'app/workers/sneakers_listener.rb', force: true + copy_file 'lib/rabbit_messages/logging.rb', force: true + copy_file 'lib/rabbit_messages/base_amqp.rb', force: true + copy_file 'lib/rabbit_messages/send.rb', force: true + copy_file 'lib/tasks/fake_message.rake', force: true + end + + def configure_gitignore + copy_file '.gitignore', force: true + end + + def configure_assets + copy_file 'app/assets/config/manifest.js', force: true + copy_file 'app/assets/images/favicon.ico', force: true + copy_file 'app/assets/javascripts/active_admin.js', force: true + copy_file 'app/assets/javascripts/jsonb.js', force: true + copy_file 'app/assets/stylesheets/active_admin.scss', force: true + copy_file 'app/assets/stylesheets/application.css', force: true + copy_file 'vendor/assets/javascripts/jquery.json-viewer.js', force: true + copy_file 'vendor/assets/javascripts/jquery.suggestions.min.js', force: true + copy_file 'vendor/assets/stylesheets/jquery.json-viewer.scss', force: true + copy_file 'vendor/assets/stylesheets/suggestions.min.css', force: true + end + + def copy_fixtures + copy_file 'spec/fixtures/like.json', force: true + copy_file 'spec/rails_helper.rb', force: true + copy_file 'spec/spec_helper.rb', force: true + end + + def configure_db + copy_file 'db/migrate/20250301085819_create_rabbit_messages.rb' + end + + def self.source_root + File.join(__dir__, 'templates') + end + + def app_name + options[:app_name] + end + + def ruby_version + { + full: RUBY_VERSION, + major: RUBY_VERSION[/^(\d+\.\d+)\..*?$/, 1] + } + end + + def rails_version + [Rails::VERSION::MAJOR, Rails::VERSION::MINOR].join('.') + end +end + +generator = ::RailsTemplate.new +generator.shell = shell +generator.options = options.merge(app_name: app_name, rails_generator: self) +generator.args = args +generator.destination_root = Dir.pwd +generator.invoke_all + +after_bundle do + run 'bundle exec rails db:drop db:create db:migrate' + run 'bundle exec rubocop -A --disable-uncorrectable' +end diff --git a/docs/library-template/templates/.gitignore b/docs/library-template/templates/.gitignore new file mode 100644 index 0000000..b210649 --- /dev/null +++ b/docs/library-template/templates/.gitignore @@ -0,0 +1,36 @@ +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key + +.DS_Store +coverage + +config/settings.local.yml +config/settings/*.local.yml +config/environments/*.local.yml diff --git a/docs/library-template/templates/.rspec b/docs/library-template/templates/.rspec new file mode 100644 index 0000000..0f30ade --- /dev/null +++ b/docs/library-template/templates/.rspec @@ -0,0 +1,2 @@ +--color +--require rails_helper diff --git a/docs/library-template/templates/.rubocop.yml b/docs/library-template/templates/.rubocop.yml new file mode 100644 index 0000000..d9f6f03 --- /dev/null +++ b/docs/library-template/templates/.rubocop.yml @@ -0,0 +1,60 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +require: +- rubocop-performance +- rubocop-rails +- rubocop-rspec + +Rails: + Enabled: true + +Rails/FilePath: + EnforcedStyle: arguments + +Style/Documentation: + Enabled: false + +Layout/LineLength: + AutoCorrect: true + Max: 100 + AllowedPatterns: ['(\A|\s)#'] + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/RescueStandardError: + Enabled: false + +Style/AsciiComments: + Enabled: false + +Style/StringLiterals: + Enabled: false + +Metrics/AbcSize: + Max: 30 + +Metrics/BlockLength: + Max: 200 + Exclude: + - 'app/admin/**/*' + - 'Rakefile' + - '**/*.rake' + - 'spec/**/*' + +Rails/HttpPositionalArguments: + Exclude: + - 'spec/**/*' + +AllCops: + Exclude: + - 'tmp/**/*' + - 'db/**/*' + - 'bin/**/*' + - 'vendor/bundle/**/*' + - 'docs/**/*' + +# кол-во строк в блоке it или specify +RSpec/ExampleLength: + Max: 10 diff --git a/docs/library-template/templates/Gemfile.erb b/docs/library-template/templates/Gemfile.erb new file mode 100644 index 0000000..a3f84b0 --- /dev/null +++ b/docs/library-template/templates/Gemfile.erb @@ -0,0 +1,53 @@ +source 'https://rubygems.org' + +gem 'rails', '>= 8.0.1' +gem 'puma', '>= 6.5.0' + +# Базы данных +gem 'pg' + +# Обеспечение работы представлений и JavaScript +gem 'sprockets-rails' +gem 'slim-rails' + +# Управление бизнес-логикой +gem 'dry-initializer' +gem 'dry-types' + +# Конфигурирование приложения +gem 'config' + +<%- unless skip_active_admin? -%> +# Система администрирования +gem 'activeadmin' +gem 'activeadmin_addons' +gem 'devise' +<%- end -%> + +# Интеграция с RabbitMQ +gem 'bunny' +gem 'sneakers' + +group :development, :test do + gem 'bundler-audit' + gem 'capybara' + gem 'database_consistency', require: false + gem 'debug', platforms: %i[ mri windows ], require: 'debug/prelude' + gem 'dotenv' + gem 'fasterer' + gem 'factory_bot_rails' + gem 'ffaker' + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + gem 'rubocop-rails-omakase', require: false + gem 'rspec-rails', '>= 7.1' + gem 'simplecov', require: false + gem 'shoulda-matchers' + gem 'pry-rails' + gem 'pry-byebug' +end + +group :development do + gem 'web-console' +end diff --git a/docs/library-template/templates/Rakefile b/docs/library-template/templates/Rakefile new file mode 100644 index 0000000..ecc33a5 --- /dev/null +++ b/docs/library-template/templates/Rakefile @@ -0,0 +1,4 @@ +require_relative 'config/application' +require 'sneakers/tasks' + +Rails.application.load_tasks diff --git a/docs/library-template/templates/app/admin/dashboard.rb b/docs/library-template/templates/app/admin/dashboard.rb new file mode 100644 index 0000000..6e9a817 --- /dev/null +++ b/docs/library-template/templates/app/admin/dashboard.rb @@ -0,0 +1,32 @@ +ActiveAdmin.register_page 'Dashboard' do + menu priority: 1, label: proc { t('active_admin.dashboard') } + + content title: proc { I18n.t('active_admin.dashboard') } do + div class: 'blank_slate_container', id: 'dashboard_default_message' do + span class: 'blank_slate' do + span I18n.t('active_admin.dashboard_welcome.welcome') + small I18n.t('active_admin.dashboard_welcome.call_to_action') + end + end + + # Here is an example of a simple dashboard with columns and panels. + # + # columns do + # column do + # panel "Recent Posts" do + # ul do + # Post.recent(5).map do |post| + # li link_to(post.title, admin_post_path(post)) + # end + # end + # end + # end + + # column do + # panel "Info" do + # para "Welcome to ActiveAdmin." + # end + # end + # end + end +end diff --git a/docs/library-template/templates/app/admin/rabbit_message.rb b/docs/library-template/templates/app/admin/rabbit_message.rb new file mode 100644 index 0000000..75afebf --- /dev/null +++ b/docs/library-template/templates/app/admin/rabbit_message.rb @@ -0,0 +1,64 @@ +# rubocop:disable Metrics/BlockLength +ActiveAdmin.register RabbitMessage do + menu priority: 5, label: proc { t('active_admin.rabbit_messages') } + + config.sort_order = 'created_at_desc' + + actions :index, :show + + filter :action, as: :select, + collection: proc { RabbitMessage.select(:action).distinct.pluck(:action).sort } + filter :success + filter :direction, as: :select, collection: proc { %w[income outcome] } + + controller do + def scoped_collection + end_of_association_chain.select( + :id, + :action, + :direction, + :success, + :routing_key, + :error_message, + :created_at, + :updated_at + ) + end + + def find_resource + scoped_collection.select(:data, :error_backtrace).find(params[:id]) + end + end + + index do + column :direction + column :action + column :routing_key + column :success + column :error_message + column :created_at + column :updated_at + actions + end + + show do + attributes_table do + row :direction + row :action + row :routing_key + row :success + row :error_message + row :created_at + row :updated_at + row :data do |message| + div class: 'b-json__show', 'data-json': message.data.to_json + end + row :error_backtrace do |message| + tag.pre do + message.error_backtrace + end + end + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/docs/library-template/templates/app/assets/config/manifest.js b/docs/library-template/templates/app/assets/config/manifest.js new file mode 100644 index 0000000..5918193 --- /dev/null +++ b/docs/library-template/templates/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css diff --git a/docs/library-template/templates/app/assets/images/.keep b/docs/library-template/templates/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/library-template/templates/app/assets/images/favicon.ico b/docs/library-template/templates/app/assets/images/favicon.ico new file mode 100644 index 0000000..29fba17 Binary files /dev/null and b/docs/library-template/templates/app/assets/images/favicon.ico differ diff --git a/docs/library-template/templates/app/assets/javascripts/active_admin.js b/docs/library-template/templates/app/assets/javascripts/active_admin.js new file mode 100644 index 0000000..b159705 --- /dev/null +++ b/docs/library-template/templates/app/assets/javascripts/active_admin.js @@ -0,0 +1,6 @@ +//= require active_admin/base +//= require activeadmin_addons/all +//= require jsonb +//= require jquery.json-viewer +//= require jquery.suggestions.min +//= require active_material diff --git a/docs/library-template/templates/app/assets/javascripts/jsonb.js b/docs/library-template/templates/app/assets/javascripts/jsonb.js new file mode 100644 index 0000000..5351865 --- /dev/null +++ b/docs/library-template/templates/app/assets/javascripts/jsonb.js @@ -0,0 +1,8 @@ +$(function() { + let $elements = $("body.show .b-json__show"); + $elements.each(function(index, element) { + let $element= $(element); + let data = $element.data('json'); + $element.jsonViewer(data, {collapsed: true, withQuotes: true}); + }) +}); \ No newline at end of file diff --git a/docs/library-template/templates/app/assets/stylesheets/active_admin.scss b/docs/library-template/templates/app/assets/stylesheets/active_admin.scss new file mode 100644 index 0000000..cdf2dcb --- /dev/null +++ b/docs/library-template/templates/app/assets/stylesheets/active_admin.scss @@ -0,0 +1,65 @@ +$am-theme-primary: #383548; +$am-theme-accent: #f76e6e; +$am-theme-backdrop: #646464; +$am-theme-paper: #c8c8c8; + +// SASS variable overrides must be declared before loading up Active Admin's styles. +// +// To view the variables that Active Admin provides, take a look at +// `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the +// Active Admin source. +// +// For example, to change the sidebar width: +// $sidebar-width: 242px; + +// Active Admin's got SASS! +@import "active_admin/mixins"; +//@import "active_admin/base"; +@import "activeadmin_addons/all"; +@import "activeadmin_addons/material"; +@import "jquery.json-viewer"; +@import "suggestions.min"; + +.status_tag { + &.current { background: #ffea44; } + &.error { background: #ff3300; } + &.success { background: #33cc33; } +} + +.b-images-image{ + img { + max-width: 100%; + } + max-width: 100px; +} + +.flash_error { + background: #ff0000; +} + +.field { + margin: 10px 0px 10px 0px; + &-margin-left-right { + margin: 0px 20px 0px 20px; + display: flex; + + textarea { + flex: 1; + height: 300px; + } + } +} + +body.logged_out { + #wrapper { + #content_wrapper { + background-color: #ccc; + width: 260px; + margin: 0px auto; + } + } +} + +.panel_contents { + padding: 0px 0px 10px 15px; +} diff --git a/docs/library-template/templates/app/assets/stylesheets/application.css b/docs/library-template/templates/app/assets/stylesheets/application.css new file mode 100644 index 0000000..288b9ab --- /dev/null +++ b/docs/library-template/templates/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/docs/library-template/templates/app/controllers/application_controller.rb b/docs/library-template/templates/app/controllers/application_controller.rb new file mode 100644 index 0000000..0477a91 --- /dev/null +++ b/docs/library-template/templates/app/controllers/application_controller.rb @@ -0,0 +1,5 @@ +class ApplicationController < ActionController::Base + skip_before_action :verify_authenticity_token + protect_from_forgery with: :null_session + allow_browser versions: :modern +end diff --git a/docs/library-template/templates/app/models/concerns/ransackable.rb b/docs/library-template/templates/app/models/concerns/ransackable.rb new file mode 100644 index 0000000..a91065f --- /dev/null +++ b/docs/library-template/templates/app/models/concerns/ransackable.rb @@ -0,0 +1,11 @@ +module Ransackable extend ActiveSupport::Concern + class_methods do + def ransackable_attributes(auth_object = nil) + self::PUBLIC_FIELDS + end + + def ransackable_associations(auth_object = nil) + self::RANSACK_ASSOCIATIONS + end + end +end diff --git a/docs/library-template/templates/app/models/rabbit_message.rb b/docs/library-template/templates/app/models/rabbit_message.rb new file mode 100644 index 0000000..dd6985d --- /dev/null +++ b/docs/library-template/templates/app/models/rabbit_message.rb @@ -0,0 +1,15 @@ +class RabbitMessage < ApplicationRecord + include Ransackable + + INCOME_MESSAGE = 'income'.freeze + OUTCOME_MESSAGE = 'outcome'.freeze + DIRECTIONS = [INCOME_MESSAGE, OUTCOME_MESSAGE].freeze + + enum :direction, DIRECTIONS.index_by(&:to_sym) + + validates :action, presence: true + validates :direction, inclusion: { in: DIRECTIONS } + + PUBLIC_FIELDS = %w[id direction action routing_key success error_message created_at updated_at] + RANSACK_ASSOCIATIONS = %w[id direction action routing_key success error_message created_at updated_at] +end diff --git a/docs/library-template/templates/app/workers/sneakers_listener.rb b/docs/library-template/templates/app/workers/sneakers_listener.rb new file mode 100644 index 0000000..21e55eb --- /dev/null +++ b/docs/library-template/templates/app/workers/sneakers_listener.rb @@ -0,0 +1,70 @@ +class SneakersListener + include Sneakers::Worker + include RabbitMessages::Logging + + VALID_ACTIONS = %w[ + like + ].freeze + QUEUE_NAME = Settings.sneakers.queue + PG_EXCEPTION = [ + ActiveRecord::ConnectionNotEstablished, + ActiveRecord::ConnectionTimeoutError, + ActiveRecord::NoDatabaseError, + ActiveRecord::StatementInvalid, + PG::ConnectionBad, + PG::UnableToSend + ].freeze + + from_queue Settings.sneakers.queue + + attr_reader :parsed_message + + def work(message) + parse_message(message) + ActiveRecord::Base.connection_pool.with_connection { process_message } + ack! + rescue *PG_EXCEPTION => e + reconnect_to_database(e) + rescue StandardError => e + log_error(e) + reject! + end + + private + + def parse_message(message) + @parsed_message = JSON.parse(message, symbolize_names: true) + logger.info("Listener worker - message: #{parsed_message}") + end + + def process_message + initialize_rabbit_message!(parsed_message) + if VALID_ACTIONS.include?(action) + send(action, parsed_message) + else + logger.error("Listener worker - unknown action: #{action}") + end + rabbit_message.update!(success: true) + end + + def like(message) + logger.info('###################################################') + logger.info('## TODO like') + logger.info('###################################################') + details = { todo: 'Ответа сервиса' } + RabbitMessages::Send.new(message, details).call + end + + def action + parsed_message[:action] + end + + def reconnect_to_database(err) + log_error(err) + sleep(10) + ActiveRecord::Base.connection.reconnect! + requeue! + rescue StandardError => e + reconnect_to_database(e) + end +end diff --git a/docs/library-template/templates/config/boot.rb b/docs/library-template/templates/config/boot.rb new file mode 100644 index 0000000..30f5120 --- /dev/null +++ b/docs/library-template/templates/config/boot.rb @@ -0,0 +1,3 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/docs/library-template/templates/config/initializers/active_admin.rb b/docs/library-template/templates/config/initializers/active_admin.rb new file mode 100644 index 0000000..fa110c8 --- /dev/null +++ b/docs/library-template/templates/config/initializers/active_admin.rb @@ -0,0 +1,302 @@ +ActiveAdmin.setup do |config| + # == Site Title + # + # Set the title that is displayed on the main layout + # for each of the active admin pages. + # + config.site_title = 'Административная панель' + # Set the link url for the title. For example, to take + # users to your main site. Defaults to no link. + # + # config.site_title_link = "/" + + # Set an optional image to be displayed for the header + # instead of a string (overrides :site_title) + # + # Note: Aim for an image that's 21px high so it fits in the header. + # + # config.site_title_image = "logo.png" + + # == Default Namespace + # + # Set the default namespace each administration resource + # will be added to. + # + # eg: + # config.default_namespace = :hello_world + # + # This will create resources in the HelloWorld module and + # will namespace routes to /hello_world/* + # + # To set no namespace by default, use: + # config.default_namespace = false + # + # Default: + # config.default_namespace = :admin + # + # You can customize the settings for each namespace by using + # a namespace block. For example, to change the site title + # within a namespace: + # + # config.namespace :admin do |admin| + # admin.site_title = "Custom Admin Title" + # end + # + # This will ONLY change the title for the admin section. Other + # namespaces will continue to use the main "site_title" configuration. + + # == User Authentication + # + # Active Admin will automatically call an authentication + # method in a before filter of all controller actions to + # ensure that there is a currently logged in admin user. + # + # This setting changes the method which Active Admin calls + # within the application controller. + # config.authentication_method = :authenticate_admin_user! + + # == User Authorization + # + # Active Admin will automatically call an authorization + # method in a before filter of all controller actions to + # method in a before filter of all controller actions to + # ensure that there is a user with proper rights. You can use + # CanCanAdapter or make your own. Please refer to documentation. + # config.authorization_adapter = ActiveAdmin::KanAdapter + + # In case you prefer Pundit over other solutions you can here pass + # the name of default policy class. This policy will be used in every + # case when Pundit is unable to find suitable policy. + # config.pundit_default_policy = 'ApplicationPolicy' + + # You can customize your CanCan Ability class name here. + # config.cancan_ability_class = "Ability" + + # You can specify a method to be called on unauthorized access. + # This is necessary in order to prevent a redirect loop which happens + # because, by default, user gets redirected to Dashboard. If user + # doesn't have access to Dashboard, he'll end up in a redirect loop. + # Method provided here should be defined in application_controller.rb. + # config.on_unauthorized_access = :access_denied + + # == Current User + # + # Active Admin will associate actions with the current + # user performing them. + # + # This setting changes the method which Active Admin calls + # (within the application controller) to return the currently logged in user. + # config.current_user_method = :current_admin_user + + # == Logging Out + # + # Active Admin displays a logout link on each screen. These + # settings configure the location and method used for the link. + # + # This setting changes the path where the link points to. If it's + # a string, the strings is used as the path. If it's a Symbol, we + # will call the method to return the path. + # + # Default: + # config.logout_link_path = :destroy_admin_user_session_path + + # This setting changes the http method used when rendering the + # link. For example :get, :delete, :put, etc.. + # + # Default: + # config.logout_link_method = :delete + + # == Root + # + # Set the action to call for the root path. You can set different + # roots for each namespace. + # + # Default: + config.root_to = 'dashboard#index' + + # == Admin Comments + # + # This allows your users to comment on any resource registered with Active Admin. + # + # You can completely disable comments: + config.comments = false + # + # You can change the name under which comments are registered: + # config.comments_registration_name = 'AdminComment' + # + # You can change the order for the comments and you can change the column + # to be used for ordering: + # config.comments_order = 'created_at ASC' + # + # You can disable the menu item for the comments index page: + # config.comments_menu = false + # + # You can customize the comment menu: + # config.comments_menu = { parent: 'Admin', priority: 1 } + + # == Batch Actions + # + # Enable and disable Batch Actions + # + config.batch_actions = true + + # == Controller Filters + # + # You can add before, after and around filters to all of your + # Active Admin resources and pages from here. + # + # config.before_action :do_something_awesome + + # == Localize Date/Time Format + # + # Set the localize format to display dates and times. + # To understand how to localize your app with I18n, read more at + # https://github.com/svenfuchs/i18n/blob/master/lib%2Fi18n%2Fbackend%2Fbase.rb#L52 + # + config.localize_format = :long + + # == Setting a Favicon + # + config.favicon = 'favicon.ico' + + # == Meta Tags + # + # Add additional meta tags to the head element of active admin pages. + # + # Add tags to all pages logged in users see: + # config.meta_tags = { author: 'My Company' } + + # By default, sign up/sign in/recover password pages are excluded + # from showing up in search engine results by adding a robots meta + # tag. You can reset the hash of meta tags included in logged out + # pages: + # config.meta_tags_for_logged_out_pages = {} + + # == Removing Breadcrumbs + # + # Breadcrumbs are enabled by default. You can customize them for individual + # resources or you can disable them globally from here. + # + # config.breadcrumb = false + + # == Create Another Checkbox + # + # Create another checkbox is disabled by default. You can customize it for individual + # resources or you can enable them globally from here. + # + # config.create_another = true + + # == Register Stylesheets & Javascripts + # + # We recommend using the built in Active Admin layout and loading + # up your own stylesheets / javascripts to customize the look + # and feel. + # + # To load a stylesheet: + # config.register_stylesheet 'my_stylesheet.css' + # + # You can provide an options hash for more control, + # which is passed along to stylesheet_link_tag(): + # config.register_stylesheet 'my_print_stylesheet.css', media: :print + # + # To load a javascript file: + # config.register_javascript 'my_javascript.js' + + # == CSV options + # + # Set the CSV builder separator + # config.csv_options = { col_sep: ';' } + # + # Force the use of quotes + # config.csv_options = { force_quotes: true } + + # == Menu System + # + # You can add a navigation menu to be used in your application, or configure a provided menu + # + # To change the default utility navigation to show a link to your website & a logout btn + # + # config.namespace :admin do |admin| + # admin.build_menu :utility_navigation do |menu| + # menu.add( + # label: "My Great Website", + # url: "http://www.mygreatwebsite.com", + # html_options: { target: :blank } + # ) + # admin.add_logout_button_to_menu menu + # end + # end + # + # If you wanted to add a static menu item to the default menu provided: + # + # config.namespace :admin do |admin| + # admin.build_menu :default do |menu| + # menu.add( + # label: "My Great Website", + # url: "http://www.mygreatwebsite.com", + # html_options: { target: :blank } + # ) + # end + # end + + # == Download Links + # + # You can disable download links on resource listing pages, + # or customize the formats shown per namespace/globally + # + # To disable/customize for the :admin namespace: + # + # config.namespace :admin do |admin| + # + # # Disable the links entirely + # admin.download_links = false + # + # # Only show XML & PDF options + # admin.download_links = [:xml, :pdf] + # + # # Enable/disable the links based on block + # # (for example, with cancan) + # admin.download_links = proc { can?(:view_download_links) } + # + # end + + # == Pagination + # + # Pagination is enabled by default for all resources. + # You can control the default per page count for all resources here. + # + config.default_per_page = Settings.app.items_per_page + # + # You can control the max per page count too. + # + # config.max_per_page = 10_000 + + # == Filters + # + # By default the index screen includes a "Filters" sidebar on the right + # hand side with a filter for each attribute of the registered model. + # You can enable or disable them for all resources here. + # + # config.filters = true + # + # By default the filters include associations in a select, which means + # that every record will be loaded for each association. + # You can enabled or disable the inclusion + # of those filters by default here. + # + # config.include_default_association_filters = true + + # == Footer + # + # By default, the footer shows the current Active Admin version. You can + # override the content of the footer here. + # + # config.footer = 'my custom footer text' + + # == Sorting + # + # By default ActiveAdmin::OrderClause is used for sorting logic + # You can inherit it with own class and inject it for all resources + # + # config.order_clause = MyOrderClause +end diff --git a/docs/library-template/templates/config/initializers/config.rb b/docs/library-template/templates/config/initializers/config.rb new file mode 100644 index 0000000..f6a222d --- /dev/null +++ b/docs/library-template/templates/config/initializers/config.rb @@ -0,0 +1,68 @@ +Config.setup do |config| + # Name of the constant exposing loaded settings + config.const_name = 'Settings' + + # Ability to remove elements of the array set in earlier loaded settings file. For example value: '--'. + # + # config.knockout_prefix = nil + + # Overwrite an existing value when merging a `nil` value. + # When set to `false`, the existing value is retained after merge. + # + # config.merge_nil_values = true + + # Overwrite arrays found in previously loaded settings file. When set to `false`, arrays will be merged. + # + # config.overwrite_arrays = true + + # Defines current environment, affecting which settings file will be loaded. + # Default: `Rails.env` + # + # config.environment = ENV.fetch('ENVIRONMENT', :development) + + # Load environment variables from the `ENV` object and override any settings defined in files. + # + # config.use_env = false + + # Define ENV variable prefix deciding which variables to load into config. + # + # Reading variables from ENV is case-sensitive. If you define lowercase value below, ensure your ENV variables are + # prefixed in the same way. + # + # When not set it defaults to `config.const_name`. + # + config.env_prefix = 'SETTINGS' + + # What string to use as level separator for settings loaded from ENV variables. Default value of '.' works well + # with Heroku, but you might want to change it for example for '__' to easy override settings from command line, where + # using dots in variable names might not be allowed (eg. Bash). + # + # config.env_separator = '.' + + # Ability to process variables names: + # * nil - no change + # * :downcase - convert to lower case + # + # config.env_converter = :downcase + + # Parse numeric values as integers instead of strings. + # + # config.env_parse_values = true + + # Validate presence and type of specific config values. Check https://github.com/dry-rb/dry-validation for details. + # + # config.schema do + # required(:name).filled + # required(:age).maybe(:int?) + # required(:email).filled(format?: EMAIL_REGEX) + # end + + # Evaluate ERB in YAML config files at load time. + # + # config.evaluate_erb_in_yaml = true + + # Name of directory and file to store config keys + # + # config.file_name = 'settings' + # config.dir_name = 'settings' +end diff --git a/docs/library-template/templates/config/initializers/rabbitmq.rb b/docs/library-template/templates/config/initializers/rabbitmq.rb new file mode 100644 index 0000000..4547dbc --- /dev/null +++ b/docs/library-template/templates/config/initializers/rabbitmq.rb @@ -0,0 +1,37 @@ +class RabbitConnectionManager + # HINT: Thread-safe instance/class variables + thread_mattr_accessor :active_connection, :active_channel + + class << self + def channel + reconnect unless connected? && active_channel&.open? + active_channel + end + + def close! + active_channel&.close + active_connection&.close + end + + def connection_settings + @connection_settings ||= Settings.rabbitmq.to_hash + end + + private + + def establish_connection + self.active_connection = Bunny.new(connection_settings) + active_connection.start + self.active_channel = active_connection.create_channel + end + + def reconnect + close! + establish_connection + end + + def connected? + active_connection&.connected? + end + end +end diff --git a/docs/library-template/templates/config/initializers/sneakers.rb b/docs/library-template/templates/config/initializers/sneakers.rb new file mode 100644 index 0000000..0d6f68e --- /dev/null +++ b/docs/library-template/templates/config/initializers/sneakers.rb @@ -0,0 +1,37 @@ +require 'sneakers' + +Sneakers.configure( + connection: Bunny.new(RabbitConnectionManager.connection_settings), + daemonize: false, + start_worker_delay: 1, + durable: true, + ack: true, + workers: Settings.sneakers.workers, + threads: Settings.sneakers.threads, + exchange: Settings.rabbitmq.exchange, + exchange_type: Settings.rabbitmq.exchange_type, + retry_timeout: 30, + retry_max_times: 3, + timeout_job_after: 60 * 5, # 5 minutes + prefetch: 1, + log: Rails.logger, + hooks: { + before_fork: lambda { + Rails.logger.info('Worker: Disconnect from the database') + ActiveRecord::Base.connection_pool.disconnect! + }, + after_fork: lambda { + Rails.logger.reopen + Rails.logger.info('Worker: Reconnect to the database') + config = Rails.application.config.database_configuration[Rails.env] + config['reaping_frequency'] ||= (ENV['DB_REAP_FREQ'].presence || 10) + config['pool'] ||= ENV['DB_POOL'].presence + ActiveRecord::Base.establish_connection(config) + } + } +) + +Sneakers.error_reporters << proc { |exception| + Sentry.capture_exception(exception) +} +Sneakers.logger.level = Rails.logger.level diff --git a/docs/library-template/templates/config/locales/ru.active_admin.yml b/docs/library-template/templates/config/locales/ru.active_admin.yml new file mode 100644 index 0000000..73dedba --- /dev/null +++ b/docs/library-template/templates/config/locales/ru.active_admin.yml @@ -0,0 +1,5 @@ +--- +ru: + active_admin: + dashboard: Главная страница + rabbit_messages: RM-сообщения diff --git a/docs/library-template/templates/config/routes.rb b/docs/library-template/templates/config/routes.rb new file mode 100644 index 0000000..881783b --- /dev/null +++ b/docs/library-template/templates/config/routes.rb @@ -0,0 +1,5 @@ +Rails.application.routes.draw do + ActiveAdmin.routes(self) + + root to: redirect('/admin/') # if Routing::Admin.present? +end \ No newline at end of file diff --git a/docs/library-template/templates/config/settings.yml b/docs/library-template/templates/config/settings.yml new file mode 100644 index 0000000..c3d8d08 --- /dev/null +++ b/docs/library-template/templates/config/settings.yml @@ -0,0 +1,20 @@ +app: + items_per_page: 20 + name: 'like' + +rabbitmq: + hosts: <%= ENV.fetch('RABBITMQ_HOSTS', 'localhost').split(',') %> + port: <%= ENV.fetch('RABBITMQ_PORT', '5672') %> + vhost: <%= ENV.fetch('RABBITMQ_VHOST', '/') %> + user: <%= ENV.fetch('RABBITMQ_USER', 'guest') %> + password: <%= ENV.fetch('RABBITMQ_PASSWORD', 'guest') %> + heartbeat_interval: <%= ENV.fetch('RABBITMQ_HEARTBEAT_INTERVAL') { 60 } %> + exchange: <%= ENV.fetch('RABBITMQ_EXCHANGE', 'services') %> + exchange_type: <%= ENV.fetch('RABBITMQ_EXCHANGE_TYPE', 'topic') %> + +sneakers: + workers: <%= ENV.fetch('SNEAKERS_WORKERS') { 1 } %> + threads: <%= ENV.fetch('SNEAKERS_THREADS') { 1 } %> + queue: <%= ENV.fetch('SNEAKERS_QUEUE') { 'like_in' } %> + health_check_lifetime: 60 + amqp_stub: <%= ENV.fetch('SNEAKERS_AMQP_STUB') { false } %> diff --git a/docs/library-template/templates/db/migrate/20250301085819_create_rabbit_messages.rb b/docs/library-template/templates/db/migrate/20250301085819_create_rabbit_messages.rb new file mode 100644 index 0000000..6c06c7c --- /dev/null +++ b/docs/library-template/templates/db/migrate/20250301085819_create_rabbit_messages.rb @@ -0,0 +1,26 @@ +class CreateRabbitMessages < ActiveRecord::Migration[8.0] + def change + enable_extension 'pgcrypto' + + create_table( + :rabbit_messages, + id: :uuid, + default: -> { 'gen_random_uuid()' }, + comment: 'Логгирование RabbitMQ-сообщений', + force: :cascade + ) do |t| + t.string :action, null: false, comment: 'Тип/экшен сообщения' + t.jsonb :data, comment: 'Структура сообщения, которая будет передаваться следующему микросервису' + t.boolean :success, default: true, null: false, comment: 'Сообщение обработалось?' + t.string :error_message, comment: 'Сообщение об ошибке' + t.text :error_backtrace, comment: 'Стектрейс ошибки' + t.string :direction, default: 'income', comment: 'Направление сообщения' + t.string :routing_key, comment: 'Ключ маршрутизации' + + t.timestamps + end + + add_index :rabbit_messages, :created_at + add_index :rabbit_messages, :action + end +end diff --git a/docs/library-template/templates/lib/rabbit_messages/base_amqp.rb b/docs/library-template/templates/lib/rabbit_messages/base_amqp.rb new file mode 100644 index 0000000..1d5d19f --- /dev/null +++ b/docs/library-template/templates/lib/rabbit_messages/base_amqp.rb @@ -0,0 +1,23 @@ +module RabbitMessages + class BaseAmqp + EXCHANGE = Settings.rabbitmq.exchange.freeze + + private + + def find_exchange(name) + return AmqpStub.new if Settings.sneakers.amqp_stub + + bunny_channel.exchange(name, passive: true) + end + + def bunny_channel + RabbitConnectionManager.channel + end + + class AmqpStub + def publish(_arg1, _arg2) + Rails.logger.info('Warning!!! Skipping sending AMQP message in dev environment!') + end + end + end +end diff --git a/docs/library-template/templates/lib/rabbit_messages/logging.rb b/docs/library-template/templates/lib/rabbit_messages/logging.rb new file mode 100644 index 0000000..bd30ed0 --- /dev/null +++ b/docs/library-template/templates/lib/rabbit_messages/logging.rb @@ -0,0 +1,40 @@ +module RabbitMessages + module Logging + extend ActiveSupport::Concern + + included do + attr_reader :rabbit_message + end + + def initialize_rabbit_message!( + data, + direction = RabbitMessage::INCOME_MESSAGE, + routing_key = Settings.sneakers.queue_from + ) + @rabbit_message = RabbitMessage.create!( + success: true, + action: data[:action], + routing_key: routing_key, + data: data, + direction: direction + ) + end + + def failed_rabbit_message(exception) + rabbit_message&.assign_attributes( + error_backtrace: exception.backtrace.join("\n"), + error_message: exception.message, + success: false + ) + end + + def log_error(err) + return unless err + + message = I18n.t('errors.exception', error_class: err.class) + Rails.logger.error(message) + + failed_rabbit_message(err) + end + end +end diff --git a/docs/library-template/templates/lib/rabbit_messages/send.rb b/docs/library-template/templates/lib/rabbit_messages/send.rb new file mode 100644 index 0000000..d6bbe54 --- /dev/null +++ b/docs/library-template/templates/lib/rabbit_messages/send.rb @@ -0,0 +1,58 @@ +module RabbitMessages + class Send < BaseAmqp + include Logging + + attr_reader :details, :parsed_message + + def initialize(parsed_message, details) + super() + @details = details + @parsed_message = parsed_message + end + + def call + initialize_rabbit_message!(parsed_message, RabbitMessage::OUTCOME_MESSAGE, + parsed_message[:from]) + + publish_exchange(payload_json) + + rabbit_message&.assign_attributes(data: payload_json) + rescue => e + log_error(e) + ensure + rabbit_message&.save + end + + private + + def publish_exchange(payload) + logging_message(payload) + exchange = find_exchange(EXCHANGE) + exchange.publish(payload.to_json, routing_key: parsed_message[:from]) + end + + def logging_message(payload) + message = I18n.t('messages.answer_send', routing_key: parsed_message[:from], payload: payload) + Rails.logger.info(message) + end + + def payload_json + { + content_type: 'application/json', + encoding: 'UTF-8', + payload: payload_data, + reply_to: Settings.app.name, + timestamp: Time.now.to_i + } + end + + def payload_data + { + params: { + initial_message: parsed_message, + details: details + } + } + end + end +end diff --git a/docs/library-template/templates/lib/tasks/fake_message.rake b/docs/library-template/templates/lib/tasks/fake_message.rake new file mode 100644 index 0000000..bebbc0e --- /dev/null +++ b/docs/library-template/templates/lib/tasks/fake_message.rake @@ -0,0 +1,14 @@ +namespace :fake_message do + desc 'Тестовое сообщение' + task start: :environment do + json_path = Rails.root.join('spec', 'fixtures', 'like.json').to_path + message = JSON.parse(File.read(json_path)) + p message + p Settings.rabbitmq.to_h + connection = Bunny.new(Settings.rabbitmq.to_hash).start + channel = connection.create_channel + exchange = channel.topic('services', durable: true) + exchange.publish(message.to_json, routing_key: Settings.sneakers.queue) + connection.close + end +end diff --git a/docs/library-template/templates/spec/fixtures/like.json b/docs/library-template/templates/spec/fixtures/like.json new file mode 100644 index 0000000..df25756 --- /dev/null +++ b/docs/library-template/templates/spec/fixtures/like.json @@ -0,0 +1,11 @@ +{ + "id": "d8560533-9d18-44e1-9fdd-4672c6a39e6c", + "from": "rake", + "action": "like", + "params": { + "id": "d8560533-9d18-44e1-9fdd-4672c6a39e6c", + "action": "like", + "folder": "f.fb2-440801-444899.zip", + "book": "440905" + } +} diff --git a/docs/library-template/templates/spec/rails_helper.rb b/docs/library-template/templates/spec/rails_helper.rb new file mode 100644 index 0000000..813de60 --- /dev/null +++ b/docs/library-template/templates/spec/rails_helper.rb @@ -0,0 +1,63 @@ +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +abort('The Rails environment is running in production mode!') if Rails.env.production? + +require 'rspec/rails' +require 'capybara/rails' + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end + +RSpec.configure do |config| + config.fixture_paths = [ Rails.root.join('spec', 'fixtures') ] + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/6-0/rspec-rails + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace('gem name') + + config.include FactoryBot::Syntax::Methods + config.include Shoulda::Matchers::ActiveModel, type: :model + config.include Shoulda::Matchers::ActiveRecord, type: :model +end diff --git a/docs/library-template/templates/spec/spec_helper.rb b/docs/library-template/templates/spec/spec_helper.rb new file mode 100644 index 0000000..88cf024 --- /dev/null +++ b/docs/library-template/templates/spec/spec_helper.rb @@ -0,0 +1,102 @@ +require 'simplecov' +SimpleCov.start 'rails' do + add_filter '/config/' + add_filter '/spec/' + add_filter '/docs/' + add_filter '/app/jobs/' +end + +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/docs/library-template/templates/vendor/.keep b/docs/library-template/templates/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/library-template/templates/vendor/assets/javascripts/jquery.json-viewer.js b/docs/library-template/templates/vendor/assets/javascripts/jquery.json-viewer.js new file mode 100644 index 0000000..3ecbc3f --- /dev/null +++ b/docs/library-template/templates/vendor/assets/javascripts/jquery.json-viewer.js @@ -0,0 +1,148 @@ +/** + * jQuery json-viewer + * @author: Alexandre Bodelot + */ +(function($){ + + /** + * Check if arg is either an array with at least 1 element, or a dict with at least 1 key + * @return boolean + */ + function isCollapsable(arg) { + return arg instanceof Object && Object.keys(arg).length > 0; + } + + /** + * Check if a string represents a valid url + * @return boolean + */ + function isUrl(string) { + var regexp = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; + return regexp.test(string); + } + + /** + * Transform a json object into html representation + * @return string + */ + function json2html(json, options) { + var html = ''; + if (typeof json === 'string') { + /* Escape tags */ + json = json.replace(/&/g, '&').replace(//g, '>'); + if (isUrl(json)) + html += '' + json + ''; + else + html += '"' + json + '"'; + } + else if (typeof json === 'number') { + html += '' + json + ''; + } + else if (typeof json === 'boolean') { + html += '' + json + ''; + } + else if (json === null) { + html += 'null'; + } + else if (json instanceof Array) { + if (json.length > 0) { + html += '[
    '; + for (var i = 0; i < json.length; ++i) { + html += '
  1. '; + /* Add toggle button if item is collapsable */ + if (isCollapsable(json[i])) { + html += ''; + } + html += json2html(json[i], options); + /* Add comma if item is not last */ + if (i < json.length - 1) { + html += ','; + } + html += '
  2. '; + } + html += '
]'; + } + else { + html += '[]'; + } + } + else if (typeof json === 'object') { + var key_count = Object.keys(json).length; + if (key_count > 0) { + html += '{
    '; + for (var key in json) { + if (json.hasOwnProperty(key)) { + html += '
  • '; + var keyRepr = options.withQuotes ? + '"' + key + '"' : key; + /* Add toggle button if item is collapsable */ + if (isCollapsable(json[key])) { + html += '' + keyRepr + ''; + } + else { + html += keyRepr; + } + html += ': ' + json2html(json[key], options); + /* Add comma if item is not last */ + if (--key_count > 0) + html += ','; + html += '
  • '; + } + } + html += '
}'; + } + else { + html += '{}'; + } + } + return html; + } + + /** + * jQuery plugin method + * @param json: a javascript object + * @param options: an optional options hash + */ + $.fn.jsonViewer = function(json, options) { + options = options || {}; + + /* jQuery chaining */ + return this.each(function() { + + /* Transform to HTML */ + var html = json2html(json, options); + if (isCollapsable(json)) + html = '' + html; + + /* Insert HTML in target DOM element */ + $(this).html(html); + + /* Bind click on toggle buttons */ + $(this).off('click'); + $(this).on('click', 'a.json-toggle', function() { + var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array'); + target.toggle(); + if (target.is(':visible')) { + target.siblings('.json-placeholder').remove(); + } + else { + var count = target.children('li').length; + var placeholder = count + (count > 1 ? ' items' : ' item'); + target.after('' + placeholder + ''); + } + return false; + }); + + /* Simulate click on toggle button when placeholder is clicked */ + $(this).on('click', 'a.json-placeholder', function() { + $(this).siblings('a.json-toggle').click(); + return false; + }); + + if (options.collapsed == true) { + /* Trigger click to collapse all nodes */ + $(this).find('a.json-toggle').click(); + } + }); + }; +})(jQuery); diff --git a/docs/library-template/templates/vendor/assets/javascripts/jquery.suggestions.min.js b/docs/library-template/templates/vendor/assets/javascripts/jquery.suggestions.min.js new file mode 100644 index 0000000..99fabcb --- /dev/null +++ b/docs/library-template/templates/vendor/assets/javascripts/jquery.suggestions.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],t):t(e.jQuery)}(this,function(e){"use strict";function t(e,t){return function(n,i){var s,o=[];return t(i)&&(s=k.splitTokens(k.split(n,e)),m.each(i,function(t,i){var r=t.value;if(k.stringEncloses(n,r))return!1;var a=k.splitTokens(k.split(r,e));0===m.minus(s,a).length&&o.push(i)})),1===o.length?o[0]:-1}}function n(e,t){var n=e.data&&e.data[t];return n&&new RegExp("^"+k.escapeRegExChars(n)+"(["+w+"]|$)","i").test(e.value)}function i(e,t){var n=//;return n.test(t)&&!n.test(e)?t:e}function s(e,t,n,s,o){var r=this;return i(r.highlightMatches(e,n,s,o),r.highlightMatches(t,n,s,o))}function o(e){this.urlSuffix=e.toLowerCase(),this.noSuggestionsHint="Неизвестное значение",this.matchers=[F.matchByNormalizedQuery(),F.matchByWords()]}function r(t,n){var i=this;i.element=t,i.el=e(t),i.suggestions=[],i.badQueries=[],i.selectedIndex=-1,i.currentValue=i.element.value,i.intervalId=0,i.cachedResponse={},i.enrichmentCache={},i.currentRequest=null,i.inputPhase=e.Deferred(),i.fetchPhase=e.Deferred(),i.enrichPhase=e.Deferred(),i.onChangeTimeout=null,i.triggering={},i.$wrapper=null,i.options=e.extend({},L,n),i.classes=x,i.disabled=!1,i.selection=null,i.$viewport=e(window),i.$body=e(document.body),i.type=null,i.status={},i.setupElement(),i.initializer=e.Deferred(),i.el.is(":visible")?i.initializer.resolve():i.deferInitialization(),i.initializer.done(e.proxy(i.initialize,i))}function a(){V.each(K,function(e){e.abort()}),K={}}function u(){J=null,L.geoLocation=X}function l(t){return e.map(t,function(e){var t=V.escapeHtml(e.text);return t&&e.matched&&(t=""+t+""),t}).join("")}function c(t,n){var i=t.split(", ");return 1===i.length?t:e.map(i,function(e){return''+e+""}).join(", ")}function d(t,n){var i=!1;return e.each(t,function(e,t){if(i=t.value==n.value&&t!=n)return!1}),i}function f(e,t){var n=t.selection,i=n&&n.data&&t.bounds;return i&&m.each(t.bounds.all,function(t,s){return i=n.data[t]===e.data[t]}),i}function p(e){var t=e.replace(/^(\d{2})(\d*?)(0+)$/g,"$1$2"),n=t.length,i=-1;return n<=2?i=2:n>2&&n<=5?i=5:n>5&&n<=8?i=8:n>8&&n<=11?i=11:n>11&&n<=15?i=15:n>15&&(i=19),k.padEnd(t,i,"0")}function g(e){this.plan=e.status.plan;var t=e.getContainer();this.element=de.selectByClass(x.promo,t)}function h(){new g(this).show()}e=e&&e.hasOwnProperty("default")?e.default:e;var y={isArray:function(e){return Array.isArray(e)},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isEmptyObject:function(e){return 0===Object.keys(e).length&&e.constructor===Object},isPlainObject:function(e){return void 0!==e&&"object"==typeof e&&null!==e&&!e.nodeType&&e!==e.window&&!(e.constructor&&!Object.prototype.hasOwnProperty.call(e.constructor.prototype,"isPrototypeOf"))}},m={compact:function(e){return e.filter(function(e){return!!e})},each:function(e,t){if(Array.isArray(e))return void e.some(function(e,n){return!1===t(e,n)});Object.keys(e).some(function(n){var i=e[n];return!1===t(i,n)})},intersect:function(e,t){var n=[];return Array.isArray(e)&&Array.isArray(t)?e.filter(function(e){return-1!==t.indexOf(e)}):n},minus:function(e,t){return t&&0!==t.length?e.filter(function(e){return-1===t.indexOf(e)}):e},makeArray:function(e){return y.isArray(e)?Array.prototype.slice.call(e):[e]},minusWithPartialMatching:function(e,t){return t&&0!==t.length?e.filter(function(e){return!t.some(function(t){return 0===t.indexOf(e)})}):e},slice:function(e,t){return Array.prototype.slice.call(e,t)}},_={delay:function(e,t){return setTimeout(e,t||0)}},v={areSame:function e(t,n){var i=!0;return typeof t==typeof n&&("object"==typeof t&&null!=t&&null!=n?(m.each(t,function(t,s){return i=e(t,n[s])}),i):t===n)},assign:function(e,t){if("function"==typeof Object.assign)return Object.assign.apply(null,arguments);if(null==e)throw new TypeError("Cannot convert undefined or null to object");for(var n=Object(e),i=1;i№",C=new RegExp("["+w+"]+","g"),E=new RegExp("[\\-\\+\\\\\\?!@#$%^&]+","g"),k={escapeHtml:function(e){var t={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return e&&m.each(t,function(t,n){e=e.replace(new RegExp(n,"g"),t)}),e},escapeRegExChars:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},formatToken:function(e){return e&&e.toLowerCase().replace(/[ёЁ]/g,"е")},getWordExtractorRegExp:function(){return new RegExp("([^"+w+"]*)(["+w+"]*)","g")},normalize:function(e,t){return k.split(e,t).join(" ")},padEnd:function(e,t,n){return String.prototype.padEnd?e.padEnd(t,n):(t>>=0,n=String(void 0!==n?n:" "),e.length>t?String(e):(t-=e.length,t>n.length&&(n+=n.repeat(t/n.length)),String(e)+n.slice(0,t)))},split:function(e,t){var n=e.toLowerCase().replace("ё","е").replace(/(\d+)([а-я]{2,})/g,"$1 $2").replace(/([а-я]+)(\d+)/g,"$1 $2"),i=m.compact(n.split(C));if(!i.length)return[];var s=i.pop(),o=m.minus(i,t);return o.push(s),o},splitTokens:function(e){var t=[];return m.each(e,function(e,n){var i=e.split(E);t=t.concat(m.compact(i))}),t},stringEncloses:function(e,t){return e.length>t.length&&-1!==e.toLowerCase().indexOf(t.toLowerCase())},tokenize:function(e,t){var n=m.compact(k.formatToken(e).split(C)),i=m.minus(n,t),s=m.minus(n,i);return n=k.withSubTokens(i.concat(s))},withSubTokens:function(e){var t=[];return m.each(e,function(e,n){var i=e.split(E);t.push(e),i.length>1&&(t=t.concat(m.compact(i)))}),t}},B={Deferred:function(){return e.Deferred()},ajax:function(t){return e.ajax(t)},extend:function(){return e.extend.apply(null,arguments)},isJqObject:function(t){return t instanceof e},param:function(t){return e.param(t)},proxy:function(t,n){return e.proxy(t,n)},select:function(t){return e(t)},supportsCors:function(){return e.support.cors}},T={getDefaultType:function(){return B.supportsCors()?"POST":"GET"},getDefaultContentType:function(){return B.supportsCors()?"application/json":"application/x-www-form-urlencoded"},fixURLProtocol:function(e){return B.supportsCors()?e:e.replace(/^https?:/,location.protocol)},addUrlParams:function(e,t){return e+(/\?/.test(e)?"&":"?")+B.param(t)},serialize:function(e){return B.supportsCors()?JSON.stringify(e,function(e,t){return null===t?void 0:t}):(e=v.compact(e),B.param(e,!0))}},j=function(){var e=0;return function(t){return(t||"")+ ++e}}(),V={escapeRegExChars:k.escapeRegExChars,escapeHtml:k.escapeHtml,formatToken:k.formatToken,normalize:k.normalize,reWordExtractor:k.getWordExtractorRegExp,stringEncloses:k.stringEncloses,addUrlParams:T.addUrlParams,getDefaultContentType:T.getDefaultContentType,getDefaultType:T.getDefaultType,fixURLProtocol:T.fixURLProtocol,serialize:T.serialize,arrayMinus:m.minus,arrayMinusWithPartialMatching:m.minusWithPartialMatching,arraysIntersection:m.intersect,compact:m.compact,each:m.each,makeArray:m.makeArray,slice:m.slice,delay:_.delay,areSame:v.areSame,compactObject:v.compact,getDeepValue:v.getDeepValue,fieldsNotEmpty:v.fieldsAreNotEmpty,indexBy:v.indexObjectsById,isArray:y.isArray,isEmptyObject:y.isEmptyObject,isFunction:y.isFunction,isPlainObject:y.isPlainObject,uniqueId:j},L={$helpers:null,autoSelectFirst:!1,containerClass:"suggestions-suggestions",count:5,deferRequestBy:100,enrichmentEnabled:!0,formatResult:null,formatSelected:null,headers:null,hint:"Выберите вариант или продолжите ввод",initializeInterval:100,language:null,minChars:1,mobileWidth:600,noCache:!1,noSuggestionsHint:null,onInvalidateSelection:null,onSearchComplete:e.noop,onSearchError:e.noop,onSearchStart:e.noop,onSelect:null,onSelectNothing:null,onSuggestionsFetch:null,paramName:"query",params:{},preventBadQueries:!1,requestMode:"suggest",scrollOnFocus:!1,serviceUrl:"https://suggestions.dadata.ru/suggestions/api/4_1/rs",tabDisabled:!1,timeout:3e3,triggerSelectOnBlur:!0,triggerSelectOnEnter:!0,triggerSelectOnSpace:!1,type:null,url:null},O=function(e){return function(t){if(0===t.length)return!1;if(1===t.length)return!0;var n=e(t[0].value);return 0===t.filter(function(t){return 0!==e(t.value).indexOf(n)}).length}}(function(e){return e}),F={matchByNormalizedQuery:function(e){return function(t,n){var i=k.normalize(t,e),s=[];return m.each(n,function(n,o){var r=n.value.toLowerCase();return!k.stringEncloses(t,r)&&(!(r.indexOf(i)>0)&&void(i===k.normalize(r,e)&&s.push(o)))}),1===s.length?s[0]:-1}},matchByWords:function(e){return t(e,O)},matchByWordsAddress:function(e){return t(e,O)},matchByFields:function(e){return function(t,n){var i=k.splitTokens(k.split(t)),s=[];return 1===n.length&&(e&&m.each(e,function(e,t){var i=v.getDeepValue(n[0],t),o=i&&k.splitTokens(k.split(i,e));o&&o.length&&(s=s.concat(o))}),0===m.minusWithPartialMatching(i,s).length)?0:-1}}},P=["ао","аобл","дом","респ","а/я","аал","автодорога","аллея","арбан","аул","б-р","берег","бугор","вал","вл","волость","въезд","высел","г","городок","гск","д","двлд","днп","дор","дп","ж/д_будка","ж/д_казарм","ж/д_оп","ж/д_платф","ж/д_пост","ж/д_рзд","ж/д_ст","жилзона","жилрайон","жт","заезд","заимка","зона","к","казарма","канал","кв","кв-л","км","кольцо","комн","кордон","коса","кп","край","линия","лпх","м","массив","местность","мкр","мост","н/п","наб","нп","обл","округ","остров","оф","п","п/о","п/р","п/ст","парк","пгт","пер","переезд","пл","пл-ка","платф","погост","полустанок","починок","пр-кт","проезд","промзона","просек","просека","проселок","проток","протока","проулок","р-н","рзд","россия","рп","ряды","с","с/а","с/мо","с/о","с/п","с/с","сад","сквер","сл","снт","спуск","ст","ст-ца","стр","тер","тракт","туп","у","ул","уч-к","ф/х","ферма","х","ш","бульвар","владение","выселки","гаражно-строительный","город","деревня","домовладение","дорога","квартал","километр","комната","корпус","литер","леспромхоз","местечко","микрорайон","набережная","область","переулок","платформа","площадка","площадь","поселение","поселок","проспект","разъезд","район","республика","село","сельсовет","слобода","сооружение","станица","станция","строение","территория","тупик","улица","улус","участок","хутор","шоссе"],D=[{id:"kladr_id",fields:["kladr_id"],forBounds:!1,forLocations:!0},{id:"postal_code",fields:["postal_code"],forBounds:!1,forLocations:!0},{id:"country_iso_code",fields:["country_iso_code"],forBounds:!1,forLocations:!0},{id:"country",fields:["country"],forBounds:!0,forLocations:!0,kladrFormat:{digits:0,zeros:13},fiasType:"country_iso_code"},{id:"region_iso_code",fields:["region_iso_code"],forBounds:!1,forLocations:!0},{id:"region_fias_id",fields:["region_fias_id"],forBounds:!1,forLocations:!0},{id:"region_type_full",fields:["region_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:2,zeros:11},fiasType:"region_fias_id"},{id:"region",fields:["region","region_type","region_type_full","region_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:2,zeros:11},fiasType:"region_fias_id"},{id:"area_fias_id",fields:["area_fias_id"],forBounds:!1,forLocations:!0},{id:"area_type_full",fields:["area_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:5,zeros:8},fiasType:"area_fias_id"},{id:"area",fields:["area","area_type","area_type_full","area_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:5,zeros:8},fiasType:"area_fias_id"},{id:"city_fias_id",fields:["city_fias_id"],forBounds:!1,forLocations:!0},{id:"city_type_full",fields:["city_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:8,zeros:5},fiasType:"city_fias_id"},{id:"city",fields:["city","city_type","city_type_full","city_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:8,zeros:5},fiasType:"city_fias_id"},{id:"city_district_fias_id",fields:["city_district_fias_id"],forBounds:!1,forLocations:!0},{id:"city_district_type_full",fields:["city_district_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"city_district_fias_id"},{id:"city_district",fields:["city_district","city_district_type","city_district_type_full","city_district_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"city_district_fias_id"},{id:"settlement_fias_id",fields:["settlement_fias_id"],forBounds:!1,forLocations:!0},{id:"settlement_type_full",fields:["settlement_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"settlement_fias_id"},{id:"settlement",fields:["settlement","settlement_type","settlement_type_full","settlement_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"settlement_fias_id"},{id:"street_fias_id",fields:["street_fias_id"],forBounds:!1,forLocations:!0},{id:"street_type_full",fields:["street_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"street_fias_id"},{id:"street",fields:["street","street_type","street_type_full","street_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"street_fias_id"},{id:"house",fields:["house","house_type","house_type_full","block","block_type"],forBounds:!0,forLocations:!1,kladrFormat:{digits:19}}],I={urlSuffix:"address",noSuggestionsHint:"Неизвестный адрес",matchers:[F.matchByNormalizedQuery(P),F.matchByWordsAddress(P)],dataComponents:D,dataComponentsById:v.indexObjectsById(D,"id","index"),unformattableTokens:P,enrichmentEnabled:!0,enrichmentMethod:"suggest",enrichmentParams:{count:1,locations:null,locations_boost:null,from_bound:null,to_bound:null},getEnrichmentQuery:function(e){return e.unrestricted_value},geoEnabled:!0,isDataComplete:function(e){var t=[this.bounds.to||"flat"],n=e.data;return!y.isPlainObject(n)||v.fieldsAreNotEmpty(n,t)},composeValue:function(e,t){var n=e.country,i=e.region_with_type||m.compact([e.region,e.region_type]).join(" ")||e.region_type_full,s=e.area_with_type||m.compact([e.area_type,e.area]).join(" ")||e.area_type_full,o=e.city_with_type||m.compact([e.city_type,e.city]).join(" ")||e.city_type_full,r=e.settlement_with_type||m.compact([e.settlement_type,e.settlement]).join(" ")||e.settlement_type_full,a=e.city_district_with_type||m.compact([e.city_district_type,e.city_district]).join(" ")||e.city_district_type_full,u=e.street_with_type||m.compact([e.street_type,e.street]).join(" ")||e.street_type_full,l=m.compact([e.house_type,e.house,e.block_type,e.block]).join(" "),c=m.compact([e.flat_type,e.flat]).join(" "),d=e.postal_box&&"а/я "+e.postal_box;return i===o&&(i=""),t&&t.saveCityDistrict||(t&&t.excludeCityDistrict?a="":a&&!e.city_district_fias_id&&(a="")),m.compact([n,i,s,o,a,r,u,l,c,d]).join(", ")},formatResult:function(){var e=[],t=!1;return D.forEach(function(n){t&&e.push(n.id),"city_district"===n.id&&(t=!0)}),function(t,n,i,s){var o,r,a,u=this,l=i.data&&i.data.city_district_with_type,c=s&&s.unformattableTokens,d=i.data&&i.data.history_values;return d&&d.length>0&&(o=k.tokenize(n,c),r=this.type.findUnusedTokens(o,t),(a=this.type.getFormattedHistoryValues(r,d))&&(t+=a)),t=u.highlightMatches(t,n,i,s),t=u.wrapFormattedValue(t,i),l&&(!u.bounds.own.length||u.bounds.own.indexOf("street")>=0)&&!y.isEmptyObject(u.copyDataComponents(i.data,e))&&(t+='
'+u.highlightMatches(l,n,i)+"
"),t}}(),findUnusedTokens:function(e,t){return e.filter(function(e){return-1===t.indexOf(e)})},getFormattedHistoryValues:function(e,t){var n=[],i="";return t.forEach(function(t){m.each(e,function(e){if(t.toLowerCase().indexOf(e)>=0)return n.push(t),!1})}),n.length>0&&(i=" (бывш. "+n.join(", ")+")"),i},getSuggestionValue:function(e,t){var n=null;return t.hasSameValues?n=e.options.restrict_value?this.getValueWithinConstraints(e,t.suggestion):e.bounds.own.length?this.getValueWithinBounds(e,t.suggestion):t.suggestion.unrestricted_value:t.hasBeenEnriched&&e.options.restrict_value&&(n=this.getValueWithinConstraints(e,t.suggestion,{excludeCityDistrict:!0})),n},getValueWithinConstraints:function(e,t,n){return this.composeValue(e.getUnrestrictedData(t.data),n)},getValueWithinBounds:function(e,t,n){var i=e.copyDataComponents(t.data,e.bounds.own.concat(["city_district_fias_id"]));return this.composeValue(i,n)}},q=[{id:"kladr_id",fields:["kladr_id"],forBounds:!1,forLocations:!0},{id:"region_fias_id",fields:["region_fias_id"],forBounds:!1,forLocations:!0},{id:"region_type_full",fields:["region_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:2,zeros:11},fiasType:"region_fias_id"},{id:"region",fields:["region","region_type","region_type_full","region_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:2,zeros:11},fiasType:"region_fias_id"},{id:"area_fias_id",fields:["area_fias_id"],forBounds:!1,forLocations:!0},{id:"area_type_full",fields:["area_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:5,zeros:8},fiasType:"area_fias_id"},{id:"area",fields:["area","area_type","area_type_full","area_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:5,zeros:8},fiasType:"area_fias_id"},{id:"city_fias_id",fields:["city_fias_id"],forBounds:!1,forLocations:!0},{id:"city_type_full",fields:["city_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:8,zeros:5},fiasType:"city_fias_id"},{id:"city",fields:["city","city_type","city_type_full","city_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:8,zeros:5},fiasType:"city_fias_id"},{id:"city_district_fias_id",fields:["city_district_fias_id"],forBounds:!1,forLocations:!0},{id:"city_district_type_full",fields:["city_district_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"city_district_fias_id"},{id:"city_district",fields:["city_district","city_district_type","city_district_type_full","city_district_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"city_district_fias_id"},{id:"settlement_fias_id",fields:["settlement_fias_id"],forBounds:!1,forLocations:!0},{id:"settlement_type_full",fields:["settlement_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"settlement_fias_id"},{id:"settlement",fields:["settlement","settlement_type","settlement_type_full","settlement_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:11,zeros:2},fiasType:"settlement_fias_id"},{id:"planning_structure_fias_id",fields:["planning_structure_fias_id"],forBounds:!1,forLocations:!0},{id:"planning_structure_type_full",fields:["planning_structure_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"planning_structure_fias_id"},{id:"planning_structure",fields:["planning_structure","planning_structure_type","planning_structure_type_full","planning_structure_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"planning_structure_fias_id"},{id:"street_fias_id",fields:["street_fias_id"],forBounds:!1,forLocations:!0},{id:"street_type_full",fields:["street_type_full"],forBounds:!1,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"street_fias_id"},{id:"street",fields:["street","street_type","street_type_full","street_with_type"],forBounds:!0,forLocations:!0,kladrFormat:{digits:15,zeros:2},fiasType:"street_fias_id"},{id:"house",fields:["house","house_type","block","building_type","building"],forBounds:!0,forLocations:!1,kladrFormat:{digits:19}}],z={urlSuffix:"fias",noSuggestionsHint:"Неизвестный адрес",matchers:[F.matchByNormalizedQuery(P),F.matchByWordsAddress(P)],dataComponents:q,dataComponentsById:v.indexObjectsById(q,"id","index"),unformattableTokens:P,isDataComplete:function(e){var t=[this.bounds.to||"house"],n=e.data;return!y.isPlainObject(n)||v.fieldsAreNotEmpty(n,t)},composeValue:function(e,t){var n=e.country,i=e.region_with_type||m.compact([e.region,e.region_type]).join(" ")||e.region_type_full,s=e.area_with_type||m.compact([e.area_type,e.area]).join(" ")||e.area_type_full,o=e.city_with_type||m.compact([e.city_type,e.city]).join(" ")||e.city_type_full,r=e.settlement_with_type||m.compact([e.settlement_type,e.settlement]).join(" ")||e.settlement_type_full,a=e.city_district_with_type||m.compact([e.city_district_type,e.city_district]).join(" ")||e.city_district_type_full,u=e.planning_structure_with_type||m.compact([e.planning_structure_type,e.planning_structure]).join(" ")||e.planning_structure_type_full,l=e.street_with_type||m.compact([e.street_type,e.street]).join(" ")||e.street_type_full,c=m.compact([e.house_type,e.house,e.block_type,e.block]).join(" "),d=m.compact([e.flat_type,e.flat]).join(" "),f=e.postal_box&&"а/я "+e.postal_box;return i===o&&(i=""),t&&t.saveCityDistrict||(t&&t.excludeCityDistrict?a="":a&&!e.city_district_fias_id&&(a="")),m.compact([n,i,s,o,a,r,u,l,c,d,f]).join(", ")},formatResult:function(){return function(e,t,n,i){var s=this;return e=s.highlightMatches(e,t,n,i),e=s.wrapFormattedValue(e,n)}}(),getSuggestionValue:I.getSuggestionValue,getValueWithinConstraints:I.getValueWithinConstraints,getValueWithinBounds:I.getValueWithinBounds},R={urlSuffix:"fio",noSuggestionsHint:!1,matchers:[F.matchByNormalizedQuery(),F.matchByWords()],fieldNames:{surname:"фамилия",name:"имя",patronymic:"отчество"},isDataComplete:function(e){var t,i=this,s=i.options.params,o=e.data;return y.isFunction(s)&&(s=s.call(i.element,e.value)),s&&s.parts?t=s.parts.map(function(e){return e.toLowerCase()}):(t=["surname","name"],n(e,"surname")&&t.push("patronymic")),v.fieldsAreNotEmpty(o,t)},composeValue:function(e){return m.compact([e.surname,e.name,e.patronymic]).join(" ")}},A={LEGAL:[2,2,5,1],INDIVIDUAL:[2,2,6,2]},$={urlSuffix:"party",noSuggestionsHint:"Неизвестная организация",matchers:[F.matchByFields({value:null,"data.address.value":P,"data.inn":null,"data.ogrn":null})],dataComponents:D,enrichmentEnabled:!0,enrichmentMethod:"findById",enrichmentParams:{count:1,locations_boost:null},getEnrichmentQuery:function(e){return e.data.hid},geoEnabled:!0,formatResult:function(e,t,n,o){var r=this,a=r.type.formatResultInn.call(r,n,t),u=r.highlightMatches(v.getDeepValue(n.data,"ogrn"),t,n),l=i(a,u),c=r.highlightMatches(v.getDeepValue(n.data,"management.name"),t,n),d=v.getDeepValue(n.data,"address.value")||"";return r.isMobile&&((o||(o={})).maxLength=50),e=s.call(r,e,v.getDeepValue(n.data,"name.latin"),t,n,o),e=r.wrapFormattedValue(e,n),d&&(d=d.replace(/^(\d{6}|Россия),\s+/i,""),d=r.isMobile?d.replace(new RegExp("^([^"+w+"]+["+w+"]+[^"+w+"]+).*"),"$1"):r.highlightMatches(d,t,n,{unformattableTokens:P})),(l||d||c)&&(e+='
'+(l||"")+""+(i(d,c)||"")+"
"),e},formatResultInn:function(e,t){var n,i,s=this,o=e.data&&e.data.inn,r=A[e.data&&e.data.type],a=/\d/;if(o)return i=s.highlightMatches(o,t,e),r&&(i=i.split(""),n=r.map(function(e){for(var t,n="";e&&(t=i.shift());)n+=t,a.test(t)&&e--;return n}),i=n.join('')+i.join("")),i}},N={urlSuffix:"email",noSuggestionsHint:!1,matchers:[F.matchByNormalizedQuery()],isQueryRequestable:function(e){return this.options.suggest_local||e.indexOf("@")>=0}},M={urlSuffix:"bank",noSuggestionsHint:"Неизвестный банк",matchers:[F.matchByFields({value:null,"data.bic":null,"data.swift":null})],dataComponents:D,geoEnabled:!0,formatResult:function(e,t,n,i){var s=this,o=s.highlightMatches(v.getDeepValue(n.data,"bic"),t,n),r=v.getDeepValue(n.data,"address.value")||"";return e=s.highlightMatches(e,t,n,i),e=s.wrapFormattedValue(e,n),r&&(r=r.replace(/^\d{6}( РОССИЯ)?, /i,""),r=s.isMobile?r.replace(new RegExp("^([^"+w+"]+["+w+"]+[^"+w+"]+).*"),"$1"):s.highlightMatches(r,t,n,{unformattableTokens:P})),(o||r)&&(e+='
'+o+""+r+"
"),e},formatSelected:function(e){return v.getDeepValue(e,"data.name.payment")||null}},W={NAME:R,ADDRESS:I,FIAS:z,PARTY:$,EMAIL:N,BANK:M};W.get=function(e){return W.hasOwnProperty(e)?W[e]:new o(e)},B.extend(L,{suggest_local:!0});var U={chains:{},on:function(e,t){return this.get(e).push(t),this},get:function(e){var t=this.chains;return t[e]||(t[e]=[])}},H={suggest:{defaultParams:{type:V.getDefaultType(),dataType:"json",contentType:V.getDefaultContentType()},addTypeInUrl:!0},"iplocate/address":{defaultParams:{type:"GET",dataType:"json"},addTypeInUrl:!1},status:{defaultParams:{type:"GET",dataType:"json"},addTypeInUrl:!0},findById:{defaultParams:{type:V.getDefaultType(),dataType:"json",contentType:V.getDefaultContentType()},addTypeInUrl:!0}},Q={suggest:{method:"suggest",userSelect:!0,updateValue:!0,enrichmentEnabled:!0},findById:{method:"findById",userSelect:!1,updateValue:!1,enrichmentEnabled:!1}};r.prototype={initialize:function(){var e=this;e.uniqueId=V.uniqueId("i"),e.createWrapper(),e.notify("initialize"),e.bindWindowEvents(),e.setOptions(),e.inferIsMobile(),e.notify("ready")},deferInitialization:function(){var e,t=this,n="mouseover focus keydown",i=function(){t.initializer.resolve(),t.enable()};t.initializer.always(function(){t.el.off(n,i),clearInterval(e)}),t.disabled=!0,t.el.on(n,i),e=setInterval(function(){t.el.is(":visible")&&i()},t.options.initializeInterval)},isInitialized:function(){return"resolved"===this.initializer.state()},dispose:function(){var e=this;e.initializer.reject(),e.notify("dispose"),e.el.removeData("suggestions").removeClass("suggestions-input"),e.unbindWindowEvents(),e.removeWrapper(),e.el.trigger("suggestions-dispose")},notify:function(t){var n=this,i=V.slice(arguments,1);return e.map(U.get(t),function(e){return e.apply(n,i)})},createWrapper:function(){var t=this;t.$wrapper=e('
'),t.el.after(t.$wrapper),t.$wrapper.on("mousedown"+S,e.proxy(t.onMousedown,t))},removeWrapper:function(){var t=this;t.$wrapper&&t.$wrapper.remove(),e(t.options.$helpers).off(S)},onMousedown:function(t){var n=this;t.preventDefault(),n.cancelBlur=!0,V.delay(function(){delete n.cancelBlur}),0==e(t.target).closest(".ui-menu-item").length&&V.delay(function(){e(document).one("mousedown",function(t){var i=n.el.add(n.$wrapper).add(n.options.$helpers);n.options.floating&&(i=i.add(n.$container)),i=i.filter(function(){return this===t.target||e.contains(this,t.target)}),i.length||n.hide()})})},bindWindowEvents:function(){var t=e.proxy(this.inferIsMobile,this);this.$viewport.on("resize"+S+this.uniqueId,t)},unbindWindowEvents:function(){this.$viewport.off("resize"+S+this.uniqueId)},scrollToTop:function(){var t=this,n=t.options.scrollOnFocus;!0===n&&(n=t.el),n instanceof e&&n.length>0&&e("body,html").animate({scrollTop:n.offset().top},"fast")},setOptions:function(t){var n=this;e.extend(n.options,t),n.type=W.get(n.options.type),e.each({requestMode:Q},function(t,i){if(n[t]=i[n.options[t]],!n[t])throw n.disable(),"`"+t+"` option is incorrect! Must be one of: "+e.map(i,function(e,t){return'"'+t+'"'}).join(", ")}),e(n.options.$helpers).off(S).on("mousedown"+S,e.proxy(n.onMousedown,n)),n.isInitialized()&&n.notify("setOptions")},inferIsMobile:function(){this.isMobile=this.$viewport.width()<=this.options.mobileWidth},clearCache:function(){this.cachedResponse={},this.enrichmentCache={},this.badQueries=[]},clear:function(){var e=this,t=e.selection;e.isInitialized()&&(e.clearCache(),e.currentValue="",e.selection=null,e.hide(),e.suggestions=[],e.el.val(""),e.el.trigger("suggestions-clear"),e.notify("clear"),e.trigger("InvalidateSelection",t))},disable:function(){var e=this;e.disabled=!0,e.abortRequest(),e.visible&&e.hide()},enable:function(){this.disabled=!1},isUnavailable:function(){return this.disabled},update:function(){var e=this,t=e.el.val();e.isInitialized()&&(e.currentValue=t,e.isQueryRequestable(t)?e.updateSuggestions(t):e.hide())},setSuggestion:function(t){var n,i,s=this;e.isPlainObject(t)&&e.isPlainObject(t.data)&&(t=e.extend(!0,{},t),s.isUnavailable()&&s.initializer&&"pending"===s.initializer.state()&&(s.initializer.resolve(),s.enable()),s.bounds.own.length&&(s.checkValueBounds(t),n=s.copyDataComponents(t.data,s.bounds.all),t.data.kladr_id&&(n.kladr_id=s.getBoundedKladrId(t.data.kladr_id,s.bounds.all)),t.data=n),s.selection=t,s.suggestions=[t],i=s.getSuggestionValue(t)||"",s.currentValue=i,s.el.val(i),s.abortRequest(),s.el.trigger("suggestions-set"))},fixData:function(){var t=this,n=t.extendedCurrentValue(),i=t.el.val(),s=e.Deferred();s.done(function(e){t.selectSuggestion(e,0,i,{hasBeenEnriched:!0}),t.el.trigger("suggestions-fixdata",e)}).fail(function(){t.selection=null,t.el.trigger("suggestions-fixdata")}),t.isQueryRequestable(n)?(t.currentValue=n,t.getSuggestions(n,{count:1,from_bound:null,to_bound:null}).done(function(e){var t=e[0];t?s.resolve(t):s.reject()}).fail(function(){s.reject()})):s.reject()},extendedCurrentValue:function(){var t=this,n=t.getParentInstance(),i=n&&n.extendedCurrentValue(),s=e.trim(t.el.val());return V.compact([i,s]).join(" ")},getAjaxParams:function(t,n){var i=this,s=e.trim(i.options.token),o=e.trim(i.options.partner),a=i.options.serviceUrl,u=i.options.url,l=H[t],c=e.extend({timeout:i.options.timeout},l.defaultParams),d={};return u?a=u:(/\/$/.test(a)||(a+="/"),a+=t,l.addTypeInUrl&&(a+="/"+i.type.urlSuffix)),a=V.fixURLProtocol(a),e.support.cors?(s&&(d.Authorization="Token "+s),o&&(d["X-Partner"]=o),d["X-Version"]=r.version,c.headers||(c.headers={}),c.xhrFields||(c.xhrFields={}),e.extend(c.headers,i.options.headers,d),c.xhrFields.withCredentials=!1):(s&&(d.token=s),o&&(d.partner=o),d.version=r.version,a=V.addUrlParams(a,d)),c.url=a,e.extend(c,n)},isQueryRequestable:function(e){var t,n=this;return t=e.length>=n.options.minChars,t&&n.type.isQueryRequestable&&(t=n.type.isQueryRequestable.call(n,e)),t},constructRequestParams:function(t,n){var i=this,s=i.options,o=e.isFunction(s.params)?s.params.call(i.element,t):e.extend({},s.params);return i.type.constructRequestParams&&e.extend(o,i.type.constructRequestParams.call(i)),e.each(i.notify("requestParams"),function(t,n){e.extend(o,n)}),o[s.paramName]=t,e.isNumeric(s.count)&&s.count>0&&(o.count=s.count),s.language&&(o.language=s.language),e.extend(o,n)},updateSuggestions:function(e){var t=this;t.fetchPhase=t.getSuggestions(e).done(function(n){t.assignSuggestions(n,e)})},getSuggestions:function(t,n,i){var s,o=this,r=o.options,a=i&&i.noCallbacks,u=i&&i.useEnrichmentCache,l=i&&i.method||o.requestMode.method,c=o.constructRequestParams(t,n),d=e.param(c||{}),f=e.Deferred();return s=o.cachedResponse[d],s&&e.isArray(s.suggestions)?f.resolve(s.suggestions):o.isBadQuery(t)?f.reject():a||!1!==r.onSearchStart.call(o.element,c)?o.doGetSuggestions(c,l).done(function(e){o.processResponse(e)&&t==o.currentValue?(r.noCache||(u?o.enrichmentCache[t]=e.suggestions[0]:(o.enrichResponse(e,t),o.cachedResponse[d]=e,r.preventBadQueries&&0===e.suggestions.length&&o.badQueries.push(t))),f.resolve(e.suggestions)):f.reject(),a||r.onSearchComplete.call(o.element,t,e.suggestions)}).fail(function(e,n,i){f.reject(),a||"abort"===n||r.onSearchError.call(o.element,t,e,n,i)}):f.reject(),f},doGetSuggestions:function(t,n){var i=this,s=e.ajax(i.getAjaxParams(n,{data:V.serialize(t)}));return i.abortRequest(),i.currentRequest=s,i.notify("request"), + s.always(function(){i.currentRequest=null,i.notify("request")}),s},isBadQuery:function(t){if(!this.options.preventBadQueries)return!1;var n=!1;return e.each(this.badQueries,function(e,i){return!(n=0===t.indexOf(i))}),n},abortRequest:function(){var e=this;e.currentRequest&&e.currentRequest.abort()},processResponse:function(t){var n,i=this;return!(!t||!e.isArray(t.suggestions))&&(i.verifySuggestionsFormat(t.suggestions),i.setUnrestrictedValues(t.suggestions),e.isFunction(i.options.onSuggestionsFetch)&&(n=i.options.onSuggestionsFetch.call(i.element,t.suggestions),e.isArray(n)&&(t.suggestions=n)),!0)},verifySuggestionsFormat:function(t){"string"==typeof t[0]&&e.each(t,function(e,n){t[e]={value:n,data:null}})},getSuggestionValue:function(t,n){var i,s=this,o=s.options.formatSelected||s.type.formatSelected,r=n&&n.hasSameValues,a=n&&n.hasBeenEnriched,u=null;return e.isFunction(o)&&(i=o.call(s,t)),"string"!=typeof i&&(i=t.value,s.type.getSuggestionValue&&null!==(u=s.type.getSuggestionValue(s,{suggestion:t,hasSameValues:r,hasBeenEnriched:a}))&&(i=u)),i},hasSameValues:function(t){var n=!1;return e.each(this.suggestions,function(e,i){if(i.value===t.value&&i!==t)return n=!0,!1}),n},assignSuggestions:function(e,t){var n=this;n.suggestions=e,n.notify("assignSuggestions",t)},shouldRestrictValues:function(){var e=this;return e.options.restrict_value&&e.constraints&&1==Object.keys(e.constraints).length},setUnrestrictedValues:function(t){var n=this,i=n.shouldRestrictValues(),s=n.getFirstConstraintLabel();e.each(t,function(e,t){t.unrestricted_value||(t.unrestricted_value=i?s+", "+t.value:t.value)})},areSuggestionsSame:function(e,t){return e&&t&&e.value===t.value&&V.areSame(e.data,t.data)},getNoSuggestionsHint:function(){var e=this;return!1!==e.options.noSuggestionsHint&&(e.options.noSuggestionsHint||e.type.noSuggestionsHint)}};var Z={setupElement:function(){this.el.attr("autocomplete","off").attr("autocorrect","off").attr("autocapitalize","off").attr("spellcheck","false").addClass("suggestions-input").css("box-sizing","border-box")},bindElementEvents:function(){var t=this;t.el.on("keydown"+S,e.proxy(t.onElementKeyDown,t)),t.el.on(["keyup"+S,"cut"+S,"paste"+S,"input"+S].join(" "),e.proxy(t.onElementKeyUp,t)),t.el.on("blur"+S,e.proxy(t.onElementBlur,t)),t.el.on("focus"+S,e.proxy(t.onElementFocus,t))},unbindElementEvents:function(){this.el.off(S)},onElementBlur:function(){var e=this;if(e.cancelBlur)return void(e.cancelBlur=!1);e.options.triggerSelectOnBlur?e.isUnavailable()||e.selectCurrentValue({noSpace:!0}).always(function(){e.hide()}):e.hide(),e.fetchPhase.abort&&e.fetchPhase.abort()},onElementFocus:function(){var t=this;t.cancelFocus||V.delay(e.proxy(t.completeOnFocus,t)),t.cancelFocus=!1},onElementKeyDown:function(e){var t=this;if(!t.isUnavailable())if(t.visible){switch(e.which){case b.ESC:t.el.val(t.currentValue),t.hide(),t.abortRequest();break;case b.TAB:if(!1===t.options.tabDisabled)return;break;case b.ENTER:t.options.triggerSelectOnEnter&&t.selectCurrentValue();break;case b.SPACE:return void(t.options.triggerSelectOnSpace&&t.isCursorAtEnd()&&(e.preventDefault(),t.selectCurrentValue({continueSelecting:!0,dontEnrich:!0}).fail(function(){t.currentValue+=" ",t.el.val(t.currentValue),t.proceedChangedValue()})));case b.UP:t.moveUp();break;case b.DOWN:t.moveDown();break;default:return}e.stopImmediatePropagation(),e.preventDefault()}else switch(e.which){case b.DOWN:t.suggest();break;case b.ENTER:t.options.triggerSelectOnEnter&&t.triggerOnSelectNothing()}},onElementKeyUp:function(e){var t=this;if(!t.isUnavailable()){switch(e.which){case b.UP:case b.DOWN:case b.ENTER:return}clearTimeout(t.onChangeTimeout),t.inputPhase.reject(),t.currentValue!==t.el.val()&&t.proceedChangedValue()}},proceedChangedValue:function(){var t=this;t.abortRequest(),t.inputPhase=e.Deferred().done(e.proxy(t.onValueChange,t)),t.options.deferRequestBy>0?t.onChangeTimeout=V.delay(function(){t.inputPhase.resolve()},t.options.deferRequestBy):t.inputPhase.resolve()},onValueChange:function(){var e,t=this;t.selection&&(e=t.selection,t.selection=null,t.trigger("InvalidateSelection",e)),t.selectedIndex=-1,t.update(),t.notify("valueChange")},completeOnFocus:function(){var e=this;e.isUnavailable()||e.isElementFocused()&&(e.update(),e.isMobile&&(e.setCursorAtEnd(),e.scrollToTop()))},isElementFocused:function(){return document.activeElement===this.element},isElementDisabled:function(){return Boolean(this.element.getAttribute("disabled")||this.element.getAttribute("readonly"))},isCursorAtEnd:function(){var e,t,n=this,i=n.el.val().length;try{if("number"==typeof(e=n.element.selectionStart))return e===i}catch(e){}return!document.selection||(t=document.selection.createRange(),t.moveStart("character",-i),i===t.text.length)},setCursorAtEnd:function(){var e=this.element;try{e.selectionEnd=e.selectionStart=e.value.length,e.scrollLeft=e.scrollWidth}catch(t){e.value=e.value}}};e.extend(r.prototype,Z),U.on("initialize",Z.bindElementEvents).on("dispose",Z.unbindElementEvents);var K={};a();var G={checkStatus:function(){function e(e){V.isFunction(t.options.onSearchError)&&t.options.onSearchError.call(t.element,null,s,"error",e)}var t=this,n=t.options.token&&t.options.token.trim()||"",i=t.options.type+n,s=K[i];s||(s=K[i]=B.ajax(t.getAjaxParams("status"))),s.done(function(n,i,s){if(n.search){var o=s.getResponseHeader("X-Plan");n.plan=o,B.extend(t.status,n)}else e("Service Unavailable")}).fail(function(){e(s.statusText)})}};r.resetTokens=a,B.extend(r.prototype,G),U.on("setOptions",G.checkStatus);var J,X=!0,Y={checkLocation:function(){var t=this,n=t.options.geoLocation;t.type.geoEnabled&&n&&(t.geoLocation=e.Deferred(),e.isPlainObject(n)||e.isArray(n)?t.geoLocation.resolve(n):(J||(J=e.ajax(t.getAjaxParams("iplocate/address"))),J.done(function(e){var n=e&&e.location&&e.location.data;n&&n.kladr_id?t.geoLocation.resolve({kladr_id:n.kladr_id}):t.geoLocation.reject()}).fail(function(){t.geoLocation.reject()})))},getGeoLocation:function(){return this.geoLocation},constructParams:function(){var t=this,n={};return t.geoLocation&&e.isFunction(t.geoLocation.promise)&&"resolved"==t.geoLocation.state()&&t.geoLocation.done(function(t){n.locations_boost=e.makeArray(t)}),n}};"GET"!=V.getDefaultType()&&(e.extend(L,{geoLocation:X}),e.extend(r,{resetLocation:u}),e.extend(r.prototype,{getGeoLocation:Y.getGeoLocation}),U.on("setOptions",Y.checkLocation).on("requestParams",Y.constructParams));var ee={enrichSuggestion:function(t,n){var i=this,s=e.Deferred();if(!i.options.enrichmentEnabled||!i.type.enrichmentEnabled||!i.requestMode.enrichmentEnabled||n&&n.dontEnrich)return s.resolve(t);if(t.data&&null!=t.data.qc)return s.resolve(t);i.disableDropdown();var o=i.type.getEnrichmentQuery(t),r=i.type.enrichmentParams,a={noCallbacks:!0,useEnrichmentCache:!0,method:i.type.enrichmentMethod};return i.currentValue=o,i.enrichPhase=i.getSuggestions(o,r,a).always(function(){i.enableDropdown()}).done(function(e){var n=e&&e[0];s.resolve(n||t,!!n)}).fail(function(){s.resolve(t)}),s},enrichResponse:function(t,n){var i=this,s=i.enrichmentCache[n];s&&e.each(t.suggestions,function(e,i){if(i.value===n)return t.suggestions[e]=s,!1})}};e.extend(r.prototype,ee);var te={width:"auto",floating:!1},ne={createContainer:function(){var t=this,n="."+t.classes.suggestion,i=t.options,s=e("
").addClass(i.containerClass).css({display:"none"});t.$container=s,s.on("click"+S,n,e.proxy(t.onSuggestionClick,t))},showContainer:function(){this.$container.appendTo(this.options.floating?this.$body:this.$wrapper)},getContainer:function(){return this.$container.get(0)},removeContainer:function(){var e=this;e.options.floating&&e.$container.remove()},setContainerOptions:function(){var t=this;t.$container.off("mousedown.suggestions"),t.options.floating&&t.$container.on("mousedown.suggestions",e.proxy(t.onMousedown,t))},onSuggestionClick:function(t){var n,i=this,s=e(t.target);if(!i.dropdownDisabled){for(i.cancelFocus=!0,i.el.focus();s.length&&!(n=s.attr("data-index"));)s=s.closest("."+i.classes.suggestion);n&&!isNaN(n)&&i.select(+n)}},getSuggestionsItems:function(){return this.$container.children("."+this.classes.suggestion)},toggleDropdownEnabling:function(e){this.dropdownDisabled=!e,this.$container.attr("disabled",!e)},disableDropdown:function(){this.toggleDropdownEnabling(!1)},enableDropdown:function(){this.toggleDropdownEnabling(!0)},hasSuggestionsToChoose:function(){var t=this;return t.suggestions.length>1||1===t.suggestions.length&&(!t.selection||e.trim(t.suggestions[0].value)!==e.trim(t.selection.value))},suggest:function(){var t=this,n=t.options,i=[];if(t.requestMode.userSelect){if(t.hasSuggestionsToChoose())n.hint&&t.suggestions.length&&i.push('
'+n.hint+"
"),t.selectedIndex=-1,t.suggestions.forEach(function(e,n){e==t.selection&&(t.selectedIndex=n),t.buildSuggestionHtml(e,n,i)});else{if(t.suggestions.length)return void t.hide();var s=t.getNoSuggestionsHint();if(!s)return void t.hide();i.push('
'+s+"
")}i.push('
'),i.push("
"),t.$container.html(i.join("")),n.autoSelectFirst&&-1===t.selectedIndex&&(t.selectedIndex=0),-1!==t.selectedIndex&&t.getSuggestionsItems().eq(t.selectedIndex).addClass(t.classes.selected),e.isFunction(n.beforeRender)&&n.beforeRender.call(t.element,t.$container),t.$container.show(),t.visible=!0}},buildSuggestionHtml:function(e,t,n){n.push('
');var i=this.options.formatResult||this.type.formatResult||this.formatResult;n.push(i.call(this,e.value,this.currentValue,e,{unformattableTokens:this.type.unformattableTokens}));var s=this.makeSuggestionLabel(this.suggestions,e);s&&n.push(''+V.escapeHtml(s)+""),n.push("
")},wrapFormattedValue:function(e,t){var n=this,i=V.getDeepValue(t.data,"state.status");return'"+e+""},formatResult:function(e,t,n,i){var s=this;return e=s.highlightMatches(e,t,n,i),s.wrapFormattedValue(e,n)},highlightMatches:function(t,n,i,s){var o,r,a,u,d,f,p,g=this,h=[],y=s&&s.unformattableTokens,m=s&&s.maxLength,_=V.reWordExtractor();if(!t)return"";for(o=k.tokenize(n,y),r=e.map(o,function(e){return new RegExp("^((.*)([\\-\\+\\\\\\?!@#$%^&]+))?("+V.escapeRegExChars(e)+")([^\\-\\+\\\\\\?!@#$%^&]*[\\-\\+\\\\\\?!@#$%^&]*)","i")});(a=_.exec(t))&&a[0];)u=a[1],h.push({text:u,hasUpperCase:u.toLowerCase()!==u,formatted:V.formatToken(u),matchable:!0}),a[2]&&h.push({text:a[2]});for(d=0;dn&&(h.splice(s,0,{text:f.text.substr(n),formatted:f.formatted.substr(n),matchable:!0}),f.text=f.text.substr(0,n),f.formatted=f.formatted.substr(0,n)),i.after&&(n=i.text.length,h.splice(s,0,{text:f.text.substr(n),formatted:f.formatted.substr(n)}),f.text=f.text.substr(0,n),f.formatted=f.formatted.substr(0,n)),f.matched=!0,!1});if(m){for(d=0;d=0;d++)f=h[d],(m-=f.text.length)<0&&(f.text=f.text.substr(0,f.text.length+m)+"...");h.length=d}return p=l(h),c(p,g.classes.nowrap)},makeSuggestionLabel:function(t,n){var i,s,o=this,r=o.type.fieldNames,a={},u=V.reWordExtractor(),l=[];if(r&&d(t,n)&&n.data&&(e.each(r,function(e){var t=n.data[e];t&&(a[e]=V.formatToken(t))}),!e.isEmptyObject(a))){for(;(i=u.exec(V.formatToken(n.value)))&&(s=i[1]);)e.each(a,function(e,t){if(t==s)return l.push(r[e]),delete a[e],!1});if(l.length)return l.join(", ")}},hide:function(){var e=this;e.visible=!1,e.selectedIndex=-1,e.$container.hide().empty()},activate:function(e){var t,n,i=this,s=i.classes.selected;return!i.dropdownDisabled&&(n=i.getSuggestionsItems(),n.removeClass(s),i.selectedIndex=e,-1!==i.selectedIndex&&n.length>i.selectedIndex)?(t=n.eq(i.selectedIndex),t.addClass(s),t):null},deactivate:function(e){var t=this;t.dropdownDisabled||(t.selectedIndex=-1,t.getSuggestionsItems().removeClass(t.classes.selected),e&&t.el.val(t.currentValue))},moveUp:function(){var e=this;if(!e.dropdownDisabled)return-1===e.selectedIndex?void(e.suggestions.length&&e.adjustScroll(e.suggestions.length-1)):0===e.selectedIndex?void e.deactivate(!0):void e.adjustScroll(e.selectedIndex-1)},moveDown:function(){var e=this;if(!e.dropdownDisabled)return e.selectedIndex===e.suggestions.length-1?void e.deactivate(!0):void e.adjustScroll(e.selectedIndex+1)},adjustScroll:function(e){var t,n,i,s=this,o=s.activate(e),r=s.$container.scrollTop();o&&o.length&&(t=o.position().top,t<0?s.$container.scrollTop(r+t):(n=t+o.outerHeight(),i=s.$container.innerHeight(),n>i&&s.$container.scrollTop(r-i+n)),s.el.val(s.suggestions[e].value))}};e.extend(L,te),e.extend(r.prototype,ne),U.on("initialize",ne.createContainer).on("dispose",ne.removeContainer).on("setOptions",ne.setContainerOptions).on("ready",ne.showContainer).on("assignSuggestions",ne.suggest);var ie={constraints:null,restrict_value:!1},se=["country_iso_code","region_iso_code","region_fias_id","area_fias_id","city_fias_id","city_district_fias_id","settlement_fias_id","planning_structure_fias_id","street_fias_id"],oe=function(e,t){var n,i,s=this,o={};s.instance=t,s.fields={},s.specificity=-1,y.isPlainObject(e)&&t.type.dataComponents&&m.each(t.type.dataComponents,function(t,n){var i=t.id;t.forLocations&&e[i]&&(s.fields[i]=e[i],s.specificity=n)}),n=Object.keys(s.fields),i=m.intersect(n,se),i.length?(m.each(i,function(e,t){o[e]=s.fields[e]}),s.fields=o,s.specificity=s.getFiasSpecificity(i)):s.fields.kladr_id&&(s.fields={kladr_id:s.fields.kladr_id},s.significantKladr=p(s.fields.kladr_id),s.specificity=s.getKladrSpecificity(s.significantKladr))};B.extend(oe.prototype,{getLabel:function(){return this.instance.type.composeValue(this.fields,{saveCityDistrict:!0})},getFields:function(){return this.fields},isValid:function(){return!y.isEmptyObject(this.fields)},getKladrSpecificity:function(e){var t=-1,n=e.length;return m.each(this.instance.type.dataComponents,function(e,i){e.kladrFormat&&n===e.kladrFormat.digits&&(t=i)}),t},getFiasSpecificity:function(e){var t=-1;return m.each(this.instance.type.dataComponents,function(n,i){n.fiasType&&e.indexOf(n.fiasType)>-1&&t0},getFields:function(){return this.locations.map(function(e){return e.getFields()})}});var ae={createConstraints:function(){this.constraints={}},setupConstraints:function(){var e,t=this,n=t.options.constraints;if(!n)return void t.unbindFromParent();B.isJqObject(n)||"string"==typeof n||"number"==typeof n.nodeType?(e=B.select(n),e.is(t.constraints)||(t.unbindFromParent(),e.is(t.el)||(t.constraints=e,t.bindToParent()))):(m.each(t.constraints,function(e,n){t.removeConstraint(n)}),m.each(m.makeArray(n),function(e,n){t.addConstraint(e)}))},filteredLocation:function(e){var t=[],n={};if(m.each(this.type.dataComponents,function(){this.forLocations&&t.push(this.id)}),y.isPlainObject(e)&&m.each(e,function(e,i){e&&t.indexOf(i)>=0&&(n[i]=e)}),!y.isEmptyObject(n))return n.kladr_id?{kladr_id:n.kladr_id}:n},addConstraint:function(e){var t=this;e=new re(e,t),e.isValid()&&(t.constraints[e.id]=e)},removeConstraint:function(e){delete this.constraints[e]},constructConstraintsParams:function(){for(var e,t,n=this,i=[],s=n.constraints,o={};B.isJqObject(s)&&(e=s.suggestions())&&!(t=v.getDeepValue(e,"selection.data"));)s=e.constraints;return B.isJqObject(s)?(t=new oe(t,e).getFields())&&(n.bounds.own.indexOf("city")>-1&&delete t.city_fias_id,o.locations=[t],o.restrict_value=!0):s&&(m.each(s,function(e,t){i=i.concat(e.getFields())}),i.length&&(o.locations=i,o.restrict_value=n.options.restrict_value)),o},getFirstConstraintLabel:function(){var e=this,t=y.isPlainObject(e.constraints)&&Object.keys(e.constraints)[0];return t?e.constraints[t].label:""},bindToParent:function(){var e=this;e.constraints.on(["suggestions-select."+e.uniqueId,"suggestions-invalidateselection."+e.uniqueId,"suggestions-clear."+e.uniqueId].join(" "),B.proxy(e.onParentSelectionChanged,e)).on("suggestions-dispose."+e.uniqueId,B.proxy(e.onParentDispose,e))},unbindFromParent:function(){var e=this,t=e.constraints;B.isJqObject(t)&&t.off("."+e.uniqueId)},onParentSelectionChanged:function(e,t,n){("suggestions-select"!==e.type||n)&&this.clear()},onParentDispose:function(e){this.unbindFromParent()},getParentInstance:function(){return B.isJqObject(this.constraints)&&this.constraints.suggestions()},shareWithParent:function(e){var t=this.getParentInstance();t&&t.type===this.type&&!f(e,t)&&(t.shareWithParent(e),t.setSuggestion(e))},getUnrestrictedData:function(e){var t=this,n=[],i={},s=-1;return m.each(t.constraints,function(t,n){m.each(t.locations,function(t,n){t.containsData(e)&&t.specificity>s&&(s=t.specificity)})}),s>=0?(e.region_kladr_id&&e.region_kladr_id===e.city_kladr_id&&n.push.apply(n,t.type.dataComponentsById.city.fields),m.each(t.type.dataComponents.slice(0,s+1),function(e,t){n.push.apply(n,e.fields)}),m.each(e,function(e,t){-1===n.indexOf(t)&&(i[t]=e)})):i=e,i}};B.extend(L,ie),B.extend(r.prototype,ae),"GET"!=T.getDefaultType()&&U.on("initialize",ae.createConstraints).on("setOptions",ae.setupConstraints).on("requestParams",ae.constructConstraintsParams).on("dispose",ae.unbindFromParent);var ue={proceedQuery:function(e){var t=this;e.length>=t.options.minChars?t.updateSuggestions(e):t.hide()},selectCurrentValue:function(e){var t=this,n=B.Deferred();return t.inputPhase.resolve(),t.fetchPhase.done(function(){var i;t.selection&&!t.visible?n.reject():(i=t.findSuggestionIndex(),t.select(i,e),-1===i?n.reject():n.resolve(i))}).fail(function(){n.reject()}),n},selectFoundSuggestion:function(){var e=this;e.requestMode.userSelect||e.select(0)},findSuggestionIndex:function(){var e,t=this,n=t.selectedIndex;return-1===n&&(e=t.el.val().trim())&&t.type.matchers.some(function(i){return-1!==(n=i(e,t.suggestions))}),n},select:function(e,t){var n,i=this,s=i.suggestions[e],o=t&&t.continueSelecting,r=i.currentValue;if(!i.triggering.Select){if(!s)return o||i.selection||i.triggerOnSelectNothing(),void i.onSelectComplete(o);n=i.hasSameValues(s),i.enrichSuggestion(s,t).done(function(s,o){var a=B.extend({hasBeenEnriched:o,hasSameValues:n},t);i.selectSuggestion(s,e,r,a)})}},selectSuggestion:function(e,t,n,i){var s=this,o=i.continueSelecting,r=!s.type.isDataComplete||s.type.isDataComplete.call(s,e),a=s.selection;s.triggering.Select||(s.type.alwaysContinueSelecting&&(o=!0),r&&(o=!1),i.hasBeenEnriched&&s.suggestions[t]&&(s.suggestions[t].data=e.data),s.requestMode.updateValue&&(s.checkValueBounds(e),s.currentValue=s.getSuggestionValue(e,i),!s.currentValue||i.noSpace||r||(s.currentValue+=" "),s.el.val(s.currentValue)),s.currentValue?(s.selection=e,s.areSuggestionsSame(e,a)||s.trigger("Select",e,s.currentValue!=n),s.requestMode.userSelect&&s.onSelectComplete(o)):(s.selection=null,s.triggerOnSelectNothing()),s.shareWithParent(e))},onSelectComplete:function(e){var t=this;e?(t.selectedIndex=-1,t.updateSuggestions(t.currentValue)):t.hide()},triggerOnSelectNothing:function(){var e=this;e.triggering.SelectNothing||e.trigger("SelectNothing",e.currentValue)},trigger:function(e){var t=this,n=V.slice(arguments,1),i=t.options["on"+e];t.triggering[e]=!0,V.isFunction(i)&&i.apply(t.element,n),t.el.trigger.call(t.el,"suggestions-"+e.toLowerCase(),n),t.triggering[e]=!1}};B.extend(r.prototype,ue),U.on("assignSuggestions",ue.selectFoundSuggestion);var le={bounds:null},ce={setupBounds:function(){this.bounds={from:null,to:null}},setBoundsOptions:function(){var t,n,i=this,s=[],o=e.trim(i.options.bounds).split("-"),r=o[0],a=o[o.length-1],u=[],l=[];i.type.dataComponents&&e.each(i.type.dataComponents,function(){this.forBounds&&s.push(this.id)}),-1===e.inArray(r,s)&&(r=null),n=e.inArray(a,s),-1!==n&&n!==s.length-1||(a=null),(r||a)&&(t=!r,e.each(s,function(e,n){if(n==r&&(t=!0),l.push(n),t&&u.push(n),n==a)return!1})),i.bounds.from=r,i.bounds.to=a,i.bounds.all=l,i.bounds.own=u},constructBoundsParams:function(){var e=this,t={};return e.bounds.from&&(t.from_bound={value:e.bounds.from}),e.bounds.to&&(t.to_bound={value:e.bounds.to}),t},checkValueBounds:function(e){var t,n=this;if(n.bounds.own.length&&n.type.composeValue){var i=n.bounds.own.slice(0);1===i.length&&"city_district"===i[0]&&i.push("city_district_fias_id"),t=n.copyDataComponents(e.data,i),e.value=n.type.composeValue(t)}},copyDataComponents:function(t,n){var i={},s=this.type.dataComponentsById;return s&&e.each(n,function(n,o){e.each(s[o].fields,function(e,n){null!=t[n]&&(i[n]=t[n])})}),i},getBoundedKladrId:function(t,n){var i,s=n[n.length-1];return e.each(this.type.dataComponents,function(e,t){if(t.id===s)return i=t.kladrFormat,!1}),t.substr(0,i.digits)+new Array((i.zeros||0)+1).join("0")}};e.extend(L,le),e.extend(r.prototype,ce),U.on("initialize",ce.setupBounds).on("setOptions",ce.setBoundsOptions).on("requestParams",ce.constructBoundsParams);var de={selectByClass:function(e,t){var n="."+e;return t?t.querySelector(n):document.querySelector(n)},addClass:function(e,t){var n=e.className.split(" ");-1===n.indexOf(t)&&n.push(t),e.className=n.join(" ")},setStyle:function(e,t,n){e.style[t]=n},listenTo:function(e,t,n,i){e.addEventListener(t,i,!1),n&&(eventsByNamespace[n]||(eventsByNamespace[n]=[]),eventsByNamespace[n].push({eventName:t,element:e,callback:i}))},stopListeningNamespace:function(e){var t=eventsByNamespace[e];t&&t.forEach(function(e){e.element.removeEventListener(e.eventName,e.callback,!1)})}};g.prototype.show=function(){"FREE"===this.plan&&this.element&&(this.setStyles(),this.setHtml())},g.prototype.setStyles=function(){this.element.style.display="block"},g.prototype.setHtml=function(){this.element.innerHTML='dadata-logo'},U.on("assignSuggestions",h),r.defaultOptions=L,r.version="20.3.0",e.Suggestions=r,e.fn.suggestions=function(t,n){return 0===arguments.length?this.first().data("suggestions"):this.each(function(){var i=e(this),s=i.data("suggestions");"string"==typeof t?s&&"function"==typeof s[t]&&s[t](n):(s&&s.dispose&&s.dispose(),s=new r(this,t),i.data("suggestions",s))})}}); \ No newline at end of file diff --git a/docs/library-template/templates/vendor/assets/stylesheets/jquery.json-viewer.scss b/docs/library-template/templates/vendor/assets/stylesheets/jquery.json-viewer.scss new file mode 100644 index 0000000..2c72ae5 --- /dev/null +++ b/docs/library-template/templates/vendor/assets/stylesheets/jquery.json-viewer.scss @@ -0,0 +1,47 @@ +/* Syntax highlighting for JSON objects */ +ul.json-dict, ol.json-array { + list-style-type: none; + margin: 0 0 0 1px; + border-left: 1px dotted #ccc; + padding-left: 2em; +} +.json-string { + color: #0B7500; +} +.json-literal { + color: #1A01CC; + font-weight: bold; +} + +/* Toggle button */ +a.json-toggle { + position: relative; + color: inherit; + text-decoration: none; +} +a.json-toggle:focus { + outline: none; +} +a.json-toggle:before { + color: #aaa; + content: "\25BC"; /* down arrow */ + position: absolute; + display: inline-block; + width: 1em; + left: -1em; +} +a.json-toggle.collapsed:before { + transform: rotate(-90deg); /* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */ + -ms-transform: rotate(-90deg); + -webkit-transform: rotate(-90deg); +} + +/* Collapsable placeholder links */ +a.json-placeholder { + color: #aaa; + padding: 0 1em; + text-decoration: none; +} +a.json-placeholder:hover { + text-decoration: underline; +} diff --git a/docs/library-template/templates/vendor/assets/stylesheets/suggestions.min.css b/docs/library-template/templates/vendor/assets/stylesheets/suggestions.min.css new file mode 100644 index 0000000..3f5956f --- /dev/null +++ b/docs/library-template/templates/vendor/assets/stylesheets/suggestions.min.css @@ -0,0 +1 @@ +.suggestions-nowrap{white-space:nowrap}.suggestions-input{-ms-box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.suggestions-input::-ms-clear{display:none}.suggestions-wrapper{position:relative;margin:0;padding:0;vertical-align:top;-webkit-text-size-adjust:100%}.suggestions-suggestions{background:#fff;border:1px solid #999;-ms-box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:default;left:0;min-width:100%;position:absolute;z-index:9999;-webkit-text-size-adjust:100%}.suggestions-suggestions strong{font-weight:400;color:#39f}.suggestions-suggestions.suggestions-mobile{border-style:none}.suggestions-suggestions.suggestions-mobile .suggestions-suggestion{border-bottom:1px solid #ddd}.suggestions-suggestion{padding:4px 4px;overflow:hidden}.suggestions-suggestion:hover{background:#f7f7f7}.suggestions-selected{background:#f0f0f0}.suggestions-selected:hover{background:#f0f0f0}.suggestions-hint{padding:4px 4px;white-space:nowrap;overflow:hidden;color:#777;font-size:85%;line-height:20px}.suggestions-subtext{color:#777}.suggestions-subtext_inline{display:inline-block;min-width:6em;vertical-align:bottom;margin:0 .5em 0 0}.suggestions-subtext-delimiter{display:inline-block;width:2px}.suggestions-subtext_label{margin:0 0 0 .25em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:0 3px;background:#f5f5f5;font-size:85%}.suggestions-value[data-suggestion-status=LIQUIDATED]{position:relative}.suggestions-value[data-suggestion-status=LIQUIDATED]:after{position:absolute;left:0;right:0;top:50%;border-top:1px solid rgba(0,0,0,.4);content:""}.suggestions-promo{font-size:85%;display:none;color:#777;padding:4px;text-align:center}.suggestions-promo a{color:#777;display:block;filter:grayscale(100%);line-height:20px;text-decoration:none}.suggestions-promo a:hover{filter:grayscale(0)}.suggestions-promo svg{height:20px;vertical-align:bottom}@media screen and (min-width:600px){.suggestions-promo{position:absolute;top:0;right:0;text-align:left}} \ No newline at end of file diff --git a/docs/thor-generator/.rubocop.yml b/docs/thor-generator/.rubocop.yml new file mode 100644 index 0000000..34f0c11 --- /dev/null +++ b/docs/thor-generator/.rubocop.yml @@ -0,0 +1,7 @@ +--- + +Style/Documentation: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false diff --git a/docs/thor-generator/Gemfile b/docs/thor-generator/Gemfile new file mode 100644 index 0000000..028b878 --- /dev/null +++ b/docs/thor-generator/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'thor' diff --git a/docs/thor-generator/Gemfile.lock b/docs/thor-generator/Gemfile.lock new file mode 100644 index 0000000..fa1b4b4 --- /dev/null +++ b/docs/thor-generator/Gemfile.lock @@ -0,0 +1,14 @@ +GEM + remote: https://rubygems.org/ + specs: + thor (1.3.2) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + thor + +BUNDLED WITH + 2.5.23 diff --git a/docs/thor-generator/config/rails_new_config.rb b/docs/thor-generator/config/rails_new_config.rb new file mode 100644 index 0000000..766db87 --- /dev/null +++ b/docs/thor-generator/config/rails_new_config.rb @@ -0,0 +1,35 @@ +module RailsNewConfig + SKIP_OPTIONS = [ + ['action_mailer', 'Action Mailer'], + ['active_job', 'Active Job'], + ['active_storage', 'Active Storage'], + ['action_cable', 'Action Cable'], + ['solid', 'Solid'], + ].freeze + + # Actual options for Rails 8.0 [rails new --help] + DEFAULT_OPTIONS = [ + '--skip-git', # Skip git init, .gitignore and .gitattributes + '--skip-docker', # Skip Dockerfile, .dockerignore and bin/docker-entrypoint + '--skip-action-mailbox', # Skip Action Mailbox gem + '--skip-action-text', # Skip Action Text gem + '--skip-asset-pipeline', # Indicates when to generate skip asset pipeline + '--skip-javascript', # Skip JavaScript files + '--skip-hotwire', # Skip Hotwire integration + '--skip-jbuilder', # Skip jbuilder gem + '--skip-test', # Skip test files + '--skip-system-test', # Skip system test files + '--skip-bootsnap', # Skip bootsnap gem + '--skip-dev-gems', # Skip development gems (e.g., web-console) + '--skip-thruster', # Skip Thruster setup + '--skip-rubocop', # Skip RuboCop setup + '--skip-ci', # Skip GitHub CI files + '--skip-kamal', # Skip Kamal setup + '--skip-dev', # Set up the application with Gemfile pointing to your Rails checkout + '--skip-devcontainer', # Generate devcontainer files + '--skip-edge', # Set up the application with a Gemfile pointing to the 8-0-stable branch on the Rails repository + '--skip-main', # Set up the application with Gemfile pointing to Rails repository main branch + '--api', # Preconfigure smaller stack for API only apps + '--minimal' # Preconfigure a minimal rails app + ].freeze +end diff --git a/docs/thor-generator/lib/app_configuration.rb b/docs/thor-generator/lib/app_configuration.rb new file mode 100644 index 0000000..87cc0ac --- /dev/null +++ b/docs/thor-generator/lib/app_configuration.rb @@ -0,0 +1,15 @@ +class AppConfiguration + attr_accessor :app_path, :database, :skip_options + + def initialize + @skip_options = [] + end + + def self.create + config = new + config.app_path = Prompts.ask_app_path + config.database = Prompts.ask_database_choice + config.skip_options = Prompts.ask_skip_options + config + end +end diff --git a/docs/thor-generator/lib/prompts.rb b/docs/thor-generator/lib/prompts.rb new file mode 100644 index 0000000..d227d2d --- /dev/null +++ b/docs/thor-generator/lib/prompts.rb @@ -0,0 +1,29 @@ +require 'thor' + +module Prompts + module_function + + include Thor::Shell + + def ask_app_path + loop do + path = Thor::Shell::Basic.new.ask('What will be the path of your application?').strip + return path unless path.empty? + + Thor::Shell::Basic.new.say('Error: Path cannot be empty. Please enter a valid path.', :red) + end + end + + def ask_database_choice + 'postgresql' + end + + def ask_skip_options + shell = Thor::Shell::Basic.new + RailsNewConfig::SKIP_OPTIONS.each_with_object([]) do |(option, description), skip_list| + answer = shell.ask("Would you like to skip '#{description}'? (yes/no, default: yes)").strip + answer = 'yes' if answer.empty? + skip_list << "--skip-#{option}" if answer.downcase.start_with?('y') + end + end +end diff --git a/docs/thor-generator/rails_new.rb b/docs/thor-generator/rails_new.rb new file mode 100644 index 0000000..bb0a4e9 --- /dev/null +++ b/docs/thor-generator/rails_new.rb @@ -0,0 +1,40 @@ +require 'erb' +require 'thor' +require_relative 'config/rails_new_config' +require_relative 'lib/app_configuration' +require_relative 'lib/prompts' +require_relative 'services/rails_installer' +require_relative 'services/command_builder' +require_relative 'services/gemfile_updater' +require_relative 'services/makefile_generator' + +class RailsNewCLI < Thor + desc 'generate', 'Generate a new Rails app with custom options' + def generate + RailsInstaller.check_and_install + + config = AppConfiguration.create + + generate_application(config) + end + + private + + def generate_application(config) + execute_rails_command(config) + generate_additional_files(config) + end + + def execute_rails_command(config) + command = CommandBuilder.build(config) + say("Running: #{command}", :yellow) + system(command) || abort('Failed to create Rails application') + end + + def generate_additional_files(config) + MakefileGenerator.generate(config) + GemfileUpdater.update(config.app_path) + end +end + +RailsNewCLI.start(ARGV) diff --git a/docs/thor-generator/services/command_builder.rb b/docs/thor-generator/services/command_builder.rb new file mode 100644 index 0000000..22b54f2 --- /dev/null +++ b/docs/thor-generator/services/command_builder.rb @@ -0,0 +1,10 @@ +class CommandBuilder + def self.build(config) + [ + "rails new #{config.app_path}", + "-d #{config.database}", + *RailsNewConfig::DEFAULT_OPTIONS, + *config.skip_options + ].join(' ') + end +end diff --git a/docs/thor-generator/services/gemfile_updater.rb b/docs/thor-generator/services/gemfile_updater.rb new file mode 100644 index 0000000..88e16fa --- /dev/null +++ b/docs/thor-generator/services/gemfile_updater.rb @@ -0,0 +1,50 @@ +class GemfileUpdater + def self.update(app_path) + new(app_path).update + end + + def initialize(app_path) + @app_path = app_path + @gemfile_path = File.join(app_path, 'Gemfile') + end + + def update + content = File.read(@gemfile_path) + updated_content = insert_development_gems(content) + File.write(@gemfile_path, updated_content) + puts "Gemfile has been updated in #{@gemfile_path}" + end + + private + + def insert_development_gems(content) + if content =~ /group :development do/ + insert_into_existing_group(content) + else + append_new_group(content) + end + end + + def insert_into_existing_group(content) + content.sub(/group :development do\s*/) do |match| + "#{match}#{development_gems}" + end + end + + def append_new_group(content) + content + "\n\ngroup :development do\n#{development_gems}end\n" + end + + def development_gems + @development_gems ||= <<~GEMS + gem 'database_consistency', require: false + gem 'ruboclean', require: false + gem 'rubocop', require: false + gem 'rubocop-factory_bot', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + gem 'rubocop-rspec_rails', require: false + GEMS + end +end diff --git a/docs/thor-generator/services/makefile_generator.rb b/docs/thor-generator/services/makefile_generator.rb new file mode 100644 index 0000000..5748204 --- /dev/null +++ b/docs/thor-generator/services/makefile_generator.rb @@ -0,0 +1,39 @@ +class MakefileGenerator + attr_reader :skip_brakeman + + def self.generate(config) + new(config).generate + end + + def initialize(config) + @config = config + @app_path = config.app_path + @skip_brakeman = config.skip_options&.include?('--skip-brakeman') + end + + def generate + content = render_template + write_makefile(content) + puts "Makefile has been generated in #{makefile_path}" + end + + private + + def render_template + template = File.read(template_path) + erb = ERB.new(template, trim_mode: '-') + erb.result(binding) + end + + def template_path + File.join(File.dirname(__dir__), 'templates', 'Makefile.erb') + end + + def makefile_path + File.join(@app_path, 'Makefile') + end + + def write_makefile(content) + File.write(makefile_path, content) + end +end diff --git a/docs/thor-generator/services/rails_installer.rb b/docs/thor-generator/services/rails_installer.rb new file mode 100644 index 0000000..67c6557 --- /dev/null +++ b/docs/thor-generator/services/rails_installer.rb @@ -0,0 +1,100 @@ +require 'English' +require 'net/http' +require 'json' +require 'thor' + +class RailsInstaller + class InstallationError < StandardError; end + + RUBYGEMS_API_URL = 'https://rubygems.org/api/v1/gems/rails.json'.freeze + + def initialize(shell: Thor::Shell::Color.new) + @shell = shell + end + + def self.check_and_install + new.check_and_install + end + + def check_and_install + if rails_installed? + check_for_update + else + handle_missing_rails + end + end + + private + + attr_reader :shell + + def rails_installed? + Gem::Specification.find_by_name('rails') + true + rescue Gem::LoadError + false + end + + def handle_missing_rails + display_message(:red, 'The `rails` gem is not installed.') + install_rails if prompt_user?('Would you like to install it now?') + end + + def check_for_update + current_version = fetch_current_version + latest_version = fetch_latest_version + + return unless latest_version + return display_current_version(current_version) if latest_version <= current_version + + handle_update(current_version, latest_version) + end + + def fetch_current_version + Gem::Specification.find_by_name('rails').version + end + + def fetch_latest_version + response = Net::HTTP.get(URI(RUBYGEMS_API_URL)) + data = JSON.parse(response) + Gem::Version.new(data['version']) + rescue StandardError => e + display_message(:red, "Error fetching latest Rails version: #{e.message}") + nil + end + + def handle_update(current_version, latest_version) + display_message(:yellow, "Current Rails version: #{current_version}") + display_message(:yellow, "Latest Rails version: #{latest_version}") + update_rails if prompt_user?('Would you like to update to the latest version?') + end + + def display_current_version(version) + display_message(:green, "You have the latest version of Rails (#{version}).") + end + + def prompt_user?(message) + answer = shell.ask("#{message} (yes/no, default: yes)").to_s.strip.downcase + answer.empty? || answer.start_with?('y') + end + + def display_message(color, message) + shell.say(message, color) + end + + def install_rails + execute_gem_command('install rails', 'install') + end + + def update_rails + execute_gem_command('update rails', 'update') + end + + def execute_gem_command(command, action) + return if system("gem #{command}") + + raise InstallationError, "Failed to #{action} the `rails` gem." + ensure + display_message(:green, "The `rails` gem has been successfully #{action}ed.") if $CHILD_STATUS.success? + end +end diff --git a/docs/thor-generator/templates/.gitignore b/docs/thor-generator/templates/.gitignore new file mode 100644 index 0000000..875e1bf --- /dev/null +++ b/docs/thor-generator/templates/.gitignore @@ -0,0 +1,22 @@ +/.bundle +/.env* +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep +/config/master.key +coverage/* +!coverage/.keep +.rspec-local +.DS_Store +.bash_history +.irb_history +.byebug_history \ No newline at end of file diff --git a/docs/thor-generator/templates/.rubocop.yml b/docs/thor-generator/templates/.rubocop.yml new file mode 100644 index 0000000..62ffe9a --- /dev/null +++ b/docs/thor-generator/templates/.rubocop.yml @@ -0,0 +1,24 @@ +--- + +require: +- rubocop-rails +- rubocop-performance +- rubocop-rspec +- rubocop-rspec_rails +- rubocop-factory_bot + +AllCops: + NewCops: enable + +Rails/UnknownEnv: + Environments: + - production + - rc + - test + - development + +Style/Documentation: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false diff --git a/docs/thor-generator/templates/Makefile.erb b/docs/thor-generator/templates/Makefile.erb new file mode 100644 index 0000000..689d3ff --- /dev/null +++ b/docs/thor-generator/templates/Makefile.erb @@ -0,0 +1,92 @@ +### Docker specific + +ifndef VERBOSE + MAKEFLAGS += --no-print-directory +endif + +APP_USER_UID=$$($(MAKE) user_id) +APP_GROUP_GID=$$($(MAKE) user_group) + +VERSION_BUILD_DATE := $$(date -u +%FT%TZ) +VERSION_BUILD_JOB_NUMBER := 0 +VERSION_BRANCH := $$(git rev-parse --abbrev-ref HEAD) +VERSION_TAG := $$(git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>>/dev/null || echo 'none') +VERSION_SHA := $$(git rev-parse HEAD) + +define BUILD_ARGS +--build-arg VERSION_BUILD_DATE="${VERSION_BUILD_DATE}" \ +--build-arg VERSION_BUILD_JOB_NUMBER="${VERSION_BUILD_JOB_NUMBER}" \ +--build-arg VERSION_BRANCH="${VERSION_BRANCH}" \ +--build-arg VERSION_TAG="${VERSION_TAG}" \ +--build-arg VERSION_SHA="${VERSION_SHA}" +endef + +user_id: + @echo $$(id -u) + +user_group: + @if [ $$(id -g) -lt 1000 ]; then echo 1000; else echo $$(id -g); fi + +create_history_files: + touch tmp/.bash_history + touch tmp/.irb_history + +set_envs: + export APP_USER_UID=$(APP_USER_UID) APP_GROUP_GID=$(APP_GROUP_GID) + +db_setup: + eval $$($(MAKE) set_envs) && docker compose run --rm --use-aliases -u $(APP_USER_UID) api bash -c "make db_test_reset" + +build_core: create_history_files + eval $$($(MAKE) set_envs) && COMPOSE_PROFILES=core docker compose build ${BUILD_ARGS} + +build: build_core + +build_exporter: create_history_files + eval $$($(MAKE) set_envs) && COMPOSE_PROFILES=exporter docker compose build ${BUILD_ARGS} + +build_all: create_history_files + eval $$($(MAKE) set_envs) && COMPOSE_PROFILES=core,exporter docker compose build ${BUILD_ARGS} + +up_core: + eval $$($(MAKE) set_envs) && COMPOSE_PROFILES=core docker compose up + +up_exporter: + eval $$($(MAKE) set_envs) && COMPOSE_PROFILES=exporter docker compose up + +up_all: + eval $$($(MAKE) set_envs) && COMPOSE_PROFILES=core,exporter docker compose up + +down_all: + eval $$($(MAKE) set_envs) && COMPOSE_PROFILES=core docker compose down + +stop_all: + eval $$($(MAKE) set_envs) && COMPOSE_PROFILES=core,exporter docker compose stop + +api_bash: + eval $$($(MAKE) set_envs) && docker compose run --rm --use-aliases -u $(APP_USER_UID) api bash + +api_exec_bash: + eval $$($(MAKE) set_envs) && docker compose exec -u $(APP_USER_UID) api bash + +### Not docker specific + +db_create: + bundle exec rails db:create + +db_migrate: + bundle exec rails db:migrate + +db_test_setup: + bundle exec rails db:test:prepare + +db_test_reset: + bundle exec rails db:create db:migrate && bundle exec rails db:test:prepare + +rubocop: + bundle exec rubocop --color + +<% unless skip_brakeman %> +brakeman: + bundle exec brakeman --color +<% end %> diff --git a/lib/tasks/book_to_mongo.rake b/lib/tasks/book_to_mongo.rake new file mode 100644 index 0000000..1a3ced4 --- /dev/null +++ b/lib/tasks/book_to_mongo.rake @@ -0,0 +1,8 @@ +namespace :book_to_mongo do + desc 'Копируем из постгреса в монко Books' + task run: :environment do + ::Book.all.find_each do |book| + ::Mongo::Book.create(BookSerializer.new(book).to_h) + end + end +end