alarmable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 81a2cd3c46bf9ce21ece16a46bdf2ccf1f78e38e
4
+ data.tar.gz: 9ebb03c91daec2ae494f8305268bd0ebeb3e7c60
5
+ SHA512:
6
+ metadata.gz: 6307267d20f76d4b0b19cd119d5e986b55f830ad9bb6b50d5371a06e9b1594d704f60f77417f5352856b0903bebe657a8584af72f31287ab2c61c001531b7ba5
7
+ data.tar.gz: 9d58b4e684b1dc04a5b976cbb2dda175f07509c1ca1eb0725e85e65e067cfca3f609768be726933983253ffa6b129f962cc5f82eacd24ecb09c8b8cdda361f25
@@ -0,0 +1,30 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = space
6
+ indent_size = 2
7
+ end_of_line = lf
8
+ charset = utf-8
9
+ trim_trailing_whitespace = true
10
+ insert_final_newline = true
11
+
12
+ [*.md]
13
+ trim_trailing_whitespace = true
14
+
15
+ [*.json]
16
+ indent_style = space
17
+ indent_size = 2
18
+
19
+ [*.yml]
20
+ indent_style = space
21
+ indent_size = 2
22
+
23
+ [Makefile]
24
+ trim_trailing_whitespace = true
25
+ indent_style = tab
26
+ indent_size = 4
27
+
28
+ [*.sh]
29
+ indent_style = space
30
+ indent_size = 2
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/
11
+ /gemfiles/vendor/
12
+ *.gemfile.lock
13
+
14
+ # rspec failure tracking
15
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,27 @@
1
+ require: rubocop-rspec
2
+
3
+ Rails:
4
+ Enabled: true
5
+
6
+ Documentation:
7
+ Enabled: true
8
+
9
+ AllCops:
10
+ DisplayCopNames: true
11
+ TargetRubyVersion: 2.3
12
+
13
+ Metrics/BlockLength:
14
+ Exclude:
15
+ - Rakefile
16
+ - spec/**/*.rb
17
+ - '**/*.rake'
18
+
19
+ # Document all the things.
20
+ Style/DocumentationMethod:
21
+ Enabled: true
22
+ RequireForNonPublicMethods: true
23
+
24
+ # Because +expect_any_instance_of().to have_received()+ is not
25
+ # supported with the +with(hash_including)+ matchers
26
+ RSpec/MessageSpies:
27
+ EnforcedStyle: receive
@@ -0,0 +1,25 @@
1
+ sudo: false
2
+
3
+ addons:
4
+ postgresql: "9.6"
5
+
6
+ services:
7
+ - postgresql
8
+
9
+ language: ruby
10
+ rvm:
11
+ - 2.4
12
+ - 2.3
13
+ - 2.2
14
+
15
+ gemfile:
16
+ - gemfiles/rails_4.gemfile
17
+ - gemfiles/rails_5.0.gemfile
18
+ - gemfiles/rails_5.1.gemfile
19
+ - gemfiles/rails_5.2.gemfile
20
+
21
+ before_install:
22
+ - gem install bundler
23
+ - psql -c 'create database alarmable;' -U postgres
24
+
25
+ script: bundle exec rake
@@ -0,0 +1,23 @@
1
+ appraise 'rails-4' do
2
+ gem 'activejob', '4.2.10'
3
+ gem 'activerecord', '4.2.10'
4
+ gem 'activesupport', '4.2.10'
5
+ end
6
+
7
+ appraise 'rails-5.0' do
8
+ gem 'activejob', '5.0.6'
9
+ gem 'activerecord', '5.0.6'
10
+ gem 'activesupport', '5.0.6'
11
+ end
12
+
13
+ appraise 'rails-5.1' do
14
+ gem 'activejob', '5.1.4'
15
+ gem 'activerecord', '5.1.4'
16
+ gem 'activesupport', '5.1.4'
17
+ end
18
+
19
+ appraise 'rails-5.2' do
20
+ gem 'activejob', '5.2.0.beta2'
21
+ gem 'activerecord', '5.2.0.beta2'
22
+ gem 'activesupport', '5.2.0.beta2'
23
+ end
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at hermann.mayer92@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in alarmable.gemspec
8
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Hausgold
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.
@@ -0,0 +1,96 @@
1
+ MAKEFLAGS += --warn-undefined-variables -j1
2
+ SHELL := bash
3
+ .SHELLFLAGS := -eu -o pipefail -c
4
+ .DEFAULT_GOAL := all
5
+ .DELETE_ON_ERROR:
6
+ .SUFFIXES:
7
+ .PHONY:
8
+
9
+ # Environment switches
10
+ MAKE_ENV ?= docker
11
+ COMPOSE_RUN_SHELL_FLAGS ?= --rm
12
+
13
+ # Directories
14
+ VENDOR_DIR ?= vendor/bundle
15
+ GEMFILES_DIR ?= gemfiles
16
+
17
+ # Host binaries
18
+ BASH ?= bash
19
+ COMPOSE ?= docker-compose
20
+ ID ?= id
21
+ MKDIR ?= mkdir
22
+ RM ?= rm
23
+
24
+ # Container binaries
25
+ BUNDLE ?= bundle
26
+ APPRAISAL ?= appraisal
27
+ RAKE ?= rake
28
+
29
+ # Files
30
+ GEMFILES ?= $(subst _,-,$(patsubst $(GEMFILES_DIR)/%.gemfile,%,\
31
+ $(wildcard $(GEMFILES_DIR)/*.gemfile)))
32
+ TEST_GEMFILES := $(GEMFILES:%=test-%)
33
+
34
+ # Define a generic shell run wrapper
35
+ # $1 - The command to run
36
+ ifeq ($(MAKE_ENV),docker)
37
+ define run-shell
38
+ $(COMPOSE) run $(COMPOSE_RUN_SHELL_FLAGS) \
39
+ -e LANG=en_US.UTF-8 -e LANGUAGE=en_US.UTF-8 -e LC_ALL=en_US.UTF-8 \
40
+ -e HOME=/tmp -e BUNDLE_APP_CONFIG=/app/.bundle \
41
+ -u `$(ID) -u` test bash -c 'sleep 0.1; echo; $(1)'
42
+ endef
43
+ else ifeq ($(MAKE_ENV),baremetal)
44
+ define run-shell
45
+ $(1)
46
+ endef
47
+ endif
48
+
49
+ all:
50
+ # Alarmable
51
+ #
52
+ # install Install the dependencies
53
+ # test Run the whole test suite
54
+ # clean Clean the dependencies
55
+ #
56
+ # shell Run an interactive shell on the container
57
+ # shell-irb Run an interactive IRB shell on the container
58
+
59
+ install:
60
+ # Install the dependencies
61
+ @$(MKDIR) -p $(VENDOR_DIR)
62
+ @$(call run-shell,$(BUNDLE) check || $(BUNDLE) install --path $(VENDOR_DIR))
63
+ @$(call run-shell,$(BUNDLE) exec $(APPRAISAL) install)
64
+
65
+ test: install
66
+ # Run the whole test suite
67
+ @$(call run-shell,$(BUNDLE) exec $(RAKE))
68
+
69
+ $(TEST_GEMFILES): GEMFILE=$(@:test-%=%)
70
+ $(TEST_GEMFILES):
71
+ # Run the whole test suite ($(GEMFILE))
72
+ @$(call run-shell,$(BUNDLE) exec $(APPRAISAL) $(GEMFILE) $(RAKE))
73
+
74
+ clean:
75
+ # Clean the dependencies
76
+ @$(RM) -rf $(VENDOR_DIR)
77
+
78
+ clean-containers:
79
+ # Clean running containers
80
+ ifeq ($(MAKE_ENV),docker)
81
+ @$(COMPOSE) down
82
+ endif
83
+
84
+ distclean: clean clean-containers
85
+
86
+ shell: install
87
+ # Run an interactive shell on the container
88
+ @$(call run-shell,$(BASH) -i)
89
+
90
+ shell-irb: install
91
+ # Run an interactive IRB shell on the container
92
+ @$(call run-shell,bin/console)
93
+
94
+ release:
95
+ # Release a new gem version
96
+ @$(RAKE) release
@@ -0,0 +1,121 @@
1
+ ![Alarmable](doc/assets/project.png)
2
+
3
+ [![Build Status](https://api.travis-ci.org/hausgold/alarmable.svg?branch=master)](https://travis-ci.org/hausgold/alarmable)
4
+
5
+ This is a reusable alarm concern for Active Record models. It adds support for
6
+ the automatic maintenance of Active Job's which are scheduled for the given
7
+ alarms. On alarm updates the jobs will be canceled and rescheduled. This is
8
+ supported only for Sidekiq, Delayed Job, resque and the Active Job TestAdapter.
9
+ (See [ActiveJob::Cancel](https://github.com/y-yagi/activejob-cancel) for the
10
+ list of supported adapters)
11
+
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ - [Database migration](#database-migration)
15
+ - [Active Record Model](#active-record-model)
16
+ - [Active Job](#active-job)
17
+ - [Development](#development)
18
+ - [Contributing](#contributing)
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'alarmable'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ ```bash
31
+ $ bundle
32
+ ```
33
+
34
+ Or install it yourself as:
35
+
36
+ ```bash
37
+ $ gem install alarmable
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Database migration
43
+
44
+ This concern requires the persistence (and availability) of two properties.
45
+
46
+ * The first is the JSONB array which holds the alarms. (`alarms`)
47
+ * The seconds is the JSONB array which holds the ids of the
48
+ scheduled alarm jobs. (`alarm_jobs`)
49
+
50
+ ```bash
51
+ $ rails generate migration AddAlarmsAndAlarmJobsToEntity \
52
+ alarms:jsonb alarm_jobs:jsonb
53
+ ```
54
+
55
+ ### Active Record Model
56
+
57
+ Furthermore a Active Record model which uses this concern must define the
58
+ Active Job class which will be scheduled. (`alarm_job`) The user must also
59
+ define the base date property of the owning side.
60
+ (`alarm_base_date_property`) This base date is mandatory to calculate the
61
+ correct alarm date/time. When the base date is not set (`nil`) no new
62
+ notification job will be enqueued. When the base date is unset on an update,
63
+ the previously enqueued job will be canceled.
64
+
65
+ ```ruby
66
+ # Your Active Record Model
67
+ class Entity < ApplicationRecord
68
+ include Alarmable
69
+ self.alarm_job = NotificationJob
70
+ self.alarm_base_date_property = :start_at
71
+ end
72
+ ```
73
+
74
+ The alarms hash needs to be an array in the following format:
75
+
76
+ ```ruby
77
+ [
78
+ {
79
+ "channel": "email", # email, push, web_notification, etc..
80
+ "before_minutes": 15 # start_at - before_minutes, >= 1
81
+
82
+ # [..] you can add custom properties if you like
83
+ }
84
+ ]
85
+ ```
86
+
87
+ ### Active Job
88
+
89
+ The given alarm job class will be scheduled with the following two arguments.
90
+
91
+ * id - The class/instance id of the record which owns the alarm
92
+ * alarm - The alarm hash itself (see the format above)
93
+
94
+ A suitable alarm job perform method should look like this:
95
+
96
+ ```ruby
97
+ # Your notification job
98
+ class NotificationJob < ApplicationJob
99
+ # @param id [String] The entity id
100
+ # @param alarm [Hash] The alarm object
101
+ def perform(id, alarm)
102
+ # Do something special for `alarm.channel` ..
103
+ end
104
+ end
105
+ ```
106
+
107
+ ## Development
108
+
109
+ After checking out the repo, run `make install` to install dependencies. Then,
110
+ run `make test` to run the tests. You can also run `make shell-irb` for an
111
+ interactive prompt that will allow you to experiment.
112
+
113
+ To release a new version, update the version number in `version.rb`, and then
114
+ run `make release`, which will create a git tag for the version, push git
115
+ commits and tags, and push the `.gem` file to
116
+ [rubygems.org](https://rubygems.org).
117
+
118
+ ## Contributing
119
+
120
+ Bug reports and pull requests are welcome on GitHub at
121
+ https://github.com/hausgold/alarmable.
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'alarmable/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'alarmable'
9
+ spec.version = Alarmable::VERSION
10
+ spec.authors = ['Hermann Mayer']
11
+ spec.email = ['hermann.mayer@hausgold.de']
12
+
13
+ spec.summary = 'A reusable alarm extension to Active Record models'
14
+ spec.description = 'This is a reusable alarm concern for Active Record' \
15
+ 'models. It adds support for the automatic maintenance' \
16
+ 'of Active Job\'s which are scheduled for the given' \
17
+ 'alarms.'
18
+
19
+ spec.homepage = 'https://github.com/hausgold/alarmable'
20
+ spec.license = 'MIT'
21
+
22
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{^(test|spec|features)/})
24
+ end
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'activejob', '>=3', '<6'
30
+ spec.add_dependency 'activejob-cancel', '~> 0.3'
31
+ spec.add_dependency 'activerecord', '>=3', '<6'
32
+ spec.add_dependency 'activesupport', '>=3', '<6'
33
+ spec.add_dependency 'hashdiff', '~> 0.3.7'
34
+
35
+ spec.add_development_dependency 'appraisal'
36
+ spec.add_development_dependency 'bundler', '~> 1.15'
37
+ spec.add_development_dependency 'rake', '~> 10.0'
38
+ spec.add_development_dependency 'rspec', '~> 3.0'
39
+ spec.add_development_dependency 'pg', '~> 0.18'
40
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'alarmable'
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__)
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,21 @@
1
+ version: "3"
2
+ services:
3
+ db:
4
+ image: postgres:9.6
5
+ network_mode: bridge
6
+ ports: ["5432"]
7
+ volumes:
8
+ - .:/app
9
+ environment:
10
+ POSTGRES_USER: postgres
11
+ POSTGRES_PASSWORD: postgres
12
+ POSTGRES_DB: alarmable
13
+
14
+ test:
15
+ image: ruby:2.3
16
+ network_mode: bridge
17
+ working_dir: /app
18
+ volumes:
19
+ - .:/app
20
+ links:
21
+ - db
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activejob", "4.2.10"
6
+ gem "activerecord", "4.2.10"
7
+ gem "activesupport", "4.2.10"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activejob", "5.0.6"
6
+ gem "activerecord", "5.0.6"
7
+ gem "activesupport", "5.0.6"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activejob", "5.1.4"
6
+ gem "activerecord", "5.1.4"
7
+ gem "activesupport", "5.1.4"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activejob", "5.2.0.beta2"
6
+ gem "activerecord", "5.2.0.beta2"
7
+ gem "activesupport", "5.2.0.beta2"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_record'
5
+ require 'active_job'
6
+ require 'active_job/cancel'
7
+ require 'hashdiff'
8
+
9
+ require 'alarmable/version'
10
+ require 'alarmable/concern'
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A reusable alarm extension to Active Record models. It adds support for the
4
+ # maintenance of Active Job's (create, update (cancel)) which are schedules for
5
+ # the given alarms. We check for changes on the alarms hash and perform
6
+ # updates accordingly.
7
+ #
8
+ # This concern requires the persistence (and availability) of two properties.
9
+ #
10
+ # * The first is the JSONB array which holds the alarms. (+alarms+)
11
+ # * The seconds is the JSONB array which holds the ids of
12
+ # scheduled alarm jobs. (+alarm_jobs+)
13
+ #
14
+ # rails generate migration AddAlarmsAndAlarmJobsToEntity \
15
+ # alarms:jsonb alarm_jobs:jsonb
16
+ #
17
+ # Furthermore a Active Record model which uses this concern must define the
18
+ # Active Job class which will be scheduled. (+alarm_job+) The user must also
19
+ # define the base date property of the owning side.
20
+ # (+alarm_base_date_property+) This base date is mandatory to calculate the
21
+ # correct alarm date/time. When the base date is not set (+nil+) no new
22
+ # notification job will be enqueued. When the base date is unset on an update,
23
+ # the previously enqueued job will be canceled.
24
+ #
25
+ # The alarms hash needs to be an array in the following format:
26
+ #
27
+ # [
28
+ # {
29
+ # "channel": "email", # email, push, web_notification, etc..
30
+ # "before_minutes": 15 # start_at - before_minutes, >= 1
31
+ # }
32
+ # ]
33
+ #
34
+ # The given alarm job class will be scheduled with the following two arguments.
35
+ #
36
+ # * id - The class/instance id of the record which owns the alarm
37
+ # * alarm - The alarm hash itself (see the format above)
38
+ #
39
+ # A suitable alarm job perform method should look like this:
40
+ #
41
+ # # @param id [String] The entity id
42
+ # # @param alarm [Hash] The alarm object
43
+ # def perform(id, alarm)
44
+ # # Do something special for +alarm.channel+ ..
45
+ # end
46
+ module Alarmable
47
+ extend ActiveSupport::Concern
48
+
49
+ class_methods do
50
+ # Getter/Setter
51
+ #
52
+ # :reek:Attribute because thats what this thing is about
53
+ attr_accessor :alarm_job, :alarm_base_date_property
54
+ end
55
+
56
+ # rubocop:disable Metrics/BlockLength because Active Support like it
57
+ included do
58
+ # Hooks
59
+ after_initialize :validate_alarm_settings, :alarm_defaults
60
+
61
+ # Here comes a little cheat sheet when and what action is performed
62
+ # on the alarm jobs.
63
+ #
64
+ # create | [ time check, reschedule]
65
+ # update | dirty check, [cancel job, time check, reschedule]
66
+ # destroy | [cancel job ]
67
+ after_create :reschedule_alarm_jobs
68
+ before_update :alarms_update_callback
69
+ before_destroy :alarms_destroy_callback
70
+
71
+ # Getter for the alarm job class.
72
+ #
73
+ # @return [Class] The alarm job class
74
+ def alarm_job
75
+ self.class.alarm_job
76
+ end
77
+
78
+ # Getter for the alarm base date property.
79
+ #
80
+ # @return [Symbol] The user defined base date property
81
+ def alarm_base_date_property
82
+ self.class.alarm_base_date_property
83
+ end
84
+
85
+ # Set some defaults on the relevant alarm properties.
86
+ def alarm_defaults
87
+ self.alarms ||= []
88
+ self.alarm_jobs ||= {}
89
+ end
90
+
91
+ # Validate the presence of the +alarm_job+ property and the accessibility
92
+ # of the specified class. Also validate the +alarm_base_date_property+
93
+ # setting.
94
+ #
95
+ # rubocop:disable Style/GuardClause because its fine like this
96
+ # :reek:NilCheck because we validate concern usage
97
+ def validate_alarm_settings
98
+ raise 'Alarmable +alarm_job+ is not configured' if alarm_job.nil?
99
+ unless alarm_job.is_a? Class
100
+ raise 'Alarmable +alarm_job+ is not instantiable'
101
+ end
102
+ if alarm_base_date_property.nil?
103
+ raise 'Alarmable +alarm_base_date_property+ is not configured'
104
+ end
105
+ unless has_attribute? alarm_base_date_property
106
+ raise 'Alarmable +alarm_base_date_property+ is not usable'
107
+ end
108
+ end
109
+ # rubocop:enable Style/GuardClause
110
+
111
+ # Generate a unique and recalculatable identifier for a given alarm object.
112
+ # We build a hash of the primary keys (before_minutes and channel) to
113
+ # achive this. Afterwards, this alarm id is used to reference dedicated
114
+ # scheduled jobs and track their updates. (Or cancel them accordingly)
115
+ #
116
+ # @param channel [String] The alarm channel
117
+ # @param before_minutes [Integer] The minutes before the alarm starts
118
+ # @return [String] The unique alarm id
119
+ #
120
+ # :reek:UtilityFunction because its a utility, for sure
121
+ def alarm_id(channel, before_minutes)
122
+ (Digest::MD5.new << "#{channel}#{before_minutes}").to_s
123
+ end
124
+
125
+ # Schedule a new Active Job for the alarm notification. This method takes
126
+ # care of the notification time (+date) and will not touch anything when
127
+ # the desired time already passed. It cancels the correct job for the
128
+ # given combination, when it is present. In the end it schedules a new
129
+ # (renewed) job for the given alarm settings.
130
+ #
131
+ # @param alarm [Hash] The alarm object
132
+ # @return [Object] The new alarm_jobs instance (partial)
133
+ # Example: { "alarm id": "job id" }
134
+ #
135
+ # rubocop:disable Metrics/AbcSize because its already broken down
136
+ # :reek:TooManyStatements because see above
137
+ # :reek:NilCheck because we dont want to cancel 'nil' job id
138
+ # :reek:DuplicateMethodCall because hash access is fast
139
+ def reschedule_alarm_job(alarm)
140
+ # Symbolize the hash keys (just to be sure).
141
+ alarm = alarm.symbolize_keys
142
+
143
+ # Calculate the alarm id for job canceling and cancel a found job.
144
+ id = alarm_id(alarm[:channel], alarm[:before_minutes])
145
+ previous_job_id = alarm_jobs.try(:[], id)
146
+ alarm_job.cancel(previous_job_id) unless previous_job_id.nil?
147
+
148
+ base_date = self[alarm_base_date_property]
149
+
150
+ # When the base date is not set, we schedule not a new notification job.
151
+ return {} if base_date.nil?
152
+
153
+ # Calculate the time when the job should run.
154
+ notify_at = base_date - alarm[:before_minutes].minutes
155
+
156
+ # Do nothing when the notification date already passed.
157
+ return {} if Time.current >= notify_at
158
+
159
+ # Put a new job to the queue with the new (current) job execution date.
160
+ job = alarm_job.set(wait_until: notify_at).perform_later(self.id, alarm)
161
+
162
+ # Construct a new alarm_jobs partial instance for this job
163
+ Hash[id, job.job_id]
164
+ end
165
+ # rubocop:enable Metrics/AbcSize
166
+
167
+ # Initiate a reschedule for each alarm in the alarm settings and
168
+ # cancel all left-overs.
169
+ #
170
+ # rubocop:disable Rails/SkipsModelValidations because we need to skip them
171
+ # :reek:TooManyStatements because its already broken down
172
+ def reschedule_alarm_jobs
173
+ # Perform the reschedule of all the current alarms.
174
+ new_alarm_jobs = alarms.each_with_object({}) do |alarm, memo|
175
+ memo.merge!(reschedule_alarm_job(alarm))
176
+ end
177
+
178
+ # Detect the differences from the original alarm_jobs hash to the new
179
+ # built (by partials) alarm_jobs hash. The jobs from negative differences
180
+ # must be canceled.
181
+ diff = HashDiff.diff(alarm_jobs, new_alarm_jobs)
182
+
183
+ diff.select { |prop| prop.first == '-' }.each do |prop|
184
+ alarm_job.cancel(prop.last)
185
+ end
186
+
187
+ # Update the alarm_jobs reference pool with our fresh hash. Bypass the
188
+ # regular validation and callbacks here, this is required to not stuck in
189
+ # endless create-update loops.
190
+ update_columns(alarm_jobs: new_alarm_jobs)
191
+ end
192
+ # rubocop:enable Rails/SkipsModelValidations
193
+
194
+ # Reschedule only on updates when the alarm settings are changed.
195
+ def alarms_update_callback
196
+ reschedule_alarm_jobs if alarms_changed?
197
+ end
198
+
199
+ # Cancel all alarm notification jobs on parent destroy.
200
+ def alarms_destroy_callback
201
+ alarm_jobs.each_value { |job_id| alarm_job.cancel(job_id) }
202
+ end
203
+ end
204
+ # rubocop:enable Metrics/BlockLength
205
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alarmable
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,229 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alarmable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hermann Mayer
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '6'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '6'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activejob-cancel
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.3'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.3'
47
+ - !ruby/object:Gem::Dependency
48
+ name: activerecord
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '6'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '3'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '6'
67
+ - !ruby/object:Gem::Dependency
68
+ name: activesupport
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '3'
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '6'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '3'
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '6'
87
+ - !ruby/object:Gem::Dependency
88
+ name: hashdiff
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: 0.3.7
94
+ type: :runtime
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: 0.3.7
101
+ - !ruby/object:Gem::Dependency
102
+ name: appraisal
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ type: :development
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ - !ruby/object:Gem::Dependency
116
+ name: bundler
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '1.15'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '1.15'
129
+ - !ruby/object:Gem::Dependency
130
+ name: rake
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: '10.0'
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - "~>"
141
+ - !ruby/object:Gem::Version
142
+ version: '10.0'
143
+ - !ruby/object:Gem::Dependency
144
+ name: rspec
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - "~>"
148
+ - !ruby/object:Gem::Version
149
+ version: '3.0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - "~>"
155
+ - !ruby/object:Gem::Version
156
+ version: '3.0'
157
+ - !ruby/object:Gem::Dependency
158
+ name: pg
159
+ requirement: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - "~>"
162
+ - !ruby/object:Gem::Version
163
+ version: '0.18'
164
+ type: :development
165
+ prerelease: false
166
+ version_requirements: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - "~>"
169
+ - !ruby/object:Gem::Version
170
+ version: '0.18'
171
+ description: This is a reusable alarm concern for Active Recordmodels. It adds support
172
+ for the automatic maintenanceof Active Job's which are scheduled for the givenalarms.
173
+ email:
174
+ - hermann.mayer@hausgold.de
175
+ executables: []
176
+ extensions: []
177
+ extra_rdoc_files: []
178
+ files:
179
+ - ".editorconfig"
180
+ - ".gitignore"
181
+ - ".rspec"
182
+ - ".rubocop.yml"
183
+ - ".travis.yml"
184
+ - Appraisals
185
+ - CODE_OF_CONDUCT.md
186
+ - Gemfile
187
+ - LICENSE.txt
188
+ - Makefile
189
+ - README.md
190
+ - Rakefile
191
+ - alarmable.gemspec
192
+ - bin/console
193
+ - bin/setup
194
+ - doc/assets/logo.png
195
+ - doc/assets/project.png
196
+ - doc/assets/project.xcf
197
+ - docker-compose.yml
198
+ - gemfiles/rails_4.gemfile
199
+ - gemfiles/rails_5.0.gemfile
200
+ - gemfiles/rails_5.1.gemfile
201
+ - gemfiles/rails_5.2.gemfile
202
+ - lib/alarmable.rb
203
+ - lib/alarmable/concern.rb
204
+ - lib/alarmable/version.rb
205
+ homepage: https://github.com/hausgold/alarmable
206
+ licenses:
207
+ - MIT
208
+ metadata: {}
209
+ post_install_message:
210
+ rdoc_options: []
211
+ require_paths:
212
+ - lib
213
+ required_ruby_version: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '0'
218
+ required_rubygems_version: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ requirements: []
224
+ rubyforge_project:
225
+ rubygems_version: 2.6.14
226
+ signing_key:
227
+ specification_version: 4
228
+ summary: A reusable alarm extension to Active Record models
229
+ test_files: []