cable_ready 4.5.0 → 5.0.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +260 -161
  3. data/Gemfile.lock +141 -96
  4. data/LATEST +1 -0
  5. data/README.md +13 -13
  6. data/Rakefile +8 -2
  7. data/app/channels/cable_ready/stream.rb +12 -0
  8. data/app/helpers/cable_ready_helper.rb +11 -0
  9. data/app/jobs/cable_ready_broadcast_job.rb +14 -0
  10. data/bin/standardize +1 -1
  11. data/cable_ready.gemspec +3 -2
  12. data/lib/cable_ready.rb +40 -5
  13. data/lib/cable_ready/broadcaster.rb +3 -4
  14. data/lib/cable_ready/cable_car.rb +17 -0
  15. data/lib/cable_ready/channel.rb +12 -32
  16. data/lib/cable_ready/channels.rb +4 -7
  17. data/lib/cable_ready/compoundable.rb +11 -0
  18. data/lib/cable_ready/config.rb +17 -5
  19. data/lib/cable_ready/identifiable.rb +30 -0
  20. data/lib/cable_ready/operation_builder.rb +80 -0
  21. data/lib/cable_ready/sanity_checker.rb +151 -0
  22. data/lib/cable_ready/stream_identifier.rb +13 -0
  23. data/lib/cable_ready/version.rb +1 -1
  24. data/lib/generators/cable_ready/channel_generator.rb +71 -0
  25. data/lib/generators/cable_ready/initializer_generator.rb +14 -0
  26. data/lib/generators/cable_ready/stream_from_generator.rb +43 -0
  27. data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +18 -0
  28. data/package.json +10 -5
  29. data/test/lib/cable_ready/cable_car_test.rb +50 -0
  30. data/test/lib/cable_ready/identifiable_test.rb +75 -0
  31. data/test/lib/cable_ready/operation_builder_test.rb +189 -0
  32. data/test/lib/generators/cable_ready/channel_generator_test.rb +157 -0
  33. data/test/support/generator_test_helpers.rb +28 -0
  34. data/test/test_helper.rb +15 -0
  35. data/yarn.lock +137 -127
  36. metadata +47 -8
  37. data/tags +0 -62
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module StreamIdentifier
5
+ def verified_stream_identifier(signed_stream_identifier)
6
+ CableReady.signed_stream_verifier.verified signed_stream_identifier
7
+ end
8
+
9
+ def signed_stream_identifier(compoundable)
10
+ CableReady.signed_stream_verifier.generate compoundable
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CableReady
4
- VERSION = "4.5.0"
4
+ VERSION = "5.0.0.pre3"
5
5
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CableReady::ChannelGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ class_option :stream_from, type: :string
7
+ class_option :stream_for, type: :string
8
+ class_option :stimulus, type: :boolean
9
+
10
+ def check_options
11
+ raise "Can't specify --stream-from and --stream-for at the same time" if options.key?(:stream_from) && options.key?(:stream_for)
12
+ end
13
+
14
+ def create_channel
15
+ generate "channel", file_name
16
+ end
17
+
18
+ def enhance_channels
19
+ if using_broadcast_to?
20
+ gsub_file "app/channels/#{file_name}_channel.rb", /# stream_from.*\n/, "stream_for #{resource}.find(params[:id])\n"
21
+ template "app/javascript/controllers/%file_name%_controller.js" if using_stimulus?
22
+ else
23
+ prepend_to_file "app/javascript/channels/#{file_name}_channel.js", "import CableReady from 'cable_ready'\n"
24
+ inject_into_file "app/javascript/channels/#{file_name}_channel.js", after: "// Called when there's incoming data on the websocket for this channel\n" do
25
+ <<-JS
26
+ if (data.cableReady) CableReady.perform(data.operations)
27
+ JS
28
+ end
29
+
30
+ gsub_file "app/channels/#{file_name}_channel.rb", /# stream_from.*\n/, "stream_from \"#{identifier}\"\n"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def option_given?
37
+ options.key?(:stream_from) || options.key?(:stream_for)
38
+ end
39
+
40
+ def using_broadcast_to?
41
+ @using_broadcast_to ||= option_given? ? options.key?(:stream_for) : yes?("Are you streaming to a resource using broadcast_to? (y/N)")
42
+ end
43
+
44
+ def using_stimulus?
45
+ @using_stimulus ||= options.fetch(:stimulus) {
46
+ yes?("Are you going to use a Stimulus controller to subscribe to this channel? (y/N)")
47
+ }
48
+ end
49
+
50
+ def resource
51
+ return @resource if @resource
52
+
53
+ stream_for = options.fetch(:stream_for) {
54
+ ask("Which resource are you streaming for?", default: class_name)
55
+ }
56
+
57
+ stream_for = file_name if stream_for == "stream_for"
58
+ @resource = stream_for.camelize
59
+ end
60
+
61
+ def identifier
62
+ return @identifier if @identifier
63
+
64
+ stream_from = options.fetch(:stream_from) {
65
+ ask("What is the stream identifier that goes into stream_from?", default: file_name)
66
+ }
67
+
68
+ stream_from = file_name if stream_from == "stream_from"
69
+ @identifier = stream_from.underscore
70
+ end
71
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module CableReady
6
+ class InitializerGenerator < Rails::Generators::Base
7
+ desc "Creates a CableReady initializer template in config/initializers"
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_initializer_file
11
+ copy_file "config/initializers/cable_ready.rb"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "fileutils"
5
+
6
+ module CableReady
7
+ class StreamFromGenerator < Rails::Generators::Base
8
+ desc "Initializes CableReady with a reference to the shared ActionCable consumer"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ def copy_controller_file
12
+ main_folder = defined?(Webpacker) ? Webpacker.config.source_path.to_s.gsub("#{Rails.root}/", "") : "app/javascript"
13
+
14
+ filepath = [
15
+ "#{main_folder}/controllers/index.js",
16
+ "#{main_folder}/controllers/index.ts",
17
+ "#{main_folder}/packs/application.js",
18
+ "#{main_folder}/packs/application.ts"
19
+ ]
20
+ .select { |path| File.exist?(path) }
21
+ .map { |path| Rails.root.join(path) }
22
+ .first
23
+
24
+ lines = File.open(filepath, "r") { |f| f.readlines }
25
+
26
+ unless lines.find { |line| line.start_with?("import CableReady") }
27
+ matches = lines.select { |line| line =~ /\A(require|import)/ }
28
+ lines.insert lines.index(matches.last).to_i + 1, "import CableReady from 'cable_ready'\n"
29
+ File.open(filepath, "w") { |f| f.write lines.join }
30
+ end
31
+
32
+ unless lines.find { |line| line.start_with?("import consumer") }
33
+ matches = lines.select { |line| line =~ /\A(require|import)/ }
34
+ lines.insert lines.index(matches.last).to_i + 1, "import consumer from '../channels/consumer'\n"
35
+ File.open(filepath, "w") { |f| f.write lines.join }
36
+ end
37
+
38
+ unless lines.find { |line| line.include?("CableReady.initialize({ consumer })") }
39
+ append_to_file filepath, "CableReady.initialize({ consumer })"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ CableReady.configure do |config|
4
+ # Enable/disable exiting / warning when the sanity checks fail options:
5
+ # `:exit` or `:warn` or `:ignore`
6
+
7
+ # config.on_failed_sanity_checks = :exit
8
+
9
+ # Enable/disable exiting / warning when there's a new CableReady release
10
+ # `:exit` or `:warn` or `:ignore`
11
+
12
+ # config.on_new_version_available = :ignore
13
+
14
+ # Define your own custom operations
15
+ # https://cableready.stimulusreflex.com/customization#custom-operations
16
+
17
+ # config.add_operation_name :jazz_hands
18
+ end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cable_ready",
3
- "version": "4.4.6",
3
+ "version": "5.0.0-pre2",
4
4
  "description": "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.",
