├── .circleci └── config.yml ├── .gitignore ├── .rspec ├── Appraisals ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── fruit_juice.gemspec ├── gemfiles ├── .bundle │ └── config ├── redis_4.gemfile ├── redis_4.gemfile.lock ├── redis_5.gemfile └── redis_5.gemfile.lock ├── lib ├── fruit_juice.rb └── fruit_juice │ ├── delayed_job.rb │ ├── hash.rb │ ├── string.rb │ └── version.rb ├── sig └── fruit_juice.rbs └── spec ├── delayed_job_spec.rb ├── fruit_juice_spec.rb └── spec_helper.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | ruby-executor: 5 | parameters: 6 | version: 7 | type: string 8 | docker: 9 | - image: ruby:<< parameters.version >> 10 | environment: 11 | REDIS_URL: redis://127.0.0.1:6379 12 | - image: cimg/redis:6.2 13 | working_directory: ~/fruit_juice 14 | 15 | jobs: 16 | test: 17 | executor: 18 | name: ruby-executor 19 | version: << parameters.ruby-version >> 20 | parameters: 21 | ruby-version: 22 | type: string 23 | steps: 24 | - checkout 25 | # Delete the Gemfile.lock for the older out of date ruby versions 26 | - run: 27 | name: Conditionally remove Gemfile.lock 28 | command: | 29 | if [[ "<< parameters.ruby-version >>" < "2.6" ]]; then 30 | rm Gemfile.lock 31 | rm gemfiles/redis_4.gemfile.lock 32 | rm gemfiles/redis_5.gemfile.lock 33 | fi 34 | - run: 35 | name: Bundle install 36 | command: bundle install 37 | - run: 38 | name: Install appraisal dependencies 39 | command: bundle exec appraisal install 40 | - run: 41 | name: Run specs 42 | command: bundle exec appraisal rspec 43 | - store_test_results: 44 | path: test-results 45 | - store_artifacts: 46 | path: test-results 47 | 48 | workflows: 49 | version: 2 50 | fruit_juice_specs: 51 | jobs: 52 | - test: 53 | matrix: 54 | parameters: 55 | ruby-version: ["2.5.0", "2.7.4", "3.2.2", "3.3.0-preview1"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | .byebug_history -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "redis-4" do 2 | gem "redis", ">= 4.0.0", "< 5.0.0" 3 | end 4 | 5 | appraise "redis-5" do 6 | gem "redis", ">= 5.0.0" 7 | end 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at crimsonknightstudios@gmail.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in fruit_juice.gemspec 6 | gemspec 7 | 8 | group :test do 9 | gem "rspec", "~> 3.0" 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | fruit_juice (0.6.0) 5 | json (>= 2.3.0) 6 | redis (>= 4.2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | appraisal (2.5.0) 12 | bundler 13 | rake 14 | thor (>= 0.14.0) 15 | connection_pool (2.3.0) 16 | diff-lcs (1.5.0) 17 | json (2.6.3) 18 | rake (13.0.6) 19 | redis (5.0.5) 20 | redis-client (>= 0.9.0) 21 | redis-client (0.11.2) 22 | connection_pool 23 | rspec (3.12.0) 24 | rspec-core (~> 3.12.0) 25 | rspec-expectations (~> 3.12.0) 26 | rspec-mocks (~> 3.12.0) 27 | rspec-core (3.12.0) 28 | rspec-support (~> 3.12.0) 29 | rspec-expectations (3.12.0) 30 | diff-lcs (>= 1.2.0, < 2.0) 31 | rspec-support (~> 3.12.0) 32 | rspec-mocks (3.12.1) 33 | diff-lcs (>= 1.2.0, < 2.0) 34 | rspec-support (~> 3.12.0) 35 | rspec-support (3.12.0) 36 | thor (1.2.1) 37 | 38 | PLATFORMS 39 | arm64-darwin-22 40 | x86_64-darwin-21 41 | x86_64-linux 42 | 43 | DEPENDENCIES 44 | appraisal (~> 2.5) 45 | fruit_juice! 46 | rspec (~> 3.0) 47 | 48 | BUNDLED WITH 49 | 2.3.26 50 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Seth Tucker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Welcome to `FruitJuice`! 3 | 4 | FruitJuice is a job enqueuing adapter for the [mosquito background job processor](https://github.com/mosquito-cr/mosquito) written in [Crystal](https://crystal-lang.org/). If you're here to find out how to enqueue background jobs from Ruby/Rails and have them processed in Crystal, you've come to the right place! 5 | 6 | ## Requirements 7 | 8 | Ruby 2.5.0+ 9 | Mosquito 1.0.0.rc1+ 10 | Crystal 1.0.0+ 11 | Redis 4+ 12 | 13 | ## Installation 14 | 15 | Install the gem and add to the application's Gemfile by executing: 16 | 17 | `bundle add fruit_juice` 18 | 19 | ## Types of Jobs 20 | 21 | There are two types of jobs that can be enqueued from Ruby/Rails with this gem. The first is a `delayed job`, the second is a `periodic job`. A delayed job goes into a job queue just like Sidekiq or DelayedJob and gets executed when it's turn in the queue arrives. A periodic job can be executed just like a task scheduled from the `whenever` gem or other cron-job scheduling gems/tools. 22 | 23 | This gem currently only supports __`delayed job`s__ 24 | 25 | ### Using Delayed Jobs 26 | 27 | Delayed jobs inherit from `FruitJuice::DelayedJob` 28 | 29 | ```ruby 30 | # my_new_job.rb 31 | class MyNewJob < FruitJuice::DelayedJob 32 | end 33 | ``` 34 | 35 | Your ruby job names are meant to match the class/job names used in mosquito 36 | ```crystal 37 | class MyNewJob < Mosquito::QueuedJob 38 | # your job params here 39 | 40 | def perform 41 | # your job start code here 42 | end 43 | end 44 | ``` 45 | 46 | 47 | FruitJuice will automatically convert the class name into the job parameter required for Mosquito to find the job and run it. 48 | 49 | If you need to override this behavior, you can initialize your job with the `job_type` parameter like this: 50 | ```ruby 51 | # Using the class MyNewRubyJob < FruitJuice::DelayedJob 52 | # The `job_type` parameter must be a string representation of the Crystal job class you want to execute the job 53 | delayed_job = MyNewRubyJob.new(job_type: "MyNewJob") 54 | delayed_job.job_type # Output: my_new_job 55 | 56 | ``` 57 | 58 | #### NameSpaced Jobs 59 | ```ruby 60 | # Using the ruby `class MyNewRubyJob < FruitJuice::DelayedJob` 61 | # Using the crystal `class ExampleNamespace::MyNewJob < Mosquito::QueuedJob` 62 | # The `job_type` parameter must be a string representation of the Crystal job class you want to execute the job 63 | delayed_job = MyNewRubyJob.new(job_type: "ExampleNamespace::MyNewJob") 64 | delayed_job.job_type # Output: example_namespace::my_new_job which matched the ExampleNamespace::MyNewJob in Crystal 65 | ``` 66 | 67 | 68 | ## How to store job options 69 | 70 | All named parameters passed into `#perform` will be turned into JSON 71 | 72 | ```ruby 73 | # Using the class MyNewRubyJob < FruitJuice::DelayedJob 74 | # The `job_type` parameter must be a string representation of the Crystal job class you want to execute the job 75 | delayed_job = MyNewRubyJob.new(job_type: "MyNewJob") 76 | delayed_job.perform(this_will: "become a job option", "job option": "and be stored", "as": "json to parse in Mosquito") 77 | ``` 78 | 79 | That's it! Triggering jobs from Ruby/Rails is pretty easy, but this makes it a nice pattern that's easy to follow. 80 | 81 | 82 | ## Development 83 | 84 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 85 | 86 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). (Hint: only Seth can push to ruby gems for now) 87 | 88 | 89 | 90 | ## Contributing 91 | 92 | Bug reports and pull requests are welcome on GitHub at https://github.com/crimson-knight/fruit_juice. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the golden rule: "if you have nothing nice or constructive to say, don't say anything at all". 93 | 94 | Please open an issue and discuss and grand plans beyond bug fixes before taking on huge changes :) 95 | 96 | 97 | ## License 98 | 99 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 100 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "fruit_juice" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /fruit_juice.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/fruit_juice/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "fruit_juice" 7 | spec.version = FruitJuice::VERSION 8 | spec.authors = ["Seth Tucker"] 9 | spec.email = ["crimsonknightstudios@gmail.com"] 10 | 11 | spec.summary = "Ruby job adapter to enqueue background jobs in Mosquito for Crystal. Uniting Ruby/Rails & Crystal!" 12 | spec.description = "This handy adapter will let you enqueue delayed jobs from a Ruby/Rails app and have the job processed by Mosquito in Crystal. The idea behind this came from a Ruby/Rails app needing a better way to process massive background jobs more effeciently, and a desire to stay curious and explore." 13 | spec.homepage = "https://github.com/crimson-knight/fruit_juice" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 2.5.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = "https://github.com/crimson-knight/fruit_juice" 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | spec.files = Dir.chdir(__dir__) do 23 | `git ls-files -z`.split("\x0").reject do |f| 24 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) 25 | end 26 | end 27 | spec.bindir = "exe" 28 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 29 | spec.require_paths = ["lib"] 30 | 31 | spec.add_dependency "redis", ">= 4.2.0" 32 | spec.add_dependency "json", ">= 2.3.0" 33 | spec.add_development_dependency "appraisal", "~> 2.5" 34 | end 35 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | -------------------------------------------------------------------------------- /gemfiles/redis_4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "redis", ">= 4.0.0", "< 5.0.0" 7 | 8 | group :test do 9 | gem "rspec", "~> 3.0" 10 | end 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/redis_4.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | fruit_juice (0.6.0) 5 | redis (>= 4.0.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.5.0) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | byebug (11.1.3) 15 | diff-lcs (1.5.0) 16 | rake (13.0.6) 17 | redis (4.6.0) 18 | rspec (3.12.0) 19 | rspec-core (~> 3.12.0) 20 | rspec-expectations (~> 3.12.0) 21 | rspec-mocks (~> 3.12.0) 22 | rspec-core (3.12.0) 23 | rspec-support (~> 3.12.0) 24 | rspec-expectations (3.12.0) 25 | diff-lcs (>= 1.2.0, < 2.0) 26 | rspec-support (~> 3.12.0) 27 | rspec-mocks (3.12.1) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.12.0) 30 | rspec-support (3.12.0) 31 | thor (1.2.1) 32 | 33 | PLATFORMS 34 | arm64-darwin-22 35 | x86_64-darwin-21 36 | 37 | DEPENDENCIES 38 | appraisal (~> 2.5) 39 | byebug 40 | fruit_juice! 41 | rake (~> 13.0) 42 | redis (>= 4.0.0, < 5.0.0) 43 | rspec (~> 3.0) 44 | 45 | BUNDLED WITH 46 | 2.3.26 47 | -------------------------------------------------------------------------------- /gemfiles/redis_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rake", "~> 13.0" 6 | gem "redis", ">= 5.0.0" 7 | 8 | group :test do 9 | gem "rspec", "~> 3.0" 10 | end 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/redis_5.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | fruit_juice (0.6.0) 5 | redis (>= 4.0.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.5.0) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | byebug (11.1.3) 15 | connection_pool (2.3.0) 16 | diff-lcs (1.5.0) 17 | rake (13.0.6) 18 | redis (5.0.5) 19 | redis-client (>= 0.9.0) 20 | redis-client (0.11.2) 21 | connection_pool 22 | rspec (3.12.0) 23 | rspec-core (~> 3.12.0) 24 | rspec-expectations (~> 3.12.0) 25 | rspec-mocks (~> 3.12.0) 26 | rspec-core (3.12.0) 27 | rspec-support (~> 3.12.0) 28 | rspec-expectations (3.12.0) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.12.0) 31 | rspec-mocks (3.12.1) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.12.0) 34 | rspec-support (3.12.0) 35 | thor (1.2.1) 36 | 37 | PLATFORMS 38 | arm64-darwin-22 39 | x86_64-darwin-21 40 | 41 | DEPENDENCIES 42 | appraisal (~> 2.5) 43 | byebug 44 | fruit_juice! 45 | rake (~> 13.0) 46 | redis (>= 5.0.0) 47 | rspec (~> 3.0) 48 | 49 | BUNDLED WITH 50 | 2.3.26 51 | -------------------------------------------------------------------------------- /lib/fruit_juice.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "redis" 4 | 5 | require_relative "fruit_juice/version" 6 | require_relative "fruit_juice/delayed_job" 7 | 8 | module FruitJuice 9 | class Error < StandardError; end 10 | end 11 | -------------------------------------------------------------------------------- /lib/fruit_juice/delayed_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "string" 3 | require_relative "hash" 4 | require "json" 5 | 6 | module FruitJuice 7 | class DelayedJob 8 | using FruitJuiceString 9 | using FruitJuiceHash 10 | 11 | attr_reader :job_type, :waiting_queue_key 12 | 13 | def initialize(**config_options) 14 | raise "job_type must be a string" if config_options.has_key?(:job_type) && config_options[:job_type].class != String 15 | 16 | @redis_adapter = set_redis_adapter(config_options[:redis_adapter]) 17 | @job_type = set_job_type(config_options[:job_type]) 18 | @waiting_queue_key = "mosquito:waiting:#{@job_type.underscore}" 19 | end 20 | 21 | def perform(**job_options) 22 | @job_options = job_options.deep_stringify_keys! 23 | enqueue_job 24 | end 25 | 26 | protected 27 | 28 | def enqueue_job 29 | enqueue_time = (Time.now.utc.to_f*1000).to_i 30 | job_id = "#{enqueue_time}:#{rand(1000)}" 31 | job_run_key = "mosquito:job_run:#{job_id}" 32 | 33 | job_run_meta_data = { 34 | # Mosquito required meta data 35 | "type": @job_type, 36 | "enqueue_time": enqueue_time.to_s, 37 | "retry_count": 0, 38 | 39 | # Job specific params 40 | "job_options": @job_options.to_json 41 | } 42 | 43 | # Required to support both Redis v4.x & v5+ due to behavior changes 44 | job_run_meta_data.transform_values!(&:to_s) if Redis::VERSION.to_i > 4 45 | 46 | @redis_adapter.hset(job_run_key, job_run_meta_data) 47 | @redis_adapter.lpush(@waiting_queue_key, job_id) 48 | job_run_key 49 | end 50 | 51 | def set_redis_adapter(redis_adapter) 52 | case redis_adapter.class.to_s.downcase 53 | when "redis" 54 | redis_adapter 55 | when "hash" 56 | Redis.new(redis_adapter) 57 | when "nilclass" 58 | if ENV["REDIS_URL"] 59 | Redis.new(url: ENV["REDIS_URL"]) 60 | else 61 | raise Error.new("Expecting a hash of options for the Redis class, a Redis instance or the REDIS_URL environment variable to be set") 62 | end 63 | end 64 | end 65 | 66 | def set_job_type(job_type) 67 | case job_type.class.to_s 68 | when "NilClass" 69 | self.class.to_s.underscore 70 | when "String" 71 | job_type.underscore 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/fruit_juice/hash.rb: -------------------------------------------------------------------------------- 1 | module FruitJuiceHash 2 | refine Hash do 3 | def deep_stringify_keys! 4 | deep_transform_keys(&:to_s) 5 | end 6 | 7 | def deep_transform_keys(&block) 8 | _deep_transform_keys_in_object!(self, &block) 9 | end 10 | 11 | def _deep_transform_keys_in_object!(object, &block) 12 | case object 13 | when Hash 14 | object.keys.each do |key| 15 | value = object.delete(key) 16 | object[yield(key)] = _deep_transform_keys_in_object!(value, &block) 17 | end 18 | object 19 | else 20 | object 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/fruit_juice/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FruitJuiceString 4 | refine String do 5 | 6 | def camelize(uppercase_first_letter = true) 7 | string = self 8 | if uppercase_first_letter 9 | string = string.sub(/^[a-z\d]*/) { |match| match.capitalize } 10 | else 11 | string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase } 12 | end 13 | string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub("/", "::") 14 | end 15 | 16 | def underscore 17 | camel_cased_word = self 18 | return camel_cased_word.to_s unless /[A-Z-]|::/.match?(camel_cased_word) 19 | word = camel_cased_word.to_s 20 | word.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" } 21 | word.tr!("-", "_") 22 | word.downcase! 23 | word 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/fruit_juice/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FruitJuice 4 | VERSION = "1.0.0" 5 | end 6 | -------------------------------------------------------------------------------- /sig/fruit_juice.rbs: -------------------------------------------------------------------------------- 1 | module FruitJuice 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /spec/delayed_job_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe FruitJuice::DelayedJob do 4 | 5 | after(:all) do 6 | Redis.new(host: "localhost", port: "6379", db: 1).flushdb 7 | end 8 | 9 | context "initialization options" do 10 | it "creates a DelayedJob using the REDIS_URL env var" do 11 | expect(FruitJuice::DelayedJob.new).to be_instance_of(FruitJuice::DelayedJob) 12 | end 13 | 14 | it "creates a DelayedJob when passing in hash options" do 15 | expect(FruitJuice::DelayedJob.new(redis_adapter: {host: "localhost", port: 6379, db: 0})).to be_instance_of(FruitJuice::DelayedJob) 16 | end 17 | 18 | it "creates a DelayedJob when passing in a Redis instance" do 19 | redis = Redis.new(host: "localhost") 20 | 21 | expect(FruitJuice::DelayedJob.new(redis_adapter: redis)).to be_instance_of(FruitJuice::DelayedJob) 22 | end 23 | 24 | it "raises an error if no redis adapter is provided and the REDIS_URL ENV var is not configured" do 25 | redis_url = ENV.delete("REDIS_URL") 26 | expect { FruitJuice::DelayedJob.new }.to raise_error "Expecting a hash of options for the Redis class, a Redis instance or the REDIS_URL environment variable to be set" 27 | ENV["REDIS_URL"] = redis_url 28 | end 29 | end 30 | 31 | context "enqueueing jobs" do 32 | it "uses a custom job_type" do 33 | delayed_job = FruitJuice::DelayedJob.new(job_type: "custom_job_type") 34 | expect(delayed_job.job_type).to eq("custom_job_type") 35 | end 36 | 37 | it "generates a job_type from the class name" do 38 | delayed_job = FruitJuice::DelayedJob.new() 39 | expect(delayed_job.job_type).to eq("fruit_juice::delayed_job") 40 | end 41 | 42 | it "queues up a delayed job when using #perform" do 43 | delayed_job = FruitJuice::DelayedJob.new() 44 | expect(delayed_job.perform.class).to eq(String) 45 | end 46 | 47 | it "queues up a delayed job when using passing params to #perform" do 48 | delayed_job = FruitJuice::DelayedJob.new() 49 | expect(delayed_job.perform(test_param: "test value")).to include "mosquito:job_run:" 50 | end 51 | 52 | it "verifies jobs are being created into the correct queue" do 53 | delayed_job = FruitJuice::DelayedJob.new() 54 | delayed_job.perform(test_param: "test_value", "the first": "params") 55 | expect(delayed_job.waiting_queue_key).to eq("mosquito:waiting:fruit_juice::delayed_job") 56 | end 57 | 58 | it "verifies the key being saved into redis is actually a json string" do 59 | delayed_job = FruitJuice::DelayedJob.new() 60 | job_run_key = delayed_job.perform(test_param: "test_value", "the first": "params") 61 | redis_adapter = Redis.new(url: ENV["REDIS_URL"]) 62 | expect(redis_adapter.hget(job_run_key, "job_options")).to eq("{\"test_param\":\"test_value\",\"the first\":\"params\"}") 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/fruit_juice_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe FruitJuice do 4 | it "has a version number" do 5 | expect(FruitJuice::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fruit_juice" 4 | 5 | begin 6 | require 'redis' 7 | rescue LoadError 8 | puts "redis is not available for this ruby version. Skipping tests for version #{RUBY_VERSION}" 9 | exit(0) 10 | end 11 | 12 | RSpec.configure do |config| 13 | # Enable flags like --only-failures and --next-failure 14 | config.example_status_persistence_file_path = ".rspec_status" 15 | 16 | # Disable RSpec exposing methods globally on `Module` and `main` 17 | config.disable_monkey_patching! 18 | 19 | config.expect_with :rspec do |c| 20 | c.syntax = :expect 21 | end 22 | end 23 | --------------------------------------------------------------------------------