pushcart 0.0.1.proto1

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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +7 -0
  4. data/Rakefile +40 -0
  5. data/app/assets/javascripts/pushcart/application.js +13 -0
  6. data/app/assets/stylesheets/pushcart/application.css +15 -0
  7. data/app/controllers/concerns/pushcart/getable.rb +127 -0
  8. data/app/controllers/concerns/pushcart/postable.rb +113 -0
  9. data/app/controllers/pushcart/application_controller.rb +4 -0
  10. data/app/controllers/pushcart/pub_sub/base_controller.rb +4 -0
  11. data/app/controllers/pushcart/pub_sub/subscriptions_controller.rb +8 -0
  12. data/app/helpers/pushcart/application_helper.rb +4 -0
  13. data/app/jobs/pushcart/subscription_renewal_job.rb +9 -0
  14. data/app/views/layouts/pushcart/application.html.erb +14 -0
  15. data/config/routes.rb +6 -0
  16. data/lib/pushcart.rb +16 -0
  17. data/lib/pushcart/composable.rb +10 -0
  18. data/lib/pushcart/connectable.rb +34 -0
  19. data/lib/pushcart/deliverable.rb +5 -0
  20. data/lib/pushcart/engine.rb +5 -0
  21. data/lib/pushcart/errors.rb +4 -0
  22. data/lib/pushcart/requestable.rb +68 -0
  23. data/lib/pushcart/stateable.rb +166 -0
  24. data/lib/pushcart/subscribable.rb +25 -0
  25. data/lib/pushcart/version.rb +3 -0
  26. data/lib/tasks/pushcart_tasks.rake +4 -0
  27. data/test/connectable_test.rb +24 -0
  28. data/test/dummy/README.rdoc +28 -0
  29. data/test/dummy/Rakefile +6 -0
  30. data/test/dummy/app/assets/javascripts/application.js +13 -0
  31. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  32. data/test/dummy/app/controllers/application_controller.rb +5 -0
  33. data/test/dummy/app/helpers/application_helper.rb +2 -0
  34. data/test/dummy/app/models/subscription.rb +9 -0
  35. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  36. data/test/dummy/bin/bundle +3 -0
  37. data/test/dummy/bin/rails +4 -0
  38. data/test/dummy/bin/rake +4 -0
  39. data/test/dummy/bin/setup +29 -0
  40. data/test/dummy/config.ru +4 -0
  41. data/test/dummy/config/application.rb +26 -0
  42. data/test/dummy/config/boot.rb +5 -0
  43. data/test/dummy/config/database.yml +25 -0
  44. data/test/dummy/config/environment.rb +5 -0
  45. data/test/dummy/config/environments/development.rb +41 -0
  46. data/test/dummy/config/environments/production.rb +79 -0
  47. data/test/dummy/config/environments/test.rb +42 -0
  48. data/test/dummy/config/initializers/assets.rb +11 -0
  49. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  50. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  51. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  52. data/test/dummy/config/initializers/inflections.rb +16 -0
  53. data/test/dummy/config/initializers/mime_types.rb +4 -0
  54. data/test/dummy/config/initializers/session_store.rb +3 -0
  55. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  56. data/test/dummy/config/locales/en.yml +23 -0
  57. data/test/dummy/config/routes.rb +4 -0
  58. data/test/dummy/config/secrets.yml +22 -0
  59. data/test/dummy/db/migrate/20151016222203_create_subscriptions.rb +59 -0
  60. data/test/dummy/db/schema.rb +51 -0
  61. data/test/dummy/db/test.sqlite3 +0 -0
  62. data/test/dummy/log/development.log +0 -0
  63. data/test/dummy/log/test.log +5270 -0
  64. data/test/dummy/public/404.html +67 -0
  65. data/test/dummy/public/422.html +67 -0
  66. data/test/dummy/public/500.html +66 -0
  67. data/test/dummy/public/favicon.ico +0 -0
  68. data/test/fixtures/subscriptions.yml +6 -0
  69. data/test/integration/content_delivery_test.rb +58 -0
  70. data/test/integration/hub_request_test.rb +127 -0
  71. data/test/pushcart_test.rb +7 -0
  72. data/test/requestable_test.rb +7 -0
  73. data/test/routes_test.rb +9 -0
  74. data/test/state_test.rb +13 -0
  75. data/test/test_helper.rb +27 -0
  76. metadata +349 -0