5
5
  "keywords": [
6
6
  "ruby",
@@ -15,17 +15,22 @@
15
15
  ],
16
16
  "homepage": "https://cableready.stimulusreflex.com/",
17
17
  "bugs": {
18
- "url": "https://github.com/hopsoft/cable_ready/issues"
18
+ "url": "https://github.com/stimulusreflex/cable_ready/issues"
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com:hopsoft/cable_ready.git"
22
+ "url": "git+https://github.com:stimulusreflex/cable_ready.git"
23
23
  },
24
24
  "license": "MIT",
25
25
  "author": "Nathan Hopkins <natehop@gmail.com>",
26
- "main": "./javascript/cable_ready.js",
26
+ "main": "./javascript/index.js",
27
+ "module": "./javascript/index.js",
28
+ "sideEffects": false,
27
29
  "scripts": {
28
- "prettier-standard-check": "yarn run prettier-standard --check ./javascript/**/*.js"
30
+ "lint": "yarn run prettier-standard:check",
31
+ "format": "yarn run prettier-standard:format",
32
+ "prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js",
33
+ "prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js"
29
34
  },
30
35
  "dependencies": {
31
36
  "morphdom": "^2.6.1"
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require_relative "../../../lib/cable_ready"
5
+
6
+ class CableReady::CableCarTest < ActiveSupport::TestCase
7
+ setup do
8
+ @cable_car = CableReady::CableCar.instance
9
+ end
10
+
11
+ test "dispatch should return json-ifiable payload" do
12
+ CableReady::CableCar.instance.reset!
13
+ dispatch = CableReady::CableCar.instance.inner_html(selector: "#users", html: "<span>winning</span>").dispatch
14
+ assert_equal([{"operation" => "innerHtml", "selector" => "#users", "html" => "<span>winning</span>"}], dispatch)
15
+ end
16
+
17
+ test "dispatch should clear operations" do
18
+ CableReady::CableCar.instance.reset!
19
+ CableReady::CableCar.instance.inner_html(selector: "#users", html: "<span>winning</span>").dispatch
20
+ assert_equal([], CableReady::CableCar.instance.instance_variable_get(:@enqueued_operations))
21
+ end
22
+
23
+ test "dispatch should maintain operations if clear is false" do
24
+ CableReady::CableCar.instance.reset!
25
+ CableReady::CableCar.instance.inner_html(selector: "#users", html: "<span>winning</span>").dispatch(clear: false)
26
+ assert_equal([{"operation" => "innerHtml", "selector" => "#users", "html" => "<span>winning</span>"}], CableReady::CableCar.instance.instance_variable_get(:@enqueued_operations))
27
+ end
28
+
29
+ test "selectors should accept any object which respond_to? to_dom_selector" do
30
+ CableReady::CableCar.instance.reset!
31
+ my_object = Struct.new(:id) do
32
+ def to_dom_selector
33
+ ".#{id}"
34
+ end
35
+ end.new("users")
36
+ dispatch = CableReady::CableCar.instance.inner_html(selector: my_object, html: "<span>winning</span>").dispatch
37
+ assert_equal([{"operation" => "innerHtml", "selector" => ".users", "html" => "<span>winning</span>"}], dispatch)
38
+ end
39
+
40
+ test "selectors should accept any object which respond_to? to_dom_id" do
41
+ CableReady::CableCar.instance.reset!
42
+ my_object = Struct.new(:id) do
43
+ def to_dom_id
44
+ id
45
+ end
46
+ end.new("users")
47
+ dispatch = CableReady::CableCar.instance.inner_html(selector: my_object, html: "<span>winning</span>").dispatch
48
+ assert_equal([{"operation" => "innerHtml", "selector" => "#users", "html" => "<span>winning</span>"}], dispatch)
49
+ end
50
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require_relative "../../../lib/cable_ready"
5
+
6
+ class User
7
+ include ActiveModel::Model
8
+
9
+ attr_accessor :id
10
+ end
11
+
12
+ class CableReady::IdentifiableTest < ActiveSupport::TestCase
13
+ include CableReady::Identifiable
14
+
15
+ test "should handle nil" do
16
+ assert_equal "#", dom_id(nil)
17
+ end
18
+
19
+ test "should work with strings" do
20
+ assert_equal "#users", dom_id("users")
21
+ assert_equal "#users", dom_id("users ")
22
+ assert_equal "#users", dom_id(" users ")
23
+ end
24
+
25
+ test "should work with symbols" do
26
+ assert_equal "#users", dom_id(:users)
27
+ assert_equal "#active_users", dom_id(:active_users)
28
+ end
29
+
30
+ test "should just return one hash" do
31
+ assert_equal "#users", dom_id("users")
32
+ assert_equal "#users", dom_id("#users")
33
+ assert_equal "#users", dom_id("##users")
34
+ end
35
+
36
+ test "should strip prefixes" do
37
+ assert_equal "#active_users", dom_id(" users ", " active ")
38
+ assert_equal "#all_active_users", dom_id(" users ", " all_active ")
39
+ end
40
+
41
+ test "should not include provided prefix if prefix is nil" do
42
+ assert_equal "#users", dom_id("users", nil)
43
+ end
44
+
45
+ test "should work with ActiveRecord::Relation" do
46
+ relation = mock("ActiveRecord::Relation")
47
+
48
+ relation.stubs(:is_a?).with(ActiveRecord::Relation).returns(true).at_least_once
49
+ relation.stubs(:is_a?).with(ActiveRecord::Base).never
50
+ relation.stubs(:model_name).returns(OpenStruct.new(plural: "users"))
51
+
52
+ assert_equal "#users", dom_id(relation)
53
+ assert_equal "#users", dom_id(relation, nil)
54
+ assert_equal "#active_users", dom_id(relation, "active")
55
+ end
56
+
57
+ test "should work with ActiveRecord::Base" do
58
+ User.any_instance.stubs(:is_a?).with(ActiveRecord::Relation).returns(false)
59
+ User.any_instance.stubs(:is_a?).with(ActiveRecord::Base).returns(true)
60
+
61
+ assert_equal "#new_user", dom_id(User.new(id: nil))
62
+
63
+ user = User.new(id: 42)
64
+
65
+ assert_equal "#user_42", dom_id(user)
66
+ assert_equal "#user_42", dom_id(user, nil)
67
+ assert_equal "#all_active_user_42", dom_id(user, "all_active")
68
+
69
+ user = User.new(id: 99)
70
+
71
+ assert_equal "#user_99", dom_id(user)
72
+ assert_equal "#user_99", dom_id(user, nil)
73
+ assert_equal "#all_active_user_99", dom_id(user, "all_active")
74
+ end
75
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require_relative "../../../lib/cable_ready"
5
+
6
+ class Death
7
+ def to_html
8
+ "I rock"
9
+ end
10
+
11
+ def to_dom_id
12
+ "death"
13
+ end
14
+
15
+ def to_operation_options
16
+ [:html, :dom_id, :spaz]
17
+ end
18
+ end
19
+
20
+ class Life
21
+ def to_operation_options
22
+ {
23
+ html: "You go, girl",
24
+ dom_id: "life"
25
+ }
26
+ end
27
+ end
28
+
29
+ class CableReady::OperationBuilderTest < ActiveSupport::TestCase
30
+ setup do
31
+ @operation_builder = CableReady::OperationBuilder.new("test")
32
+ end
33
+
34
+ test "should create enqueued operations" do
35
+ assert_not_nil @operation_builder.instance_variable_get(:@enqueued_operations)
36
+ end
37
+
38
+ test "should add observer to cable ready" do
39
+ assert_not_nil CableReady.config.instance_variable_get(:@observer_peers)[@operation_builder]
40
+ end
41
+
42
+ test "should remove observer when destroyed" do
43
+ @operation_builder = nil
44
+ assert_nil CableReady.config.instance_variable_get(:@observer_peers)[@operation_builder]
45
+ end
46
+
47
+ test "should add operation method" do
48
+ @operation_builder.add_operation_method("foobar")
49
+ assert @operation_builder.respond_to?(:foobar)
50
+ end
51
+
52
+ test "added operation method should add keys" do
53
+ @operation_builder.add_operation_method("foobar")
54
+ @operation_builder.foobar({name: "passed_option"})
55
+
56
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
57
+
58
+ assert_equal 1, operations.size
59
+ assert_equal({"name" => "passed_option", "operation" => "foobar"}, operations.first)
60
+ end
61
+
62
+ test "should json-ify operations" do
63
+ @operation_builder.add_operation_method("foobar")
64
+ @operation_builder.foobar({name: "passed_option"})
65
+ assert_equal("[{\"name\":\"passed_option\",\"operation\":\"foobar\"}]", @operation_builder.to_json)
66
+ end
67
+
68
+ test "should apply! many operations" do
69
+ @operation_builder.apply!({name: "passed_option"})
70
+
71
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
72
+ assert_equal 1, operations.size
73
+ assert_equal({"name" => "passed_option"}, operations.first)
74
+ end
75
+
76
+ test "should apply! many operations from a string" do
77
+ @operation_builder.apply!(JSON.generate({name: "passed_option"}))
78
+
79
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
80
+ assert_equal 1, operations.size
81
+ assert_equal({"name" => "passed_option"}, operations.first)
82
+ end
83
+
84
+ test "operations payload should omit empty operations" do
85
+ @operation_builder.add_operation_method("foobar")
86
+ payload = @operation_builder.operations_payload
87
+ assert_equal([], payload)
88
+ end
89
+
90
+ test "operations payload should camelize keys" do
91
+ @operation_builder.add_operation_method("foo_bar")
92
+ @operation_builder.foo_bar({beep_boop: "passed_option"})
93
+ assert_equal([{"operation" => "fooBar", "beepBoop" => "passed_option"}], @operation_builder.operations_payload)
94
+ end
95
+
96
+ test "should take first argument as selector" do
97
+ @operation_builder.add_operation_method("inner_html")
98
+
99
+ @operation_builder.inner_html("#smelly", html: "<span>I rock</span>")
100
+
101
+ operations = [{"operation" => "innerHtml", "html" => "<span>I rock</span>", "selector" => "#smelly"}]
102
+
103
+ assert_equal(operations, @operation_builder.operations_payload)
104
+ end
105
+
106
+ test "should use previously passed selector in next operation" do
107
+ @operation_builder.add_operation_method("inner_html")
108
+ @operation_builder.add_operation_method("set_focus")
109
+
110
+ @operation_builder.set_focus("#smelly").inner_html(html: "<span>I rock</span>")
111
+
112
+ operations = [
113
+ {"operation" => "setFocus", "selector" => "#smelly"},
114
+ {"operation" => "innerHtml", "html" => "<span>I rock</span>", "selector" => "#smelly"}
115
+ ]
116
+
117
+ assert_equal(operations, @operation_builder.operations_payload)
118
+ end
119
+
120
+ test "should clear previous_selector after calling reset!" do
121
+ @operation_builder.add_operation_method("inner_html")
122
+ @operation_builder.inner_html(selector: "#smelly", html: "<span>I rock</span>")
123
+
124
+ @operation_builder.reset!
125
+
126
+ @operation_builder.inner_html(html: "<span>winning</span>")
127
+
128
+ assert_equal([{"operation" => "innerHtml", "html" => "<span>winning</span>"}], @operation_builder.operations_payload)
129
+ end
130
+
131
+ test "should use previous_selector if present and should use `selector` if explicitly provided" do
132
+ @operation_builder.add_operation_method("inner_html")
133
+ @operation_builder.add_operation_method("set_focus")
134
+
135
+ @operation_builder.set_focus("#smelly").inner_html(html: "<span>I rock</span>").inner_html(html: "<span>I rock too</span>", selector: "#smelly2")
136
+
137
+ operations = [
138
+ {"operation" => "setFocus", "selector" => "#smelly"},
139
+ {"operation" => "innerHtml", "html" => "<span>I rock</span>", "selector" => "#smelly"},
140
+ {"operation" => "innerHtml", "html" => "<span>I rock too</span>", "selector" => "#smelly2"}
141
+ ]
142
+
143
+ assert_equal(operations, @operation_builder.operations_payload)
144
+ end
145
+
146
+ test "should pull html option from Death object" do
147
+ @operation_builder.add_operation_method("inner_html")
148
+ death = Death.new
149
+
150
+ @operation_builder.inner_html(html: death)
151
+
152
+ operations = [{"operation" => "innerHtml", "html" => "I rock"}]
153
+
154
+ assert_equal(operations, @operation_builder.operations_payload)
155
+ end
156
+
157
+ test "should pull html option with selector from Death object" do
158
+ @operation_builder.add_operation_method("inner_html")
159
+ death = Death.new
160
+
161
+ @operation_builder.inner_html(death, html: death)
162
+
163
+ operations = [{"operation" => "innerHtml", "html" => "I rock", "selector" => "#death"}]
164
+
165
+ assert_equal(operations, @operation_builder.operations_payload)
166
+ end
167
+
168
+ test "should pull html and dom_id options from Death object" do
169
+ @operation_builder.add_operation_method("inner_html")
170
+ death = Death.new
171
+
172
+ @operation_builder.inner_html(death)
173
+
174
+ operations = [{"operation" => "innerHtml", "html" => "I rock", "domId" => "death"}]
175
+
176
+ assert_equal(operations, @operation_builder.operations_payload)
177
+ end
178
+
179
+ test "should pull html and dom_id options from Life object" do
180
+ @operation_builder.add_operation_method("inner_html")
181
+ life = Life.new
182
+
183
+ @operation_builder.inner_html(life)
184
+
185
+ operations = [{"operation" => "innerHtml", "html" => "You go, girl", "domId" => "life"}]
186
+
187
+ assert_equal(operations, @operation_builder.operations_payload)
188
+ end
189
+ end