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

Delete BaseHTTPService and implement new BaseXmlService (affects [eclipse-marketplace f-droid]; also testing on [uptimerobot circleci]) #2037

Merged
merged 15 commits into from
Sep 3, 2018

Conversation

PyvesB
Copy link
Member

@PyvesB PyvesB commented Sep 1, 2018

There's quite a few things going on here:

  • as a follow up to new badge: f-droid  #1965 (comment), BaseHTTPService was removed by pushing up the _request function to the BaseService class. Tests were added for this method.
  • following a similar reasoning, _validate was also pushed up to BaseService, as this allowed to easily implement BaseXmlService alongside the existing BaseJsonService class with very little code duplication.
  • asJson was pulled out of error-helpers.js and now sits in response-parsers.js with a new friend, asXml. These functions are now properly tested.
  • the eclipse-markteplace service was broken down and migrated to use the new BaseXmlService class.
  • calls were migrated to async/await were relevant, following Consistent use of async/await #2028 (comment).

I'll launch a Heroku review app so we can play around with the migrated Eclipse badges and make sure we haven't missed anything! 👍

Eclipse Markteplace badges were migrated as an example
@PyvesB PyvesB added service-badge New or updated service badge developer-experience Dev tooling, test framework, and CI core Server, BaseService, GitHub auth, Shared helpers labels Sep 1, 2018
@shields-ci
Copy link

shields-ci commented Sep 1, 2018

Warnings
⚠️

This PR modified helper functions in lib/ but not accompanying tests.
That's okay so long as it's refactoring existing code.

⚠️

This PR modified service code for f-droid but not its test code.
That's okay so long as it's refactoring existing code.

Messages
📖

✨ Thanks for your contribution to Shields, @PyvesB!

📖

Thanks for contributing to our documentation. We ❤️ our documentarians!

Generated by 🚫 dangerJS


async function asXml({ buffer, res }) {
let xml
xml2js.parseString(buffer, (err, parsedData) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

This uses a callback and works fine because the underlying implementation is not async (see Leonidas-from-XIV/node-xml2js#159 (comment)), but there is probably a better way of doing this. Any ideas/guidance?

Copy link
Member

Choose a reason for hiding this comment

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

Aw, that is a sadly unpleasant thread. ☹️

Two thoughts:

  1. For the new services, we could adopt the parser mentioned here, which is synchronous: Sync version of parseString Leonidas-from-XIV/node-xml2js#159 (comment) They say it's faster.
  2. You could wrap the callback function in a promise using util.promisify:
const { promisify } = require('util')
const parseXml = promisify(require('xml2js').parseString)

let xml
try {
  // `parseXml` is returning an awaitable promise so if you were assigning its
  // return value to a variable, you'd need `await`. However on a return from
  // an async function, there's no need to specify `await`.
  return parseXml(buffer)
} catch (err) {
  throw 
}

Copy link
Member Author

@PyvesB PyvesB Sep 1, 2018

Choose a reason for hiding this comment

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

I tied solution 1, switching to fast-xml-parser.
Good points:

  • cleaner code in response-parser.js.
  • xml2js has a tendency of introducing arrays everywhere even when not necessary. Switching has simplified the Joi schemas and the constructed JS objects.
  • claimed to be faster.

Bad point:

  • no helpful exception message if the XML is not parsable.

I ran npm i fast-xml-parser to update package-lock.json, is this the correct approach? A huge diff is generated on the file.

@PyvesB
Copy link
Member Author

PyvesB commented Sep 1, 2018

Badges generated for my plugin Notepad4e, using the Heroku review app:

Eclipse Marketplace
Eclipse Marketplace
Eclipse Marketplace
Eclipse Marketplace
Eclipse Marketplace

@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 11:21 Inactive
@PyvesB PyvesB changed the title Delete BaseHTTPService and implement new BaseXmlService Delete BaseHTTPService and implement new BaseXmlService (affects [eclipse-marketplace f-droid]) Sep 1, 2018
Copy link
Member

@paulmelnikow paulmelnikow left a comment

Choose a reason for hiding this comment

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

This looks great. Nice work! I like how you moved _validate to the base class, too.

I added [uptimerobot circleci] to the PR title – which are just a couple examples of services that use BaseJsonService, which was also affected by this change.

'use strict'

const { InvalidResponse } = require('../services/errors')
const xml2js = require('xml2js')
Copy link
Member

Choose a reason for hiding this comment

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

We're only using this code in one place. Would it be more direct to move it there, inline?

Copy link
Member Author

Choose a reason for hiding this comment

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

With fast-xml-parser, the parser is now used in two places, so keeping the require up there. 😄

Copy link
Member

Choose a reason for hiding this comment

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

Ah, sorry, I meant the functions themselves, not the requires. Instead of having these in a separate module, could they just be inlined into the _requestX methods in the base services?


async function asXml({ buffer, res }) {
let xml
xml2js.parseString(buffer, (err, parsedData) => {
Copy link
Member

Choose a reason for hiding this comment

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

Aw, that is a sadly unpleasant thread. ☹️

Two thoughts:

  1. For the new services, we could adopt the parser mentioned here, which is synchronous: Sync version of parseString Leonidas-from-XIV/node-xml2js#159 (comment) They say it's faster.
  2. You could wrap the callback function in a promise using util.promisify:
const { promisify } = require('util')
const parseXml = promisify(require('xml2js').parseString)

let xml
try {
  // `parseXml` is returning an awaitable promise so if you were assigning its
  // return value to a variable, you'd need `await`. However on a return from
  // an async function, there's no need to specify `await`.
  return parseXml(buffer)
} catch (err) {
  throw 
}

expect(e.prettyMessage).to.equal('unparseable xml response')
}
})
})
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for adding these tests! Well worth keeping and adjusting if this functionality is moved inline, which would widen the bracket a bit.

We aren't using nock directly anywhere right now, though it's probably a good idea to start. I've written service tests for things that ought to be unit tests, and smaller-bracket unit tests for things, e.g. the one in #2036, which probably should mock an HTTP response.

logTrace(emojic.dart, 'Response JSON (before validation)', json, {
deep: true,
})
return this.constructor._validate(json, schema)
Copy link
Member

Choose a reason for hiding this comment

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

Wow, this is much clearer!

expect(serviceData).to.deep.equal({
message: 'some-string',
})
})
Copy link
Member

