newsletterable 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 25e624e8412c36225fbf5abeca8dc9719a9f862b
4
+ data.tar.gz: b189d86b3c90404fcb0a23598ab2deb8b7192589
5
+ SHA512:
6
+ metadata.gz: adb51010471ea00c312979fcee7b589973110ce232922b1873fef2dcb826b6f4eb3e1c8c057a40ddb1459f864517c7b582ae3dfccbbef80736363ef211ef586c
7
+ data.tar.gz: b46d19892e07d937806632b92d6b54fe827f285b82774f3ddff756a89bc765cdced395e7362ad96e49925c8ab92424e9930508309272edba23527791f3e1b1a8
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in newsletterable.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'pry-byebug'
8
+ end
data/Guardfile ADDED
@@ -0,0 +1,14 @@
1
+ guard :rspec, cmd: "bundle exec rspec" do
2
+ require "guard/rspec/dsl"
3
+ dsl = Guard::RSpec::Dsl.new(self)
4
+
5
+ # RSpec files
6
+ rspec = dsl.rspec
7
+ watch(rspec.spec_helper) { rspec.spec_dir }
8
+ watch(rspec.spec_support) { rspec.spec_dir }
9
+ watch(rspec.spec_files)
10
+
11
+ # Ruby files
12
+ ruby = dsl.ruby
13
+ dsl.watch_spec_files_for(ruby.lib_files)
14
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Stan Bondi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Newsletterable
2
+
3
+ Keep mailing list subscriptions in sync with your
4
+ ActiveRecord or Mongoid (ActiveModel) models.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'newsletterable'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install newsletterable
21
+
22
+ ## Usage
23
+
24
+ Your initializer:
25
+
26
+
27
+ ```ruby
28
+ Newsletterable.configure do |config|
29
+ # Mailchimp API key
30
+ config.api_key = ENV['api_key']
31
+
32
+ # Default lists
33
+ config.default_lists = {
34
+ newsletter: '123abc',
35
+ everyone: 'cba321'
36
+ }
37
+
38
+ # Specify worker class or custom callback for enqueuing to your worker.
39
+ # Workers usually include Newsletterable::Worker (defines the perform method)
40
+ config.worker = MyWorker
41
+ # or define a callable which will enqueue anyway you like, or synchonously
42
+ config.worker = lambda { |id| SleepyWorker.perform_async('NewsletterTask', id.to_s) }
43
+ end
44
+ ```
45
+
46
+ Your subscriber model e.g User class
47
+ Must respond_to `email` (TODO: make this configurable)
48
+
49
+ ```ruby
50
+ class User < ActiveRecord::Base
51
+ include Newsletterable::Subscriber
52
+
53
+ has_many :newsletter_subscriptions, as: :subscribable
54
+
55
+ # Boolean fields `newsletter` and `everyone`
56
+ subscribe_on :newsletter
57
+ # will unsubscribe if the subscriber class is destroyed
58
+ subscribe_on :everyone, unsubscribe_on_destroy: true
59
+ end
60
+ ```
61
+
62
+ Your Subscription model (each row is a subscription to a mailchimp list)
63
+ Here using mongoid so that we can easier see the required methods/fields.
64
+
65
+ ```ruby
66
+ class NewsletterSubscription
67
+ include Mongoid::Document
68
+ include Newsletterable::Model
69
+
70
+ # This must be an association to the subscribable object
71
+ belongs_to :subscribable, polymorphic: true
72
+
73
+ # Used to store old email addresses when object is destroyed or email updated
74
+ field :old_email, type: String
75
+ # List id of suibscription
76
+ field :list, type: String
77
+ # Either pending subscribed unsubscribed out_of_date or error
78
+ field :state, type: String
79
+ end
80
+
81
+ ```
82
+
83
+ Add a worker (using anything you like)
84
+
85
+ ```ruby
86
+
87
+ class MyWorker
88
+ include Sidekiq::Worker
89
+ include Newsletterable::Worker
90
+
91
+ sidekiq_options ....
92
+ end
93
+ ```
94
+
95
+ ## Contributing
96
+
97
+ 1. Fork it ( https://github.com/fixate/newsletterable/fork )
98
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
99
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
100
+ 4. Push to the branch (`git push origin my-new-feature`)
101
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,58 @@
1
+ require "active_model"
2
+ require "active_support/concern"
3
+ require "active_support/core_ext"
4
+
5
+ require "newsletterable/version"
6
+ require "newsletterable/configuration"
7
+ require "newsletterable/errors"
8
+ require "newsletterable/model"
9
+ require "newsletterable/service"
10
+ require "newsletterable/subscriber"
11
+ require "newsletterable/worker"
12
+ require "newsletterable/orm_adapters/adapter"
13
+ require "newsletterable/railtie" if defined?(Rails)
14
+
15
+ module Newsletterable
16
+ extend self
17
+ @@lock = Mutex.new
18
+
19
+ def configuration
20
+ @@lock.synchronize do
21
+ @@configuration ||= Configuration.new(defaults)
22
+ end
23
+ end
24
+
25
+ def configure
26
+ yield configuration
27
+ end
28
+
29
+ def self.eager_load!
30
+ # No autoloads
31
+ end
32
+
33
+ private
34
+
35
+ def defaults
36
+ {
37
+ worker: Newsletterable::Worker,
38
+ list_resolver: default_list_resolver
39
+ }
40
+ end
41
+
42
+ def default_list_resolver
43
+ proc do |subscriber, list_name, lists|
44
+ result = case lists
45
+ when String, Array
46
+ lists
47
+ when Hash
48
+ lists[list_name]
49
+ else
50
+ if lists.respond_to?(:call)
51
+ lists.call(subscriber)
52
+ end
53
+ end
54
+
55
+ Array.wrap(result)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,12 @@
1
+ module Newsletterable
2
+ class Configuration
3
+ attr_accessor :api_key, :default_lists, :subscriptions_model, :worker,
4
+ :list_resolver, :orm_adapter
5
+
6
+ def initialize(attrs = {})
7
+ attrs.each do |k, v|
8
+ send(:"#{k}=", v)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Newsletterable
2
+ class ConfigurationError < RuntimeError; end
3
+ end
@@ -0,0 +1,67 @@
1
+ module Newsletterable
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ Newsletterable.configuration.subscriptions_model = self
7
+
8
+ define_newsletterable_state_methods(self)
9
+ define_newsletterable_callbacks(self)
10
+ end
11
+
12
+ def email
13
+ subscribable.nil? ? self.old_email : subscribable.email
14
+ end
15
+
16
+ def enqueue_subscription
17
+ worker = Newsletterable.configuration.worker
18
+ case worker
19
+ when Symbol, String
20
+ # Get worker from method in model
21
+ self.send(worker)
22
+ else
23
+ # Get worker from lambda function
24
+ if worker.respond_to?(:call)
25
+ worker.call(self.id)
26
+ # worker is already a Worker class, call perform_async
27
+ # TODO: should we require perform_async, make it configurable or make this an adapter
28
+ elsif worker.respond_to?(:perform_async)
29
+ worker.perform_async(self.id)
30
+ else
31
+ raise ConfigurationError, "config.worker is nil or invalid."
32
+ end
33
+ end
34
+ end
35
+
36
+ module ClassMethods
37
+ def define_newsletterable_callbacks(klass)
38
+ klass.class_eval do
39
+ opts =[:enqueue_subscription, {
40
+ if: proc { |n| !n.subscribed? },
41
+ on: [:create, :update]
42
+ }]
43
+
44
+ if respond_to?(:after_commit)
45
+ after_commit(*opts)
46
+ else
47
+ after_save(*opts)
48
+ end
49
+ end
50
+ end
51
+
52
+ def define_newsletterable_state_methods(klass)
53
+ %i{ pending subscribed unsubscribed out_of_date error }.each do |state|
54
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
55
+ def #{state}!
56
+ self.state = '#{state}'
57
+ end
58
+
59
+ def #{state}?
60
+ self.state.to_s == '#{state}'
61
+ end
62
+ RUBY
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,21 @@
1
+ module Newsletterable
2
+ module OrmAdapters
3
+ class ActiveModel < Adapter
4
+ def query_subscription(query, initialize = false)
5
+ scope = subscriptions_model.where(query)
6
+
7
+ if initialize
8
+ scope.first_or_initialize do |s|
9
+ s.pending!
10
+ end
11
+ else
12
+ scope.first
13
+ end
14
+ end
15
+
16
+ def save(record)
17
+ record.save!
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ require 'newsletterable/orm_adapters/active_model'
2
+
3
+ module Newsletterable
4
+ module OrmAdapters
5
+ class ActiveRecord < ActiveModel
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ module Newsletterable
2
+ module OrmAdapters
3
+ class Adapter
4
+
5
+ def self.factory(name)
6
+ klass = "Newsletterable::OrmAdapters::#{name.to_s.camelize}"
7
+ unless (Module.const_get(klass).is_a?(Class) rescue false)
8
+ require "newsletterable/orm_adapters/#{name}"
9
+ end
10
+ klass.constantize.new
11
+ end
12
+
13
+ protected
14
+
15
+ def subscriptions_model
16
+ Newsletterable.configuration.subscriptions_model
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ require 'newsletterable/orm_adapters/active_model'
2
+
3
+ module Newsletterable
4
+ module OrmAdapters
5
+ class Mongoid < ActiveModel
6
+ def query_subscription(query, initialize = true)
7
+ # Work around for https://github.com/mongoid/mongoid/issues/4101
8
+ if initialize && query[:subscribable]
9
+ subscribable = query.delete(:subscribable)
10
+ query[:subscribable_id] = subscribable.id
11
+ query[:subscribable_type] = subscribable.class.to_s
12
+ end
13
+
14
+ super(query, initialize)
15
+ end
16
+
17
+ def save(record)
18
+ record.with(safe: true).save!
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ require "rails"
2
+
3
+ module MandrillQueue
4
+ class Railtie < Rails::Railtie # :nodoc:
5
+ config.eager_load_namespaces << Newsletterable
6
+
7
+ initializer "newsletterable.initialize" do |app|
8
+ Newsletterable.configure do |config|
9
+ if defined?(ActiveRecord)
10
+ config.orm_adapter = :active_record
11
+ elsif defined?(Mongoid)
12
+ config.orm_adapter = :mongoid
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,61 @@
1
+ module Newsletterable
2
+ class Service
3
+ attr_reader :subscribable
4
+
5
+ def initialize(subscribable)
6
+ @subscribable = subscribable
7
+ end
8
+
9
+ def subscribe(lists)
10
+ lists.each do |list|
11
+ subscription = orm_adapter.query_subscription({
12
+ subscribable: subscribable,
13
+ list: list
14
+ }, true)
15
+
16
+ if subscription.unsubscribed? || subscription.error?
17
+ subscription.pending!
18
+ end
19
+
20
+ orm_adapter.save(subscription) unless subscription.subscribed?
21
+ end
22
+ end
23
+
24
+ def unsubscribe(lists)
25
+ lists.each do |list|
26
+ subscription = orm_adapter.query_subscription(
27
+ subscribable: subscribable,
28
+ list: list
29
+ )
30
+ unless subscription.nil?
31
+ subscription.old_email = subscribable.email
32
+ subscription.unsubscribed!
33
+ orm_adapter.save(subscription)
34
+ end
35
+ end
36
+ end
37
+
38
+ def update(lists, old_email)
39
+ lists.each do |list|
40
+ subscription = orm_adapter.query_subscription(
41
+ subscribable: subscribable,
42
+ list: list
43
+ )
44
+
45
+ if subscription.nil?
46
+ raise RuntimeError, "No subscription to update for subscribable #{subscribable.id}."
47
+ end
48
+
49
+ subscription.out_of_date!
50
+ subscription.old_email = old_email
51
+ orm_adapter.save(subscription)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def orm_adapter
58
+ @orm_adapter ||= OrmAdapters::Adapter.factory(Newsletterable.configuration.orm_adapter)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,84 @@
1
+ module Newsletterable
2
+ module Subscriber
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_eval %(
7
+ class << self; attr_accessor :__newsletterable_options; end
8
+ )
9
+ end
10
+
11
+ def subscription_service
12
+ @subscription_service ||= Service.new(self)
13
+ end
14
+
15
+ def manage_subscription(list_name)
16
+ field = field_for(list_name)
17
+ list_ids = resolve_list(list_name)
18
+
19
+ if send(field)
20
+ subscription_service.subscribe(list_ids)
21
+ else
22
+ subscription_service.unsubscribe(list_ids)
23
+ end
24
+ end
25
+
26
+ def resolve_list(list_name)
27
+ options = options_for(list_name)
28
+ lists = options[:lists] || Newsletterable.configuration.default_lists
29
+
30
+ resolver = options[:resolver] || Newsletterable.configuration.list_resolver
31
+
32
+ if resolver.is_a?(Symbol)
33
+ self.send(resolver, list_name, lists)
34
+ elsif resolver.respond_to?(:call)
35
+ resolver.call(self, list_name, lists)
36
+ else
37
+ raise ConfigurationError, 'Newsletterable resolver should be a Symbol or callable.'
38
+ end
39
+ end
40
+
41
+ def update_subscription(list_name)
42
+ old_email = email_was
43
+
44
+ yield
45
+
46
+ list_ids = resolve_list(list_name)
47
+ subscription_service.update(list_ids, old_email)
48
+ end
49
+
50
+ def remove_subscription(list_name)
51
+ list_ids = resolve_list(list_name)
52
+ subscription_service.unsubscribe(list_ids)
53
+ end
54
+
55
+ def options_for(list_name)
56
+ self.class.__newsletterable_options[list_name]
57
+ end
58
+
59
+ def field_for(list_name)
60
+ if options = options_for(list_name)
61
+ options[:field]
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
67
+ module ClassMethods
68
+
69
+ def subscribe_on(list_name, options = {})
70
+ field = options[:field] || list_name
71
+
72
+ self.__newsletterable_options ||= {}
73
+ self.__newsletterable_options[list_name] = options.merge(field: field)
74
+
75
+ after_save -> { manage_subscription(list_name) }, if: :"#{field}_changed?"
76
+ around_update -> { update_subscription(list_name) }, if: -> { send(:"#{field}?") && email_changed? }
77
+ unless options[:unsubscribe_on_destroy]
78
+ before_destroy -> { remove_subscription(list_name) }, if: :"#{field}?"
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,3 @@
1
+ module Newsletterable
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,74 @@
1
+ require 'mailchimp'
2
+
3
+ module Newsletterable
4
+ module Worker
5
+ extend ActiveSupport::Concern
6
+
7
+ def perform(subscription_id)
8
+ @subscription = orm_adapter.query_subscription(id: subscription_id)
9
+
10
+ if @subscription.nil?
11
+ logger.warn "No subscription with id #{subscription_id} exists. Ignoring."
12
+ else
13
+ process
14
+ end
15
+ end
16
+
17
+ def process
18
+ logger.info "Processing mailing list subscription for #{@subscription.email}..."
19
+ logger.debug "Status: [#{@subscription.state}]"
20
+
21
+ case @subscription.state.to_sym
22
+ when :pending || :error
23
+ logger.info "Subscribing #{@subscription.email} to '#{@subscription.list}' list"
24
+ logger.info "Trying to subscribe after previous error!" if @subscription.error?
25
+ add(@subscription)
26
+ @subscription.subscribed!
27
+ @subscription.save!
28
+ when :unsubscribed
29
+ logger.info "Unsubscribing #{@subscription.email} from '#{@subscription.list}' list"
30
+ remove(@subscription)
31
+ @subscription.destroy!
32
+ when :out_of_date
33
+ logger.info "Updating subscription for #{@subscription.email}"
34
+ update(@subscription)
35
+ @subscription.old_email = nil
36
+ @subscription.subscribed!
37
+ @subscription.save!
38
+ end
39
+
40
+ rescue Mailchimp::Error => ex
41
+ logger.error "#{@subscription.email}: #{ex.message || 'Unknown error.'}"
42
+ @subscription.error!
43
+ @subscription.save
44
+ raise
45
+ end
46
+
47
+ private
48
+
49
+ def orm_adapter
50
+ @orm_adapter ||= OrmAdapters::Adapter.factory(Newsletterable.configuration.orm_adapter)
51
+ end
52
+
53
+ def mailchimp
54
+ @mailchimp_api ||= Mailchimp::API.new(Newsletterable.configuration.api_key)
55
+ end
56
+
57
+ def add(subscription)
58
+ mailchimp.lists.subscribe(subscription.list, { email: subscription.email }, {}, 'html', false)
59
+ rescue Mailchimp::ListAlreadySubscribedError
60
+ logger.warn "#{subscription.email} already subscribed to '#{subscription.list}'."
61
+ end
62
+
63
+ def remove(subscription)
64
+ mailchimp.lists.unsubscribe(subscription.list, { email: subscription.email })
65
+ rescue Mailchimp::ListNotSubscribedError
66
+ logger.warn "#{subscription.email} is not subscribed to '#{subscription.list}'."
67
+ end
68
+
69
+ def update(subscription)
70
+ raise UpdateError, "old_email required to update subscription for subscription #{subscription.id}." if subscription.old_email.nil?
71
+ mailchimp.lists.subscribe(subscription.list, { email: subscription.old_email }, { 'new-email' => subscription.email }, 'html', true, true)
72
+ end
73
+ end
74
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: newsletterable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stan Bondi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mailchimp-api
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.7'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: temping
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - stan@fixate.it
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - Gemfile
119
+ - Guardfile
120
+ - LICENSE.txt
121
+ - README.md
122
+ - Rakefile
123
+ - lib/newsletterable.rb
124
+ - lib/newsletterable/configuration.rb
125
+ - lib/newsletterable/errors.rb
126
+ - lib/newsletterable/model.rb
127
+ - lib/newsletterable/orm_adapters/active_model.rb
128
+ - lib/newsletterable/orm_adapters/active_record.rb
129
+ - lib/newsletterable/orm_adapters/adapter.rb
130
+ - lib/newsletterable/orm_adapters/mongoid.rb
131
+ - lib/newsletterable/railtie.rb
132
+ - lib/newsletterable/service.rb
133
+ - lib/newsletterable/subscriber.rb
134
+ - lib/newsletterable/version.rb
135
+ - lib/newsletterable/worker.rb
136
+ homepage: ''
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.4.5
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Keep mailing list subscriptions in sync with your models
160
+ test_files: []