alarmable 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []