Skip to content
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

Simplify types, inheritance structure, and API #156

Merged
merged 15 commits into from
Feb 6, 2024
Merged

Conversation

diehuxx
Copy link

@diehuxx diehuxx commented Feb 3, 2024

All Changes

  • Simplify inheritance structure of Message and its subclasses and Resource and its subclasses.
  • Simplify types in protocol and http-server packages. Remove generics wherever possible. Replace generics with intersection types which are easier to read. Remove all types which could be considered "typescript neckbeardery"
  • Remove Message.factory and Resource.factory. Introduce standalone functions parseMessage and parseResource, reducing coupling and circular dependency.
  • Improve test coverage tests for Message and Rfq. In follow up PRs, I will further improve test coverage in the all three packages. I'm not testing in this PR because it's already a big diff.
  • Slight refactor to the API of TbdexHttpServer for adding callback functions. http-server has no tests currently, so tests for this new API will come in a follow up PR.

Changes to public API surface

tbdex/protocol

I updated the README to reflect the new uses.

All uses of MessageKindClass and ResourceKindClass have been replaced with Message and Resource.

Message.parse and Resource.parse have been replaced with parseMessage() and parseResource().

Each message-kind subclass and resource-kind subclass (e.g. Rfq, Offering, etc.) now have their own dedicated .parse() method which checks that the input is the expected message kind. For example, Rfq.parse(inputJson) will return an instance of the Rfq class if and only if inputJson is a valid, signed RFQ.

Message.verify(), Resource.verify(), Message.validate(), and Resource.validate() are no longer static methods. They have been replaced with instance methods

tbdex/http-server

Updated to reflect type changes from tbdex/protocol, most notably MessageKindClass -> Message.

Alter API for adding callbacks methods to Tbdex server. Removes methods get and submit, replacing them with

  • onSubmitRfq(callback: SubmitRfqCallback): void
  • onSubmitOrder(callback: SubmitOrderCallback): void
  • onSubmitClose(callback: SubmitCloseCallback): void
  • onGetExchanges(callback: GetExchangesCallback): void
  • onGetOfferings(callback: GetOfferingsCallback): void

Copy link

changeset-bot bot commented Feb 3, 2024

🦋 Changeset detected

Latest commit: 099c88f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@tbdex/http-client Minor
@tbdex/http-server Minor
@tbdex/protocol Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

github-actions bot commented Feb 3, 2024

TBDocs Report

🛑 Errors: 0
⚠️ Warnings: 12

@tbdex/protocol

  • Project entry file: packages/protocol/src/main.ts

@tbdex/http-client

  • Project entry file: packages/http-client/src/main.ts
📄 File: packages/http-client/src/errors/request-error.ts
⚠️ extractor:ae-missing-release-tag: "RequestErrorParams" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) #L1
⚠️ extractor:ae-undocumented: Missing documentation for "RequestErrorParams". #L1
⚠️ extractor:ae-undocumented: Missing documentation for "recipientDid". #L13
⚠️ extractor:ae-undocumented: Missing documentation for "url". #L14
📄 File: packages/http-client/src/errors/request-token-error.ts
⚠️ extractor:ae-missing-release-tag: "RequestTokenErrorParams" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) #L3
⚠️ extractor:ae-undocumented: Missing documentation for "RequestTokenErrorParams". #L3
📄 File: packages/http-client/src/errors/response-error.ts
⚠️ extractor:ae-missing-release-tag: "ResponseErrorParams" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) #L3
⚠️ extractor:ae-undocumented: Missing documentation for "ResponseErrorParams". #L3
⚠️ extractor:ae-undocumented: Missing documentation for "statusCode". #L15
⚠️ extractor:ae-undocumented: Missing documentation for "details". #L16
⚠️ extractor:ae-undocumented: Missing documentation for "recipientDid". #L17
⚠️ extractor:ae-undocumented: Missing documentation for "url". #L18

@tbdex/http-server

  • Project entry file: packages/http-server/src/main.ts