@@ -0,0 +1,5 @@
1
+ module Pushcart
2
+ module Deliverable
3
+ extend ActiveSupport::Concern
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Pushcart
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Pushcart
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Pushcart
2
+ class ConfigurationError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,68 @@
1
+ module Pushcart
2
+ module Requestable
3
+ extend ActiveSupport::Concern
4
+
5
+ def request_subscription
6
+ request(:subscription)
7
+ end
8
+
9
+ def request_renewal
10
+ update_columns(
11
+ renewal_requested_at: nil,
12
+ renewal_request_responded_at: nil,
13
+ renewal_request_response_code: nil,
14
+ renewal_request_response_error: nil,
15
+ renewal_denied_at: nil,
16
+ renewal_denial_reason: nil,
17
+ renewal_challenged_at: nil,
18
+ renewal_verified_at: nil,
19
+ )
20
+
21
+ request(:renewal)
22
+ end
23
+
24
+ def request_unsubscription
25
+ request(:unsubscription)
26
+ end
27
+
28
+ def request(type)
29
+ modes = {
30
+ subscription: "subscribe",
31
+ renewal: "subscribe",
32
+ unsubscription: "unsubscribe"
33
+ }
34
+
35
+ mode = mode[type]
36
+ prefix = types.to_s
37
+
38
+ # Subscriber Sends Subscription Request
39
+
40
+ update_attribute(:"#{prefix}_requested_at", Time.now)
41
+ response = connection(mode: mode).post(hub)
42
+
43
+ # Subscription Response Details
44
+
45
+ update_attribute(:"#{prefix}_request_responded_at", Time.now)
46
+ update_attribute(:"#{prefix}_request_response_code", response.status)
47
+
48
+ (response.status == 202) ? _accepted(response) : _rejected(response)
49
+ end
50
+
51
+ # If the request was accepted, trigger the +receive+ event on the state
52
+ # machine (which will transition to different states depending on whether
53
+ # this is a subscribe, renew, or unsubscribe request).
54
+ def _accepted(response)
55
+ receive!
56
+ end
57
+
58
+ # If the request was not accepted for any reason (rejected by the hub, or
59
+ # the response returned any status other than 202), trigger the +reject+
60
+ # event. This will move the subscription to a `rejected` state regardless
61
+ # of what kind of request was made.
62
+ # TODO Make sure unsub. rejections are being handled correctly.
63
+ def _rejected(response)
64
+ update_attribute(:"#{prefix}_request_response_error", request_response.body)
65
+ reject!
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,166 @@
1
+ module Pushcart
2
+ module Stateable
3
+ extend ActiveSupport::Concern
4
+ include AASM
5
+
6
+ included do
7
+ aasm do
8
+ state :undiscovered, initial: true
9
+ # state :discovered
10
+ #
11
+ # state :requesting
12
+ # state :received
13
+ # state :verified
14
+ #
15
+ # state :rerequesting
16
+ # state :rereceived
17
+ #
18
+ # state :unrequesting
19
+ # state :unreceived
20
+ #
21
+ # state :canceled
22
+ # state :rejected
23
+ # state :denied
24
+ # state :unsubscribed
25
+ # state :lapsed
26
+ #
27
+ # event :cancel do
28
+ # transitions from: [:undiscovered, :discovered, :requesting, :received], to: :canceled
29
+ # end
30
+ #
31
+ # event :discover, if: :discoverable? do
32
+ # transitions from: :undiscovered, to: :discovered
33
+ # end
34
+ #
35
+ # event :request do
36
+ # transitions from: :discovered, to: :requesting
37
+ # transitions from: :verified, to: :rerequesting
38
+ #
39
+ # # If the request state change is successful, actually trigger the
40
+ # # request based on whether it's a renewal or not.
41
+ # success do
42
+ # rerequesting? ? request_renewal : request_subscription
43
+ # end
44
+ # end
45
+ #
46
+ # event :receive do
47
+ # transitions from: :requesting, to: :received
48
+ # transitions from: :rerequesting, to: :rereceived
49
+ # transitions from: :unrequesting, to: :unreceived
50
+ # end
51
+ #
52
+ # event :verify, unless: :terminated?, success: Proc.new {|*args| _verify_success(*args) } do
53
+ # transitions from: [:requesting, :received, :rerequesting, :rereceived], to: :verified
54
+ # transitions from: [:unrequesting, :unreceived], to: :unsubscribed
55
+ # end
56
+ #
57
+ # event :reject do
58
+ # transitions from: [:requesting, :rerequesting], to: :rejected
59
+ # # transitions from: :unrequesting, to: :?? TODO
60
+ # end
61
+ #
62
+ # event :unsubscribe, unless: :terminated? do
63
+ # transitions from: [:verified, :rerequesting, :rereceived], to: :unrequesting
64
+ # end
65
+ #
66
+ # event :deny do
67
+ # # TODO `after` seems like the wrong callback here, but the docs say it
68
+ # # will happen before the transition...
69
+ # transitions from: [:requesting, :received, :verified, :rerequesting, :rereceived, :unrequesting, :unreceived], to: :denied, after: Proc.new {|*args| _deny_after(*args) }
70
+ # end
71
+ #
72
+ # event :lapse do
73
+ # transitions from: [:verified, :rerequesting, :rereceived, :unrequesting, :unreceived], to: :lapsed
74
+ # end
75
+ end
76
+ end
77
+
78
+ # TODO Make sure this callback is happening at the right point in the chain
79
+ def _verify_success(lease)
80
+ if unsubscribed?
81
+ update_attribute(:unsubscription_verified_at, Time.now)
82
+ elsif renewal_requested_at
83
+ # TODO Find a better way to know if this is a renewal
84
+ increment!(:renewals)
85
+ update_attribute(:renewal_verified_at, Time.now)
86
+ else
87
+ update_attribute(:subscription_verified_at, Time.now)
88
+ end
89
+ end
90
+
91
+ # TODO Make sure this callback is happening at the right point in the chain
92
+ def _deny_after(reason)
93
+ # NOTE: Unsubscribe requests should never be denied during validation, so
94
+ # even if a denial request comes in while the subscription is in an
95
+ # `unsubscribing?` state, it is assumed that the denial is unrelated to
96
+ # that request, and the denial will be attached to either the originally
97
+ # subscription, or the most recent renewal.
98
+ if renewing? || renewed?
99
+ update_attribute(:renewal_denied_at, Time.now)
100
+ update_attribute(:renewal_denial_reason, reason)
101
+ else
102
+ update_attribute(:subscription_denied_at, Time.now)
103
+ update_attribute(:subscription_denial_reason, reason)
104
+ end
105
+ end
106
+
107
+ def discoverable?
108
+ # TODO has all the info it needs to make a request
109
+ end
110
+
111
+ # Returns `true` if the subscription has a pending initial subscription
112
+ # request or renewal request open with the hub.
113
+ def subscribing?
114
+ !expired && (renewing? || requesting? || received?)
115
+ end
116
+
117
+ # Returns `true` if the subscription has a pending subscription renewal
118
+ # request open with the hub.
119
+ def renewing?
120
+ !expired? && (rerequesting? || rereceived?)
121
+ end
122
+
123
+ # Return `true` if this subscription has ever been successfully renewed,
124
+ # even if it has since been terminated (by expiring, unsubscribing, etc).
125
+ def renewed?
126
+ renewals > 0
127
+ end
128
+
129
+ # Returns `true` if the subscription has a pending unsubscription
130
+ # request open with the hub.
131
+ def unsubscribing?
132
+ !expired? && (unrequesting? || unreceived?)
133
+ end
134
+
135
+ # Returns `true` if the subscription has been verified, and has not expired
136
+ # or been otherwise invalidated (denied, unsubscribed, etc). When a
137
+ # subscription is `active` it is expecting to get updates from the hub.
138
+ # Once a subscription has been verified, it remains active throughout
139
+ # the renewal and unsubscribe processes unless something invalidates it.
140
+ def active?
141
+ !expired? && (verified? || renewing? || unsubscribing?)
142
+ end
143
+
144
+ # Returns `true` if the subscription (and any subsequent renewals) have
145
+ # exceeded the lease provided by the hub. Because expiration is passive and
146
+ # has results in no callbacks, it is possible that a subscription expired
147
+ # without its state being updated to reflect the change. Any time +expired?+
148
+ # is called, it will ensure the state is in sync with reality.
149
+ # @return [true, false]
150
+ def expired?
151
+ return true if lapsed?
152
+
153
+ if expired_at <= Time.now
154
+ lapse if may_lapse?
155
+
156
+ return true
157
+ end
158
+ end
159
+
160
+ # Returns `true` if the subscription has been canceled for any reason, such
161
+ # as expiring or being ended by the hub.
162
+ def terminated?
163
+ canceled? || rejected? || denied? || unsubscribed? || lapsed? || expired?
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,25 @@
1
+ module Pushcart
2
+ module Subscribable
3
+ extend ActiveSupport::Concern
4
+ include Composable
5
+
6
+ included do
7
+ # Callbacks
8
+ # before_validation(on: :create) do
9
+ # # Create a secret key that the hub will use to generate HMAC digests,
10
+ # # allowing for validation of content delivery payloads.
11
+ # # NOTE: Must be less than 200 bytes in length.
12
+ # self.hmac_secret_key = SecureRandom.hex
13
+ # end
14
+
15
+ # Validations
16
+ validates :hub, presence: true, url: true
17
+ validates :topic, presence: true, url: true
18
+ validates :topic_content_type, presence: true
19
+ validates :hmac_secret_key, presence: true
20
+
21
+ # Scopes
22
+ # scope :expired, -> { where(["expired_at <= ?", Time.now]) }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module Pushcart
2
+ VERSION = "0.0.1.proto1"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :pushcart do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,24 @@
1
+ require "test_helper"
2
+
3
+ class ConnectableTest < ActiveSupport::TestCase
4
+ fixtures :subscriptions
5
+
6
+ def setup
7
+ @subscription = subscriptions(:example)
8
+ end
9
+
10
+ test "callback URI errors when missing config values" do
11
+ ENV["PUSH_CALLBACK_SCHEME"] = nil
12
+ assert_raises(Pushcart::ConfigurationError) { @subscription.callback_uri }
13
+ ENV["PUSH_CALLBACK_SCHEME"] = "http"
14
+ end
15
+
16
+ test "callback URI errors when missing id" do
17
+ @subscription.id = nil
18
+ assert_raises(ArgumentError) { @subscription.callback_uri }
19
+ end
20
+
21
+ test "returns an entire callback URL" do
22
+ assert_equal "http://pubsub.example.com:88/subscriptions/1/callback", @subscription.callback_uri.to_s
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path("../config/application", __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Prevent CSRF attacks by raising an exception.
3
+ # For APIs, you may want to use :null_session instead.
4
+ protect_from_forgery with: :exception
5
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,9 @@
1
+ class Subscription < ActiveRecord::Base
2
+ include Pushcart::Subscribable
3
+ # before_validation(on: :create) do
4
+ # # Create a secret key that the hub will use to generate HMAC digests,
5
+ # # allowing for validation of content delivery payloads.
6
+ # # NOTE: Must be less than 200 bytes in length.
7
+ # self.hmac_secret_key = SecureRandom.hex
8
+ # end
9
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6
+ <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
3
+ require_relative '../config/boot'
4
+ require 'rails/commands'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../config/boot'
3
+ require 'rake'
4
+ Rake.application.run
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pathname'
3
+
4
+ # path to your application root.
5
+ APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6
+
7
+ Dir.chdir APP_ROOT do
8
+ # This script is a starting point to setup your application.
9
+ # Add necessary setup steps to this file:
10
+
11
+ puts "== Installing dependencies =="
12
+ system "gem install bundler --conservative"
13
+ system "bundle check || bundle install"
14
+
15
+ # puts "\n== Copying sample files =="
16
+ # unless File.exist?("config/database.yml")
17
+ # system "cp config/database.yml.sample config/database.yml"
18
+ # end
19
+
20
+ puts "\n== Preparing database =="
21
+ system "bin/rake db:setup"
22
+
23
+ puts "\n== Removing old logs and tempfiles =="
24
+ system "rm -f log/*"
25
+ system "rm -rf tmp/cache"
26
+
27
+ puts "\n== Restarting application server =="
28
+ system "touch tmp/restart.txt"
29
+ end