diff --git a/Gemfile b/Gemfile index 38f004f..a2e9446 100644 --- a/Gemfile +++ b/Gemfile @@ -14,7 +14,7 @@ gem 'puma', '~> 5.6' # Use SCSS for stylesheets gem 'sass-rails', '~> 6.0' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker -gem 'webpacker', '~> 4.0' +# gem 'webpacker', '~> 4.0' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder @@ -30,6 +30,7 @@ gem 'sidekiq', '~> 6.5' gem 'sidekiq-scheduler' gem 'pry' gem "sidekiq-cron", "~> 1.1" +gem 'kaminari', '~> 1.2' # Use Active Storage variant # gem 'image_processing', '~> 1.2' diff --git a/app/controllers/pastes_controller.rb b/app/controllers/pastes_controller.rb index 1821d82..7bdbb59 100644 --- a/app/controllers/pastes_controller.rb +++ b/app/controllers/pastes_controller.rb @@ -4,12 +4,57 @@ class PastesController < ApplicationController # GET /pastes # GET /pastes.json def index - @pastes = Paste.all + # Get combined pastes from both SQLite and Elasticsearch + if params[:search].present? + # For search, use Elasticsearch only for now + @pastes = Paste.from_elasticsearch(limit: 20).select do |paste| + paste.content.downcase.include?(params[:search].downcase) || + paste.title.downcase.include?(params[:search].downcase) + end + else + # Show combined recent pastes + @pastes = Paste.recent_combined(limit: 20) + end + + # Filter by language if specified + if params[:language].present? + @pastes = @pastes.select { |paste| paste.language == params[:language] } + end + + # For filter dropdown - combine languages from both sources + sqlite_languages = Paste.languages + es_languages = @pastes.map(&:language).uniq.compact + @languages = (sqlite_languages + es_languages).uniq.sort + + # Stats for dashboard + @total_pastes = Paste.count + get_elasticsearch_count + @total_languages = @languages.count + end + + private + + def get_elasticsearch_count + begin + client = Elasticsearch::Client.new(url: ENV.fetch('ELASTICSEARCH_URL', 'http://pastebinner-elasticsearch:9200')) + response = client.count(index: 'pastes') + response['count'] || 0 + rescue => e + Rails.logger.error "Elasticsearch count error: #{e.message}" + 0 + end end # GET /pastes/1 # GET /pastes/1.json def show + # Increment hit counter only for SQLite pastes + @paste.increment_hits! if @paste.respond_to?(:increment_hits!) + + # For raw text display + if params[:raw] == 'true' + render plain: @paste.content, content_type: 'text/plain' + return + end end # GET /pastes/new @@ -63,12 +108,32 @@ class PastesController < ApplicationController private # Use callbacks to share common setup or constraints between actions. -# def set_paste -# @paste = Paste.find(params[:id]) -# end + def set_paste + # Try to find in SQLite first + begin + @paste = Paste.find(params[:id]) + rescue ActiveRecord::RecordNotFound + # If not found in SQLite, try Elasticsearch by document ID + @paste = find_elasticsearch_paste(params[:id]) + unless @paste + redirect_to pastes_path, alert: 'Paste not found' + end + end + end + + def find_elasticsearch_paste(doc_id) + begin + client = Elasticsearch::Client.new(url: ENV.fetch('ELASTICSEARCH_URL', 'http://pastebinner-elasticsearch:9200')) + response = client.get(index: 'pastes', id: doc_id) + Paste.elasticsearch_paste_to_object(response) + rescue => e + Rails.logger.error "Elasticsearch get error: #{e.message}" + nil + end + end # Never trust parameters from the scary internet, only allow the white list through. -# def paste_params -# params.require(:paste).permit(:search_pastes) -# end + def paste_params + params.require(:paste).permit(:title, :content, :language, :expires_at) + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be79..8e8b175 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,62 @@ module ApplicationHelper + def syntax_highlight(content, language = 'text', options = {}) + return '' if content.blank? + + css_classes = ["language-#{language}"] + css_classes << 'line-numbers' if options[:line_numbers] + + content_tag :pre, class: css_classes.join(' ') do + content_tag :code, content, class: "language-#{language}" + end + end + + def language_icon(language) + icons = { + 'javascript' => '🟨', + 'python' => '🐍', + 'ruby' => '💎', + 'java' => '☕', + 'php' => '🐘', + 'html' => '🌐', + 'css' => '🎨', + 'sql' => '🗄️', + 'json' => '📋', + 'xml' => '📄', + 'bash' => '🐚', + 'go' => '🐹', + 'rust' => '🦀', + 'typescript' => '🔷' + } + + icons[language.to_s.downcase] || '📄' + end + + def time_ago_in_words_short(time) + return '' unless time + + distance = Time.current - time + + case distance + when 0..59 + "#{distance.to_i}s ago" + when 60..3599 + "#{(distance / 60).to_i}m ago" + when 3600..86399 + "#{(distance / 3600).to_i}h ago" + when 86400..2591999 + "#{(distance / 86400).to_i}d ago" + else + time.strftime('%b %d, %Y') + end + end + + def truncate_content(content, length = 150) + return '' if content.blank? + + if content.length > length + content[0..length].gsub(/\s\w+\s*$/, '...') + else + content + end + end end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9cd55d4..835eb3a 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -8,6 +8,35 @@ require("turbolinks").start() require("@rails/activestorage").start() require("channels") +// Initialize Prism.js on page load and turbolinks navigation +document.addEventListener('turbolinks:load', function() { + if (typeof Prism !== 'undefined') { + Prism.highlightAll(); + } +}); + +// Handle copy to clipboard functionality +document.addEventListener('turbolinks:load', function() { + // Add click handler for copy buttons + document.querySelectorAll('.copy-btn').forEach(function(btn) { + btn.addEventListener('click', function() { + const target = this.getAttribute('data-target'); + const textArea = document.querySelector(target); + if (textArea) { + textArea.select(); + document.execCommand('copy'); + + // Provide visual feedback + const originalText = this.textContent; + this.textContent = 'Copied!'; + setTimeout(() => { + this.textContent = originalText; + }, 1000); + } + }); + }); +}); + // Uncomment to copy all static images under ../images to the output folder and reference // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) diff --git a/app/models/paste.rb b/app/models/paste.rb index 7c08872..8890d02 100644 --- a/app/models/paste.rb +++ b/app/models/paste.rb @@ -1,31 +1,232 @@ require 'elasticsearch/model' +require 'ostruct' class Paste < ApplicationRecord - include Elasticsearch::Model - include Elasticsearch::Model::Callbacks + include Elasticsearch::Model + # include Elasticsearch::Model::Callbacks # Temporarily disabled for seeding + # Pagination + paginates_per 20 -# this doesnt work yet, but this is how we build a way to progrmatically query ES -# def self.search(query) -# __elasticsearch__.search( -# { -# query: { -# multi_match: { -# query: query, -# } -# }, -# "highlight": { -# "pre_tags": [ -# "@kibana-highlighted-field@" -# ], -# "post_tags": [ -# "@/kibana-highlighted-field@" -# ], -# "fields": { -# "*": {} -# } -# } -# } -# ) -# end + # Validations + validates :content, presence: true + validates :paste_key, uniqueness: true, allow_blank: true + + # Callbacks + before_save :set_defaults + before_save :detect_language + before_save :calculate_size + + # Scopes + scope :by_language, ->(lang) { where(language: lang) if lang.present? } + scope :recent, -> { order(created_at: :desc) } + scope :popular, -> { order(hits: :desc) } + + # Class methods + def self.search_content(query) + return all if query.blank? + + __elasticsearch__.search({ + query: { + multi_match: { + query: query, + fields: ['content', 'title'] + } + }, + highlight: { + pre_tags: [''], + post_tags: [''], + fields: { + content: {} + } + } + }) + end + + def self.languages + distinct.pluck(:language).compact.sort + end + + # Fetch pastes from Elasticsearch + def self.from_elasticsearch(limit: 20, offset: 0) + begin + client = Elasticsearch::Client.new(url: ENV.fetch('ELASTICSEARCH_URL', 'http://pastebinner-elasticsearch:9200')) + + response = client.search( + index: 'pastes', + body: { + size: limit, + from: offset, + sort: [{ '_score': { order: 'desc' } }], + query: { match_all: {} } + } + ) + + response['hits']['hits'].map do |hit| + elasticsearch_paste_to_object(hit) + end + rescue => e + Rails.logger.error "Elasticsearch error: #{e.message}" + [] + end + end + + # Convert Elasticsearch hit to paste-like object + def self.elasticsearch_paste_to_object(hit) + source = hit['_source'] + paste_id = extract_paste_key_from_metadata(source['paste_metadata']) + + OpenStruct.new( + id: hit['_id'], + title: "Scraped Paste #{paste_id}", + content: source['paste_text'] || '', + language: detect_language_from_content(source['paste_text']), + created_at: Time.current, + updated_at: Time.current, + hits: 0, + size: (source['paste_text'] || '').length, + paste_key: paste_id, + source: 'elasticsearch' + ) + end + + # Extract paste key from metadata array + def self.extract_paste_key_from_metadata(metadata) + return 'unknown' unless metadata.is_a?(Array) && metadata.length >= 2 + + url = metadata[1] # The scrape URL + if url =~ /\/api_scrape_item\.php\?i=([A-Za-z0-9]+)/ + $1 + else + 'unknown' + end + end + + # Simple language detection based on content + def self.detect_language_from_content(content) + return 'text' if content.blank? + + case content + when /^\s*<\?php/m then 'php' + when /^\s*#!/m then 'bash' + when /function\s*\(/m, /=>\s*{/m, /console\.log/m then 'javascript' + when /def\s+\w+/m, /import\s+\w+/m then 'python' + when / limit ? "#{content[0..limit]}..." : content + end + + def formatted_size + return '0 B' if size.nil? || size.zero? + + units = %w[B KB MB GB] + base = 1024 + exp = (Math.log(size) / Math.log(base)).to_i + exp = [exp, units.size - 1].min + + "%.1f %s" % [size.to_f / base**exp, units[exp]] + end + + def language_display + language.present? ? language.humanize : 'Plain Text' + end + + def increment_hits! + increment!(:hits) + end + + def expired? + expires_at && expires_at < Time.current + end + + def prism_language + # Map common languages to Prism.js language identifiers + language_mappings = { + 'javascript' => 'javascript', + 'js' => 'javascript', + 'python' => 'python', + 'py' => 'python', + 'ruby' => 'ruby', + 'rb' => 'ruby', + 'java' => 'java', + 'cpp' => 'cpp', + 'c++' => 'cpp', + 'c' => 'c', + 'html' => 'html', + 'css' => 'css', + 'php' => 'php', + 'sql' => 'sql', + 'json' => 'json', + 'xml' => 'xml', + 'yaml' => 'yaml', + 'yml' => 'yaml', + 'bash' => 'bash', + 'shell' => 'bash', + 'go' => 'go', + 'rust' => 'rust', + 'typescript' => 'typescript', + 'ts' => 'typescript' + } + + return language_mappings[language.to_s.downcase] if language.present? + 'text' + end + + private + + def set_defaults + self.paste_key = SecureRandom.hex(8) if paste_key.blank? + self.hits ||= 0 + self.title = "Untitled" if title.blank? + end + + def detect_language + return if content.blank? + + # Simple language detection based on content patterns + case content + when /def\s+\w+\s*\(/ + self.language = 'python' if language.blank? + when /function\s+\w+\s*\(/ + self.language = 'javascript' if language.blank? + when /class\s+\w+/ + self.language = 'java' if language.blank? + when /<\?php/ + self.language = 'php' if language.blank? + when / <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> - <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> + + + + + + + + + + + + + + + + + - <%= render 'search/form' %> + <% unless controller_name == 'pastes' %> + <%= render 'search/form' %> + <% end %> <%= yield %> diff --git a/app/views/pastes/index.html.erb b/app/views/pastes/index.html.erb index cbdce98..4ab67f0 100644 --- a/app/views/pastes/index.html.erb +++ b/app/views/pastes/index.html.erb @@ -1,27 +1,174 @@ -

