Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions spec/cable/connection_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ describe Cable::Connection do
end
end

it "accepts a case-insensitive command and keys (issue #108)" do
connect do |connection, socket|
connection.receive({"Command" => "Subscribe", "Identifier" => {channel: "ChatChannel", room: "1"}.to_json}.to_json)
sleep 100.milliseconds

socket.messages.should contain({"type" => "confirm_subscription", "identifier" => {channel: "ChatChannel", room: "1"}.to_json}.to_json)

connection.close
socket.close
end
end

it "accepts without params hash key" do
connect do |connection, socket|
connection.receive({"command" => "subscribe", "identifier" => {channel: "AppearanceChannel"}.to_json}.to_json)
Expand Down
14 changes: 14 additions & 0 deletions spec/cable/payload_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ describe Cable::Payload do
payload.action.should eq("invite")
end

it "parses case-insensitive top-level keys (issue #108)" do
payload_json = {
"Command" => "message",
"Identifier" => {channel: "ChatChannel"}.to_json,
"DATA" => {invite_id: 3, action: "invite"}.to_json,
}.to_json

payload = Cable::Payload.from_json(payload_json)
payload.command.should eq("message")
payload.channel.should eq("ChatChannel")
payload.data.should eq({"invite_id" => 3})
payload.action.should eq("invite")
end

it "raises a SerializableError when the identifier is not a string" do
payload_json = {
command: "subscribe",
Expand Down
10 changes: 7 additions & 3 deletions src/cable/connection.cr
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,13 @@ module Cable
return unless message.presence
payload = Cable::Payload.from_json(message)

return subscribe(payload) if payload.command == "subscribe"
return unsubscribe(payload) if payload.command == "unsubscribe"
return message(payload) if payload.command == "message"
# Compare case-insensitively so a command like "Subscribe" still
# dispatches correctly (see issue #108).
case payload.command.downcase
when "subscribe" then subscribe(payload)
when "unsubscribe" then unsubscribe(payload)
when "message" then message(payload)
end
end

def subscribe(payload : Cable::Payload)
Expand Down
25 changes: 25 additions & 0 deletions src/cable/payload.cr
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,31 @@ module Cable
@[JSON::Field(ignore: true)]
getter action : String = ""

# Crystal's JSON parsing is case-sensitive, but cable clients in the wild
# aren't always consistent about the casing of the top-level protocol keys.
# Normalize them to lowercase before deserializing so a payload such as
# `{"Command": "subscribe", ...}` is accepted instead of raising
# "Missing JSON attribute: command".
#
# Only the top-level keys (command/identifier/data) are touched; the nested
# values — channel names and the identifier/data blobs — are left exactly as
# sent, since those remain case-sensitive. See issue #108.
def self.from_json(string : String) : self
if object = JSON.parse(string).as_h?
string = String.build do |io|
JSON.build(io) do |json|
json.object do
object.each do |key, value|
json.field(key.downcase) { value.to_json(json) }
end
end
end
end
end

new(JSON::PullParser.new(string))
end

# After the Payload is deserialized, parse the data.
# This will ensure we know if it's an action.
def after_initialize
Expand Down
Loading