-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[event-hubs] add AmqpAnnotatedMessage support #15939
Changes from 2 commits
4890de0
fbb34c4
8335ec6
2a8881c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,19 @@ import { message } from "rhea-promise"; | |
import isBuffer from "is-buffer"; | ||
import { Buffer } from "buffer"; | ||
import { logErrorStackTrace, logger } from "./log"; | ||
import { isObjectWithProperties } from "./util/typeGuards"; | ||
|
||
/** | ||
* The allowed AMQP message body types. | ||
* @internal | ||
*/ | ||
export type BodyTypes = "data" | "value" | "sequence"; | ||
|
||
/** @internal */ | ||
export const dataSectionTypeCode = 0x75 as const; | ||
/** @internal */ | ||
export const sequenceSectionTypeCode = 0x76 as const; | ||
/** @internal */ | ||
export const valueSectionTypeCode = 0x77 as const; | ||
|
||
/** | ||
* The default data transformer that will be used by the Azure SDK. | ||
|
@@ -17,21 +29,25 @@ export const defaultDataTransformer = { | |
* and returns an encoded body (some form of AMQP type). | ||
* | ||
* @param body - The AMQP message body | ||
* @returns The encoded AMQP message body as an AMQP Data type | ||
* (data section in rhea terms). Section object with following properties: | ||
* - typecode: 117 (0x75) | ||
* - content: The given AMQP message body as a Buffer. | ||
* - multiple: true | undefined. | ||
* @param bodyType - The AMQP section to story the body in. | ||
* @returns The encoded AMQP message body as an AMQP Data/Sequence/Value section. | ||
*/ | ||
encode(body: unknown): any { | ||
encode(body: unknown, bodyType: BodyTypes): any { | ||
let result: any; | ||
if (isBuffer(body)) { | ||
// string, undefined, null, boolean, array, object, number should end up here | ||
// coercing undefined to null as that will ensure that null value will be given to the | ||
// customer on receive. | ||
if (body === undefined) body = null; | ||
if (bodyType === "value") { | ||
// TODO: Expose value_section from `rhea` similar to the data_section and sequence_section. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is actually my TODO but we should probably file an issue to push this change into rhea. |
||
// Right now there isn't a way to create a value section officially. | ||
result = message.data_section(body); | ||
result.typecode = valueSectionTypeCode; | ||
} else if (bodyType === "sequence") { | ||
result = message.sequence_section(body); | ||
} else if (isBuffer(body)) { | ||
result = message.data_section(body); | ||
} else { | ||
// string, undefined, null, boolean, array, object, number should end up here | ||
// coercing undefined to null as that will ensure that null value will be given to the | ||
// customer on receive. | ||
if (body === undefined) body = null; | ||
try { | ||
const bodyStr = JSON.stringify(body); | ||
result = message.data_section(Buffer.from(bodyStr, "utf8")); | ||
|
@@ -49,38 +65,91 @@ export const defaultDataTransformer = { | |
}, | ||
|
||
/** | ||
* A function that takes the body property from an AMQP message | ||
* (an AMQP Data type (data section in rhea terms)) and returns the decoded message body. | ||
* If it cannot decode the body then it returns the body | ||
* as-is. | ||
* @param body - The AMQP message body | ||
* @returns decoded body or the given body as-is. | ||
* A function that takes the body property from an AMQP message, which can come from either | ||
* the 'data', 'value' or 'sequence' sections of an AMQP message. | ||
* | ||
* If the body is not a JSON string the the raw contents will be returned, along with the bodyType | ||
* indicating which part of the AMQP message the body was decoded from. | ||
* | ||
* @param body - The AMQP message body as received from rhea. | ||
* @returns The decoded/raw body and the body type. | ||
*/ | ||
decode(body: unknown): any { | ||
let processedBody: any = body; | ||
decode(body: unknown | RheaAmqpSection): { body: unknown; bodyType: BodyTypes } { | ||
try { | ||
if (isObjectWithProperties(body, ["content"]) && isBuffer(body.content)) { | ||
// This indicates that we are getting the AMQP described type. Let us try decoding it. | ||
processedBody = body.content; | ||
} | ||
try { | ||
// Trying to stringify and JSON.parse() anything else will fail flat and we shall return | ||
// the original type back | ||
const bodyStr: string = processedBody.toString("utf8"); | ||
processedBody = JSON.parse(bodyStr); | ||
} catch (err) { | ||
logger.verbose( | ||
"[decode] An error occurred while trying JSON.parse() on the received body. " + | ||
"The error is %O", | ||
err | ||
); | ||
if (isRheaAmqpSection(body)) { | ||
switch (body.typecode) { | ||
case dataSectionTypeCode: | ||
return { body: tryToJsonDecode(body.content), bodyType: "data" }; | ||
case sequenceSectionTypeCode: | ||
return { body: body.content, bodyType: "sequence" }; | ||
case valueSectionTypeCode: | ||
return { body: body.content, bodyType: "value" }; | ||
} | ||
} else { | ||
// TODO: test case | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a TODO because it's not done yet or is there an interesting challenge here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haha, I meant to erase that but the problem with adding TODOs is you have to actually go back and grep them... |
||
if (isBuffer(body)) { | ||
return { body: tryToJsonDecode(body), bodyType: "data" }; | ||
} | ||
|
||
return { body, bodyType: "value" }; | ||
} | ||
} catch (err) { | ||
logger.verbose( | ||
"[decode] An error occurred while decoding the received message body. The error is: %O", | ||
err | ||
); | ||
throw err; | ||
} | ||
return processedBody; | ||
} | ||
}; | ||
|
||
/** | ||
* Attempts to decode 'body' as a JSON string. If it fails it returns body | ||
* verbatim. | ||
* | ||
* @param body - An AMQP message body. | ||
* @returns A JSON decoded object, or body if body was not a JSON string. | ||
* | ||
* @internal | ||
*/ | ||
function tryToJsonDecode(body: unknown): unknown { | ||
let processedBody: any = body; | ||
try { | ||
// Trying to stringify and JSON.parse() anything else will fail flat and we shall return | ||
// the original type back | ||
const bodyStr: string = processedBody.toString("utf8"); | ||
processedBody = JSON.parse(bodyStr); | ||
} catch (err) { | ||
logger.verbose( | ||
"[decode] An error occurred while trying JSON.parse() on the received body. The error is %O", | ||
err | ||
); | ||
} | ||
return processedBody; | ||
} | ||
|
||
/** | ||
* Mirror of the internal Section interface in rhea. | ||
* | ||
* @internal | ||
*/ | ||
export interface RheaAmqpSection { | ||
typecode: | ||
| typeof dataSectionTypeCode | ||
| typeof sequenceSectionTypeCode | ||
| typeof valueSectionTypeCode; | ||
content: any; | ||
} | ||
|
||
/** @internal */ | ||
export function isRheaAmqpSection( | ||
possibleSection: any | RheaAmqpSection | ||
): possibleSection is RheaAmqpSection { | ||
return ( | ||
possibleSection != null && | ||
typeof possibleSection.typecode === "number" && | ||
(possibleSection.typecode === dataSectionTypeCode || | ||
possibleSection.typecode === valueSectionTypeCode || | ||
possibleSection.typecode === sequenceSectionTypeCode) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd throw in a blank line here - it's really easy to think the 'if' and the 'underlying else's' all are part of the same chain.
With that said - the 'body being null' thing seems incorrect to me. I know this came from SB, so it's potentially wrong there as well.
For instance, if you pass
null
you get this from JSON.stringify:I haven't looked, but perhaps this all plays nicely with the rhea encoding method for
message.<data|value|sequence>_section()
methods?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only time
null
would have some transformation done on it is for thedata_section
, which case it will be JSON stringified. I'd agree that this would cause a problem, except we also attempt to JSON parse any data_section bodies. This would result in the body beingnull
, not"null"
.That said, we might run into a problem is when talking to the other languages. There's no guarantee other languages are also calling JSON.parse (in fact I doubt they are), so they may see 'null' as a string rather than a null type. It looks like this is already existing behavior, but since
null
and'null'
both result innull
being returned from JSON.parse, updating this is safe.