<%= notice %>

+

Pastebinner - Browse Pastes

-

Pastes

- - - - - - - - - - +
+

Recent Pastes (<%= @total_pastes %> total - SQLite + Elasticsearch)

+ + + <%= form_with url: pastes_path, method: :get, local: true, class: "search-form" do |f| %> +
+ <%= f.text_field :search, placeholder: "Search pastes...", value: params[:search], class: "search-input" %> + <%= f.select :language, options_for_select([["All Languages", ""]] + @languages.map { |l| [l.titleize, l] }, params[:language]), {}, { class: "language-select" } %> + <%= f.submit "Search", class: "search-btn" %> + <%= link_to "Clear", pastes_path, class: "clear-btn" %> +
+ <% end %> + + <% if @pastes.any? %> <% @pastes.each do |paste| %> -
- - - - - +
+
+

<%= link_to paste.display_title, paste_path(paste.id) %>

+ <% if paste.respond_to?(:source) && paste.source == 'elasticsearch' %> + Live Scraped + <% else %> + Local + <% end %> +
+

+ Language: <%= paste.language %> | + Size: <%= paste.size || (paste.content&.length || 0) %> characters + <% if paste.paste_key.present? %> + | Key: <%= paste.paste_key %> + <% end %> +

+
+
<%= truncate(paste.content, length: 300) %>
+
+
<% end %> - -
Search pastes
<%= paste.search_pastes %><%= link_to 'Show', paste %><%= link_to 'Edit', edit_paste_path(paste) %><%= link_to 'Destroy', paste, method: :delete, data: { confirm: 'Are you sure?' } %>
+ <% else %> +

