cable_ready 4.4.3 → 5.0.0.pre1

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 +238 -155
  3. data/Gemfile.lock +144 -100
  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/lib/cable_ready.rb +41 -0
  12. data/lib/cable_ready/broadcaster.rb +3 -4
  13. data/lib/cable_ready/cable_car.rb +17 -0
  14. data/lib/cable_ready/channel.rb +14 -36
  15. data/lib/cable_ready/channels.rb +22 -65
  16. data/lib/cable_ready/compoundable.rb +11 -0
  17. data/lib/cable_ready/config.rb +78 -0
  18. data/lib/cable_ready/identifiable.rb +19 -0
  19. data/lib/cable_ready/operation_builder.rb +69 -0
  20. data/lib/cable_ready/sanity_checker.rb +151 -0
  21. data/lib/cable_ready/stream_identifier.rb +13 -0
  22. data/lib/cable_ready/version.rb +1 -1
  23. data/lib/generators/cable_ready/channel_generator.rb +71 -0
  24. data/lib/generators/cable_ready/initializer_generator.rb +14 -0
  25. data/lib/generators/cable_ready/stream_from_generator.rb +43 -0
  26. data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +18 -0
  27. data/test/lib/cable_ready/cable_car_test.rb +28 -0
  28. data/test/lib/cable_ready/identifiable_test.rb +75 -0
  29. data/test/lib/cable_ready/operation_builder_test.rb +128 -0
  30. data/test/lib/generators/cable_ready/channel_generator_test.rb +157 -0
  31. data/test/support/generator_test_helpers.rb +28 -0
  32. data/test/test_helper.rb +15 -0
  33. metadata +66 -15
  34. data/cable_ready.gemspec +0 -25
  35. data/package.json +0 -36
  36. data/tags +0 -57
  37. data/yarn.lock +0 -2552
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CableReady::SanityChecker
4
+ LATEST_VERSION_FORMAT = /^(\d+\.\d+\.\d+)$/
5
+ NODE_VERSION_FORMAT = /(\d+\.\d+\.\d+.*):/
6
+ JSON_VERSION_FORMAT = /(\d+\.\d+\.\d+.*)"/
7
+
8
+ class << self
9
+ def check!
10
+ return if CableReady.config.on_failed_sanity_checks == :ignore
11
+ return if called_by_generate_config?
12
+
13
+ instance = new
14
+ instance.check_javascript_package_version
15
+ instance.check_new_version_available
16
+ end
17
+
18
+ private
19
+
20
+ def called_by_generate_config?
21
+ ARGV.include? "cable_ready:initializer"
22
+ end
23
+ end
24
+
25
+ def check_javascript_package_version
26
+ if javascript_package_version.nil?
27
+ warn_and_exit <<~WARN
28
+ Can't locate the cable_ready npm package.
29
+ Either add it to your package.json as a dependency or use "yarn link cable_ready" if you are doing development.
30
+ WARN
31
+ end
32
+
33
+ unless javascript_version_matches?
34
+ warn_and_exit <<~WARN
35
+ The cable_ready npm package version (#{javascript_package_version}) does not match the Rubygem version (#{gem_version}).
36
+ To update the cable_ready npm package:
37
+ yarn upgrade cable_ready@#{gem_version}
38
+ WARN
39
+ end
40
+ end
41
+
42
+ def check_new_version_available
43
+ return unless Rails.env.development?
44
+ return if CableReady.config.on_new_version_available == :ignore
45
+ return unless using_stable_release
46
+ begin
47
+ latest_version = URI.open("https://raw.githubusercontent.com/stimulusreflex/cable_ready/master/LATEST", open_timeout: 1, read_timeout: 1).read.strip
48
+ if latest_version != CableReady::VERSION
49
+ puts <<~WARN
50
+
51
+ There is a new version of CableReady available!
52
+ Current: #{CableReady::VERSION} Latest: #{latest_version}
53
+
54
+ If you upgrade, it is very important that you update BOTH Gemfile and package.json
55
+ Then, run `bundle install && yarn install` to update to #{latest_version}.
56
+
57
+ WARN
58
+ exit if CableReady.config.on_new_version_available == :exit
59
+ end
60
+ rescue
61
+ puts "CableReady #{CableReady::VERSION} update check skipped: connection timeout"
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def javascript_version_matches?
68
+ javascript_package_version == gem_version
69
+ end
70
+
71
+ def using_stable_release
72
+ stable = CableReady::VERSION.match?(LATEST_VERSION_FORMAT)
73
+ puts "CableReady #{CableReady::VERSION} update check skipped: pre-release build" unless stable
74
+ stable
75
+ end
76
+
77
+ def gem_version
78
+ @_gem_version ||= CableReady::VERSION.gsub(".pre", "-pre")
79
+ end
80
+
81
+ def javascript_package_version
82
+ @_js_version ||= find_javascript_package_version
83
+ end
84
+
85
+ def find_javascript_package_version
86
+ if (match = search_file(package_json_path, regex: /version/))
87
+ match[JSON_VERSION_FORMAT, 1]
88
+ elsif (match = search_file(yarn_lock_path, regex: /^cable_ready/))
89
+ match[NODE_VERSION_FORMAT, 1]
90
+ end
91
+ end
92
+
93
+ def search_file(path, regex:)
94
+ return unless File.exist?(path)
95
+ File.foreach(path).grep(regex).first
96
+ end
97
+
98
+ def package_json_path
99
+ Rails.root.join("node_modules", "cable_ready", "package.json")
100
+ end
101
+
102
+ def yarn_lock_path
103
+ Rails.root.join("yarn.lock")
104
+ end
105
+
106
+ def initializer_path
107
+ @_initializer_path ||= Rails.root.join("config", "initializers", "cable_ready.rb")
108
+ end
109
+
110
+ def warn_and_exit(text)
111
+ puts "WARNING:"
112
+ puts text
113
+ exit_with_info if CableReady.config.on_failed_sanity_checks == :exit
114
+ end
115
+
116
+ def exit_with_info
117
+ puts
118
+
119
+ if File.exist?(initializer_path)
120
+ puts <<~INFO
121
+ If you know what you are doing and you want to start the application anyway,
122
+ you can add the following directive to the CableReady initializer,
123
+ which is located at #{initializer_path}
124
+
125
+ CableReady.configure do |config|
126
+ config.on_failed_sanity_checks = :warn
127
+ end
128
+
129
+ INFO
130
+ else
131
+ puts <<~INFO
132
+ If you know what you are doing and you want to start the application anyway,
133
+ you can create a CableReady initializer with the command:
134
+
135
+ bundle exec rails generate cable_ready:config
136
+
137
+ Then open your initializer at
138
+
139
+ #{initializer_path}
140
+
141
+ and then add the following directive:
142
+
143
+ CableReady.configure do |config|
144
+ config.on_failed_sanity_checks = :warn
145
+ end
146
+
147
+ INFO
148
+ end
149
+ exit false unless Rails.env.test?
150
+ end
151
+ end
@@ -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.4.3"
4
+ VERSION = "5.0.0.pre1"
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
@@ -0,0 +1,28 @@
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({"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({"inner_html" => [{"selector" => "#users", "html" => "<span>winning</span>"}]}, CableReady::CableCar.instance.instance_variable_get(:@enqueued_operations))
27
+ end
28
+ 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,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require_relative "../../../lib/cable_ready"
5
+
6
+ class CableReady::OperationBuilderTest < ActiveSupport::TestCase
7
+ setup do
8
+ @operation_builder = CableReady::OperationBuilder.new("test")
9
+ end
10
+
11
+ test "should create enqueued operations" do
12
+ assert_not_nil @operation_builder.instance_variable_get(:@enqueued_operations)
13
+ end
14
+
15
+ test "should add observer to cable ready" do
16
+ assert_not_nil CableReady.config.instance_variable_get(:@observer_peers)[@operation_builder]
17
+ end
18
+
19
+ test "should remove observer when destroyed" do
20
+ @operation_builder = nil
21
+ assert_nil CableReady.config.instance_variable_get(:@observer_peers)[@operation_builder]
22
+ end
23
+
24
+ test "should add operation method" do
25
+ @operation_builder.add_operation_method("foobar")
26
+ assert @operation_builder.respond_to?(:foobar)
27
+ end
28
+
29
+ test "added operation method should add keys" do
30
+ @operation_builder.add_operation_method("foobar")
31
+ @operation_builder.foobar({name: "passed_option"})
32
+
33
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
34
+
35
+ assert_equal 1, operations["foobar"].size
36
+ assert_equal({"name" => "passed_option"}, operations["foobar"].first)
37
+ end
38
+
39
+ test "should json-ify operations" do
40
+ @operation_builder.add_operation_method("foobar")
41
+ @operation_builder.foobar({name: "passed_option"})
42
+ assert_equal("{\"foobar\":[{\"name\":\"passed_option\"}]}", @operation_builder.to_json)
43
+ end
44
+
45
+ test "should apply! many operations" do
46
+ @operation_builder.apply!(foobar: [{name: "passed_option"}])
47
+
48
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
49
+ assert_equal 1, operations["foobar"].size
50
+ assert_equal({"name" => "passed_option"}, operations["foobar"].first)
51
+ end
52
+
53
+ test "should apply! many operations from a string" do
54
+ @operation_builder.apply!(JSON.generate({foobar: [{name: "passed_option"}]}))
55
+
56
+ operations = @operation_builder.instance_variable_get(:@enqueued_operations)
57
+ assert_equal 1, operations["foobar"].size
58
+ assert_equal({"name" => "passed_option"}, operations["foobar"].first)
59
+ end
60
+
61
+ test "operations payload should omit empty operations" do
62
+ @operation_builder.add_operation_method("foobar")
63
+ payload = @operation_builder.operations_payload
64
+ assert_equal({}, payload)
65
+ end
66
+
67
+ test "operations payload should camelize keys" do
68
+ @operation_builder.add_operation_method("foo_bar")
69
+ @operation_builder.foo_bar({beep_boop: "passed_option"})
70
+ assert_equal({"fooBar" => [{"beepBoop" => "passed_option"}]}, @operation_builder.operations_payload)
71
+ end
72
+
73
+ test "should take first argument as selector" do
74
+ @operation_builder.add_operation_method("inner_html")
75
+
76
+ @operation_builder.inner_html("#smelly", html: "<span>I rock</span>")
77
+
78
+ operations = {
79
+ "innerHtml" => [{"html" => "<span>I rock</span>", "selector" => "#smelly"}]
80
+ }
81
+
82
+ assert_equal(operations, @operation_builder.operations_payload)
83
+ end
84
+
85
+ test "should use previously passed selector in next operation" do
86
+ @operation_builder.add_operation_method("inner_html")
87
+ @operation_builder.add_operation_method("set_focus")
88
+
89
+ @operation_builder.set_focus("#smelly").inner_html(html: "<span>I rock</span>")
90
+
91
+ operations = {
92
+ "setFocus" => [{"selector" => "#smelly"}],
93
+ "innerHtml" => [{"html" => "<span>I rock</span>", "selector" => "#smelly"}]
94
+ }
95
+
96
+ assert_equal(operations, @operation_builder.operations_payload)
97
+ end
98
+
99
+ test "should clear previous_selector after calling reset!" do
100
+ @operation_builder.add_operation_method("inner_html")
101
+ @operation_builder.inner_html(selector: "#smelly", html: "<span>I rock</span>")
102
+
103
+ @operation_builder.reset!
104
+
105
+ @operation_builder.inner_html(html: "<span>winning</span>")
106
+
107
+ assert_equal({"innerHtml" => [{"html" => "<span>winning</span>"}]}, @operation_builder.operations_payload)
108
+ end
109
+
110
+ test "should use previous_selector if present and should use `selector` if explicitly provided" do
111
+ @operation_builder.add_operation_method("inner_html")
112
+ @operation_builder.add_operation_method("set_focus")
113
+
114
+ @operation_builder.set_focus("#smelly").inner_html(html: "<span>I rock</span>").inner_html(html: "<span>I rock too</span>", selector: "#smelly2")
115
+
116
+ operations = {
117
+ "setFocus" => [
118
+ {"selector" => "#smelly"}
119
+ ],
120
+ "innerHtml" => [
121
+ {"html" => "<span>I rock</span>", "selector" => "#smelly"},
122
+ {"html" => "<span>I rock too</span>", "selector" => "#smelly2"}
123
+ ]
124
+ }
125
+
126
+ assert_equal(operations, @operation_builder.operations_payload)
127
+ end
128
+ end