Choose a reason for hiding this comment

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

Should we add a third test, of valid XML which fails the schema, to establish that _validate is wired up correctly?


async handle({ name }) {
const { marketplace } = await this.fetch({ name, schema })
const downloads = base.endsWith('dt')
Copy link
Member

Choose a reason for hiding this comment

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

It might be slightly clearer to condition on interval here.

@@ -0,0 +1,38 @@
'use strict'
Copy link
Member

Choose a reason for hiding this comment

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

Were there any changes in the tests, or were they just moved?

Copy link
Member Author

Choose a reason for hiding this comment

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

The five existing tests we copy-pasted as is in the different tester classes. Additionally, I added one "not found" test case for each implementation.

)
return value
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I love the idea of moving _validate here. Nice work!

logTrace(emojic.bowAndArrow, 'Request', url, '\n', options)
const { res, buffer } = await this._sendAndCacheRequest(url, options)
logTrace(emojic.dart, 'Response status code', res.statusCode)
return checkErrorResponse.asPromise(errorMessages)({ buffer, res })
Copy link
Member

Choose a reason for hiding this comment

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

AFAICT the only difference between json and xml is the parsing – is that right?

Another way we could consider handling the relationship between _request and _validate is to have _request delegate to a _parse function, and put the format-specific bits there. The service code would invoke _request which would be BaseService's implementation, and that would in turn invoke _parse which would be BaseXxService's.

This would force me to change slightly the way I handled #2031 because every service would then need a _parse implementation. It would also generally force everything to be a native object that can be checked by a schema. However I don't see a problem with that. We could make a BaseSvgBadge subclass easily enough. And encouraging schema validation is good.

I like this idea because the _validate function is in BaseService so it would feel natural to call it from its _request function. The logging code probably could be moved from BaseXxService to BaseService as well.

Copy link
Member Author

@PyvesB PyvesB Sep 1, 2018

Choose a reason for hiding this comment

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

The requests are slightly different as well, in particular the Accept headers. If we go with your suggestion, we'll also have to add a get defaultHeaders function or something like that, which we would override in the implementations.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, right, the request options, that's a good point. The headers could also be added in a _request override. Though the differences in the requests may mean it's clearer to handle this the way it is.

@paulmelnikow paulmelnikow changed the title Delete BaseHTTPService and implement new BaseXmlService (affects [eclipse-marketplace f-droid]) Delete BaseHTTPService and implement new BaseXmlService (affects [eclipse-marketplace f-droid]; also testing on [uptimerobot circleci]) Sep 1, 2018
Conflicts:
	services/base-json.js
	services/base-json.spec.js
@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 19:38 Inactive
@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 19:45 Inactive
services/base.js Outdated
error.message
)
throw new InvalidResponse({
prettyMessage: 'invalid data response',
Copy link
Member

Choose a reason for hiding this comment

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

How about invalid response data?

@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 20:54 Inactive
@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 21:03 Inactive
@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 21:20 Inactive
@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 21:21 Inactive
@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 21:26 Inactive
@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 1, 2018 21:28 Inactive
@@ -10,7 +10,7 @@
"integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
"dev": true,
"requires": {
"@babel/highlight": "^7.0.0"
"@babel/highlight": "7.0.0"
Copy link
Member

Choose a reason for hiding this comment

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

This package-lock diff is unusually large. Could you make sure you're on the latest npm, reset this file, and then run npm install again to see if that tightens it up?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, updating to the latest version of npm seems to have helped a lot. 😉

paulmelnikow
paulmelnikow previously approved these changes Sep 2, 2018
Copy link
Member

@paulmelnikow paulmelnikow left a comment

Choose a reason for hiding this comment

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

Clarified one (prios) suggestion, though happy to defer that, too! This looks great!

@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 2, 2018 19:50 Inactive
paulmelnikow
paulmelnikow previously approved these changes Sep 2, 2018
@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 2, 2018 19:55 Inactive
@PyvesB
Copy link
Member Author

PyvesB commented Sep 2, 2018

@paulmelnikow I've tried inlining the code as per your suggestion, but I think I'm getting confused:

async _requestJson({ schema, url, options = {}, errorMessages = {} }) {
    const logTrace = (...args) => trace.logTrace('fetch', ...args)
    const mergedOptions = {
      ...{ headers: { Accept: 'application/json' } },
      ...options,
    }
    const jsonData = await this._request({
      url,
      options: mergedOptions,
      errorMessages,
    })
    let json
    try {
      json = JSON.parse(jsonData)
    } catch (err) {
      throw new InvalidResponse({
        prettyMessage: 'unparseable json response',
        underlyingError: err,
      })
    }
    logTrace(emojic.dart, 'Response JSON (before validation)', json, {
      deep: true,
    })
    return this.constructor._validate(json, schema)
  }

All JSON tests have started failing with unparseable json response. Am I somehow being thick?

@paulmelnikow
Copy link
Member

Does checkErrorResponse return just th buffer? If so the code looks fine. You could try logging jsonData. Do the service tests work? Could the problem be with the unit tests themselves?

@paulmelnikow
Copy link
Member

Ah, no, I think it returns { buffer, res }. I think you want const { buffer } = await this._request(...).

@paulmelnikow paulmelnikow temporarily deployed to shields-staging-pr-2037 September 3, 2018 17:59 Inactive
@PyvesB
Copy link
Member Author

PyvesB commented Sep 3, 2018

Indeed, I was getting confused with the return types, thanks for the help. This should now be ready to go! 👍

@PyvesB PyvesB merged commit 7417dc5 into badges:master Sep 3, 2018
@shields-deployment
Copy link

This pull request was merged to master branch. This change is now waiting for deployment, which will usually happen within a few days. Stay tuned by joining our #ops channel on Discord!

After deployment, changes are copied to gh-pages branch:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Server, BaseService, GitHub auth, Shared helpers developer-experience Dev tooling, test framework, and CI service-badge New or updated service badge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants