cable_ready 4.5.0 → 5.0.0.pre3

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 (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