TBDocs Report Updated at 2024-02-06T23:33:14Z 099c88f

@mistermoe
Copy link
Member

diving into this now

@mistermoe
Copy link
Member

regarding changes made to @tbdex/protocol

Definitely feels like an overall improvement! shaved the neckbeard at the cost of what looks like repetition on initial glance but i think could be argued isn't actually. Either way, the cognitive overhead required to add a new message kind or make a change is significantly reduced which is awesome! Just compared adding a new message kind on main and then on your branch and it's significantly easier here.

Also, it doesn't seem like we sacrificed any type specificity at the public api layer. VSCode is still correctly guiding me through creating all types of messages. Biggest wins imo are:

  • ditching MessageKind which was super annoying because it was difficult to decide whether to Message or MessageKind.
  • ditching the hack i added to prevent circular dependencies

regarding changes made to @tbdex/http-server:

Still forming an opinion here because the api surface did notably change.

Other comments:

  • Can you include a writeup of all the breaking changes at the public api level with examples so that we can share it with devrel? i think they have guides that are using what's currently on main and we'll need a way to convey what's changed and how.

@diehuxx
Copy link
Author

diehuxx commented Feb 5, 2024

Definitely feels like an overall improvement! shaved the neckbeard at the cost of what looks like repetition on initial glance but i think could be argued isn't actually.

Glad you think so! I made a deliberate choice to allow small amounts of repetition in order to make the API surface and the implementation simpler.

Can you include a writeup of all the breaking changes at the public api level with examples so that we can share it with devrel? i think they have guides that are using what's currently on main and we'll need a way to convey what's changed and how.

Absolutely. I'll add details to the PR description tomorrow.

Copy link

codecov bot commented Feb 5, 2024

Codecov Report

Merging #156 (654c31f) into main (85e5841) will increase coverage by 6.82%.
The diff coverage is 91.27%.

❗ Current head 654c31f differs from pull request most recent head 099c88f. Consider uploading reports for the commit 099c88f to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #156      +/-   ##
==========================================
+ Coverage   78.69%   85.52%   +6.82%     
==========================================
  Files          19       35      +16     
  Lines        1169     2798    +1629     
  Branches       99      234     +135     
==========================================
+ Hits          920     2393    +1473     
- Misses        249      405     +156     
Components Coverage Δ
protocol 90.01% <95.49%> (∅)
http-client 86.78% <78.04%> (-0.05%) ⬇️
http-server 71.30% <70.96%> (+1.17%) ⬆️

data.push(resource)
const responseBody = await response.json()
const jsonOfferings = responseBody.data as any[]
for (let jsonResource of jsonOfferings) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (let jsonResource of jsonOfferings) {
for (let jsonOffering of jsonOfferings) {

Copy link
Contributor

@jiyoonie9 jiyoonie9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

soo awesome!! i love the changes you've made to protocol (which in turn also affects http-client positively in simplifying types and tightening type constraints wherever possible). i also very much appreciate the small cleanups throughout this PR.

i don't immediately love the api changes to http-server - i wish we could preserve some of our generic magic in submit<T> and get<T> so we don't have to write / update concrete impls if tbdex messages change in the future. but i think changes i have to make as a consumer won't be too bad.

/** the message you want to send */
message: Message<T> | MessageModel<T>
message: T
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is pretty great!!

packages/http-client/src/client.ts Show resolved Hide resolved
packages/http-server/src/http-server.ts Show resolved Hide resolved
@@ -82,7 +83,7 @@
"build:esm": "rimraf dist/esm dist/types && tsc",
"build:cjs": "rimraf dist/cjs && tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > ./dist/cjs/package.json",
"build:browser": "rimraf dist/browser.mjs dist/browser.js && node build/bundles.js",
"test:node": "rimraf tests/compiled && pnpm compile-validators && tsc -p tests/tsconfig.json && mocha",
"test:node": "rimraf tests/compiled && pnpm compile-validators && tsc -p tests/tsconfig.json && c8 mocha",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is c8 for?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c8 produces a code coverage report when you run the tests

id : id,
exchangeId : id,
createdAt : new Date().toISOString()
}

// TODO: hash `data.payinMethod.paymentDetails` and set `private`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we keep these // TODO if they aren't completed in this pr and are still to be done?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like in other areas of this PR that we're doing away with the concept of private fields? not 100% sure tho

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I should have provided more context there. In this proposal for how to handle private data, we're doing away with the notion of private as a top-level field in RFQs. This PR does not implement the proposal, but it does clean up private in preparation for implementing the proposal. I'll put the TODO back in.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

packages/protocol/src/message-kinds/rfq.ts Show resolved Hide resolved
let message: Message
switch(jsonMessage.metadata.kind) {
case 'rfq':
message = new Rfq(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
message = new Rfq(
message = await Rfq.parse(rawMessage)

just wondering if we can use parse() method you already wrote for each of the concrete message impls?

Copy link
Author

@diehuxx diehuxx Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer not to reuse Rfq.parse etc. Rfq.parse() does some of the same things that are already done in parseMessage, so we would be repeating operations (.verify(), and rawToMessageModel). While we could remove those operations from parseMessage, I think it would be less clear what parseMessage is doing. Also, i prefer to think of Rfq.parse() and parseMessage as part of the public API for this library, and that we use the constructors and instance methods when appropriate internally.

})
})

