cable_ready 4.4.3 → 5.0.0.pre1

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