No pastes found. <%= link_to "Clear filters", pastes_path %>

+ <% end %> + -
+ \ No newline at end of file diff --git a/app/views/pastes/show.html.erb b/app/views/pastes/show.html.erb index feb99c5..3a20364 100644 --- a/app/views/pastes/show.html.erb +++ b/app/views/pastes/show.html.erb @@ -1,9 +1,356 @@ -

<%= notice %>

+
+ +
+
+
+

+ <%= language_icon(@paste.language) %> + <%= @paste.display_title %> +

+ + +
+ +
+ <%= link_to "📄 Raw Text", paste_path(@paste, raw: true), + class: "action-btn secondary", target: "_blank" %> + + <%= link_to "🔙 Back to List", pastes_path, class: "action-btn secondary" %> +
+
+
-

- Search pastes: - <%= @paste.search_pastes %> -

+ +
+ <% if @paste.content.present? %> +
+ + <%= language_icon(@paste.language) %> <%= @paste.language_display %> + + + 📏 <%= @paste.content.lines.count %> lines + +
+ +
+ <%= syntax_highlight(@paste.content, @paste.prism_language, line_numbers: true) %> +
+ <% else %> +
+
📭
+

No Content Available

+

This paste appears to be empty or the content could not be loaded.

+
+ <% end %> +
-<%= link_to 'Edit', edit_paste_path(@paste) %> | -<%= link_to 'Back', pastes_path %> + + +
+ + + + diff --git a/config/database.yml b/config/database.yml index 4a8a1b2..c4bd6af 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,9 +1,3 @@ -# SQLite. Versions 3.8.0 and up are supported. -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' -# default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> @@ -13,13 +7,10 @@ development: <<: *default database: db/development.sqlite3 -# 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: db/test.sqlite3 production: <<: *default - database: db/production.sqlite3 + database: db/production.sqlite3 \ No newline at end of file diff --git a/config/webpacker.yml b/config/webpacker.yml index 46ed57d..7c83cab 100644 --- a/config/webpacker.yml +++ b/config/webpacker.yml @@ -1,5 +1,3 @@ -# Note: You must restart bin/webpack-dev-server for changes to take effect - default: &default source_path: app/javascript source_entry_path: packs @@ -8,15 +6,8 @@ default: &default cache_path: tmp/cache/webpacker check_yarn_integrity: false webpack_compile_output: false - - # Additional paths webpack should lookup modules - # ['app/assets', 'engine/foo/app/assets'] resolved_paths: [] - - # Reload manifest.json on all requests so we reload latest compiled packs cache_manifest: false - - # Extract and emit a css file extract_css: false static_assets_extensions: @@ -51,18 +42,14 @@ default: &default development: <<: *default compile: true + check_yarn_integrity: false - # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules - check_yarn_integrity: true - - # Reference: https://webpack.js.org/configuration/dev-server/ dev_server: https: false host: localhost port: 3035 public: localhost:3035 hmr: false - # Inline should be set to true if using HMR inline: true overlay: true compress: true @@ -74,22 +61,13 @@ development: watch_options: ignored: '**/node_modules/**' - test: <<: *default compile: true - - # Compile test packs to a separate directory public_output_path: packs-test production: <<: *default - - # Production depends on precompilation of packs prior to booting for performance. compile: false - - # Extract and emit a css file extract_css: true - - # Cache manifest.json for performance - cache_manifest: true + cache_manifest: true \ No newline at end of file diff --git a/db/migrate/20250126120000_add_fields_to_pastes.rb b/db/migrate/20250126120000_add_fields_to_pastes.rb new file mode 100644 index 0000000..85054e0 --- /dev/null +++ b/db/migrate/20250126120000_add_fields_to_pastes.rb @@ -0,0 +1,16 @@ +class AddFieldsToPastes < ActiveRecord::Migration[6.1] + def change + add_column :pastes, :title, :string + add_column :pastes, :language, :string + add_column :pastes, :paste_key, :string + add_column :pastes, :content, :text + add_column :pastes, :size, :integer + add_column :pastes, :hits, :integer, default: 0 + add_column :pastes, :created_by, :string + add_column :pastes, :expires_at, :datetime + + add_index :pastes, :paste_key, unique: true + add_index :pastes, :language + add_index :pastes, :created_at + end +end \ No newline at end of file diff --git a/db/seeds.rb b/db/seeds.rb index 1beea2a..0872ee9 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,267 @@ -# 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 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) +# Sample paste data for testing the web interface + +puts "Creating sample pastes..." + +# Python sample +Paste.create!( + title: "Hello World Python", + content: <<~PYTHON, + def hello_world(): + """A simple greeting function""" + name = input("What's your name? ") + print(f"Hello, {name}!") + return name + + if __name__ == "__main__": + hello_world() + PYTHON + language: "python" +) + +# JavaScript sample +Paste.create!( + title: "React Component", + content: <<~JAVASCRIPT, + import React, { useState, useEffect } from 'react'; + + const UserProfile = ({ userId }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch(`/api/users/${userId}`) + .then(response => response.json()) + .then(userData => { + setUser(userData); + setLoading(false); + }) + .catch(error => { + console.error('Error fetching user:', error); + setLoading(false); + }); + }, [userId]); + + if (loading) return
Loading...
; + if (!user) return
User not found
; + + return ( +
+

{user.name}

+

{user.email}

+

Joined: {new Date(user.createdAt).toLocaleDateString()}

+
+ ); + }; + + export default UserProfile; + JAVASCRIPT + language: "javascript" +) + +# Ruby sample +Paste.create!( + title: "Rails Migration", + content: <<~RUBY, + class CreateUsersTable < ActiveRecord::Migration[6.1] + def change + create_table :users do |t| + t.string :name, null: false + t.string :email, null: false, index: { unique: true } + t.string :password_digest + t.text :bio + t.boolean :active, default: true + t.timestamps + end + + add_index :users, :name + add_index :users, :created_at + end + end + RUBY + language: "ruby" +) + +# SQL sample +Paste.create!( + title: "Database Query", + content: <<~SQL, + SELECT + u.name, + u.email, + COUNT(p.id) as post_count, + AVG(p.views) as avg_views + FROM users u + LEFT JOIN posts p ON u.id = p.user_id + WHERE u.active = true + AND u.created_at >= '2024-01-01' + GROUP BY u.id, u.name, u.email + HAVING COUNT(p.id) > 5 + ORDER BY avg_views DESC + LIMIT 20; + SQL + language: "sql" +) + +# HTML sample +Paste.create!( + title: "Landing Page Template", + content: <<~HTML, + + + + + + My Awesome App + + + +
+ + +
+

Build Amazing Things

+

The fastest way to create and deploy your applications

+ +
+
+ +
+

Why Choose Us?

+
+
+

Fast

+

Lightning-fast performance

+
+
+

Secure

+

Enterprise-grade security

+
+
+

Scalable

+

Grows with your business

+
+
+
+ + + HTML + language: "html" +) + +# CSS sample +Paste.create!( + title: "Modern CSS Grid Layout", + content: <<~CSS, + .container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + padding: 2rem; + max-width: 1200px; + margin: 0 auto; + } + + .card { + background: white; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: transform 0.3s ease, box-shadow 0.3s ease; + } + + .card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + } + + .card-header { + padding: 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + } + + .card-title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + } + + .card-body { + padding: 1.5rem; + } + + @media (max-width: 768px) { + .container { + grid-template-columns: 1fr; + padding: 1rem; + } + } + CSS + language: "css" +) + +# JSON sample +Paste.create!( + title: "API Response Example", + content: <<~JSON, + { + "status": "success", + "data": { + "users": [ + { + "id": 1, + "username": "john_doe", + "email": "john@example.com", + "profile": { + "firstName": "John", + "lastName": "Doe", + "avatar": "https://example.com/avatars/john.jpg", + "bio": "Software developer passionate about clean code" + }, + "preferences": { + "theme": "dark", + "notifications": { + "email": true, + "push": false, + "sms": false + }, + "privacy": { + "profilePublic": true, + "showEmail": false + } + }, + "stats": { + "postsCount": 42, + "followersCount": 156, + "followingCount": 89 + }, + "createdAt": "2024-01-15T10:30:00Z", + "lastLoginAt": "2024-01-26T14:45:23Z" + } + ], + "pagination": { + "currentPage": 1, + "totalPages": 10, + "totalItems": 100, + "itemsPerPage": 10 + } + }, + "meta": { + "timestamp": "2024-01-26T15:00:00Z", + "version": "1.0", + "requestId": "req_123456789" + } + } + JSON + language: "json" +) + +puts "Created #{Paste.count} sample pastes!" +puts "You can now browse them at http://localhost:3000" diff --git a/docker-compose.yml b/docker-compose.yml index 0995838..3345df3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,8 @@ services: - redis env_file: - .env + environment: + - DISABLE_SPRING=1 networks: - elastic elasticsearch: diff --git a/lib/redis_helper.rb b/lib/redis_helper.rb index 0390d00..c56c9ff 100644 --- a/lib/redis_helper.rb +++ b/lib/redis_helper.rb @@ -1,7 +1,7 @@ class RedisHelper attr_accessor :connection - def initialize(host: host, db: db) + def initialize(host: nil, db: nil) @host = host @db = db @connection = Redis.new(host: host, db: db)