// describe('validate', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove these commented out bits if not relevant anymore?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upd8d

* @param message - the message to parse. can either be an object or a string
* @returns {@link Message}
*/
export async function parseMessage(rawMessage: MessageModel | string): Promise<Message> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on being able to call this like MessageUtils.parseMessage() as a static method? a good amount of our other API surfaces tend to follow this pattern i.e. Rfq.parse() or Crypto.verify()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me. Instead of MessageUtils, I'm calling it Parser which matches the grammatical pattern of our Validator and DidResolver. MessageUtils seems a little too broad, plus this file contains utils for parsing resources in addition to messages.

@jiyoonie9
Copy link
Contributor

oh, could you add a changeset? steps here

@diehuxx
Copy link
Author

diehuxx commented Feb 6, 2024

i don't immediately love the api changes to http-server - i wish we could preserve some of our generic magic in submit and get

Fair enough. Here's my defense.

First, a petite polemic: Generics are a fatal temptation. The DRYness that generics promise only breeds a thirst for even more generics. One drop may be invigorating, but we must not become addicted, lest we resent their absence.

In this PR I adopt the principle that anything that can be accomplished clearly and readably without generics, should be done without generics. The existing get and submit functions in http-server have largely the same appearance as the new onSubmitRfq(), onGetOfferings, etc.

For example, server.submit('rfq', rfqCallback) looks pretty similar to server.onSubmitRfq(rfqCallback). Both versions require the consumer to realize that they are specifying a callback for the submit RFQ endpoint. But the latter has a simpler implementation by avoiding the use of generics. As a bonus, I'd argue that it's easier to document/understand that calling server.onSubmitRfq(rfqCallback) overwrites (rather than adds to) the original callback.

so we don't have to write / update concrete impls if tbdex messages change

I'd argue this is an anti-pattern in the context of tbdex/http-server. There's a limited number of endpoints we're trying to support. If we add new tbdex messages and endpoints, we should have to add new methods to TbdexHttpServer and add unit tests accordingly. Being able to add new endpoints without needing to update TbdexHttpServer seems like a recipe for untested codepaths.

@mistermoe
Copy link
Member

fair @diehuxx i'll take maintainability and clarity over hipster api surface vibes. fwiw server.submit('rfq', rfqCallback) always felt kinda odd to me when i implemented it because i always read it as "i the server am submitting an rfq and then calling rfqCallback". whereas server.onSubmitRfq reads far more like "i the server, on receiving an rfq will call rfqCallback

looks like all that's left is addressing the tbdocs warnings i think?

@mistermoe
Copy link
Member

cc: @angiejones @chris-tbd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants