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

[RFC] New API for registering services #963

Merged
merged 7 commits into from
Dec 4, 2017
Merged

Conversation

Daniel15
Copy link
Member

This is a proof-of-concept for what a simplified API for registering services could look like. It's actually not that much code - Essentially it's just a simple facade over all the stuff that exists today. To keep things simple, I just kept everything in server.js for now. I converted over the AppVeyor service as an example.

Some notes:

  • Code duplication is mostly removed.
    • Services no longer need to manually call cache or sendBadge. The service handler simply returns the badge data, and the wrapper around it handles caching
    • Service URLs no longer need to manually append .(svg|png|gif|jpg|json) to the end, it's handled automatically
  • Metadata (name, type, and example URLs) are included so that we can automatically generate the try.html page in the future

Note that this does use async functions, which is a new feature in Node.js 7.x. This means we'd need to bump the Node.js version from 6 to 7. However, I think the code clarity is worth it! The handler has a nice linear flow with a regular try-catch block for handling network errors. We could use promises instead, but async function are the future! 😃

Tested the following URLs on my computer (using Node.js 7.8) and it works nicely:
http://localhost:8080/appveyor/ci/gruntjs/grunt.svg
http://localhost:8080/appveyor/ci/gruntjs/grunt/master.svg
http://localhost:8080/appveyor/ci/kittens/yarn.svg

cc @paulmelnikow
References #948

server.js Outdated

const badgeData = getBadgeData(config.type, data);
config.handler(params, request.asPromise)
.then(serviceData => {
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 is still using promise syntax since I didn't want to touch the implementation of cache for now.

server.js Outdated
registerService({
name: 'AppVeyor',
type: 'build',
uri: '/appveyor/ci/([^/]+/[^/]+)(?:/(.+))?',
Copy link
Member Author

Choose a reason for hiding this comment

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

Would be nice to have a simplified way to specify routes (like /appveyor/ci/:user/:project/:branch? or /appveyor/ci/{user}/{project}/{?branch} or something similar) to make the developer experience a bit nicer. Regexes can be pretty inefficient for URLs and abstracting that away would allow a more efficient routing algorithm (eg. using a trie)

However, I'm sticking with small incremental changes and things like that need bigger discussions, so this is a regex for now 😛

Copy link
Member

Choose a reason for hiding this comment

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

Agreed on all counts. Regexes are hard to write and to read.

They are also unidirectional: it's easy to take text and match it with a regex, and hard to use a regex to construct text. A format like /appveyor/ci/:user/:project/:branch? would enable prompting a user for inputs, and constructing a badge URI from that (#777, #466, #701), and similarly would help with an automated tool to create badge URIs (#776).

Upgrading the routing could easily happen in a future pass. 😃

Copy link
Member Author

Choose a reason for hiding this comment

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

@paulmelnikow - Yeah, I agree. If this was using a simpler format, the examples section below could just be a list of the parameters, rather than the full URL. Example:

uri: '/appveyor/ci/:user/:project/:branch?',
examples: [
  { user: 'gruntjs', project: 'grunt' },
],

@paulmelnikow paulmelnikow added the developer-experience Dev tooling, test framework, and CI label Apr 26, 2017
@paulmelnikow
Copy link
Member

This looks neat! Look forward to sitting down with it.

@Daniel15
Copy link
Member Author

Daniel15 commented May 1, 2017

Thinking about this more, perhaps I should totally avoid async/await and just use promises, so that it'd work today without upgrading to a newer Node.js version. async/await is just promises under the hood, so it could be changed to async/await in the future.

@paulmelnikow
Copy link
Member

This interface looks really promising. The callbacks really "clean up" well! ✨

I'm leaving you a round of comments.

Thinking about this more, perhaps I should totally avoid async/await and just use promises, so that it'd work today without upgrading to a newer Node.js version.

The handler functions would be a lot of code to rewrite and review twice. We could run an experiment, converting one very popular badge to this async/await form, trying it out in production, and seeing if there are any adverse effects. If async/await are stable enough to be used, I'm inclined to use them. 😃

server.js Outdated
registerService({
name: 'AppVeyor',
type: 'build',
uri: '/appveyor/ci/([^/]+/[^/]+)(?:/(.+))?',
Copy link
Member

Choose a reason for hiding this comment

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

Agreed on all counts. Regexes are hard to write and to read.

They are also unidirectional: it's easy to take text and match it with a regex, and hard to use a regex to construct text. A format like /appveyor/ci/:user/:project/:branch? would enable prompting a user for inputs, and constructing a badge URI from that (#777, #466, #701), and similarly would help with an automated tool to create badge URIs (#776).

Upgrading the routing could easily happen in a future pass. 😃

server.js Outdated
});
}));
},
});
Copy link
Member

Choose a reason for hiding this comment

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

For the logistics of this PR, would you mind moving the badge definition into a separate file? It'll be a bit easier to read.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure! I think it's self-contained enough that don't that won't be too difficult. I was planning on doing that anyways, as my end goal is to separate out all the services from server.js.

server.js Outdated
});
} catch (e) {
return {text: 'inaccessible'};
}
Copy link
Member

Choose a reason for hiding this comment

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

Ideally, the service author wouldn't need to implement this try block. It's boilerplate.

Would it be possible to omit it, let the network error bubble up to the calling function, and handle it there?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah good idea, the call to the handler in registerService itself could have a try-catch block. I'll do it!

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually I just remembered that I do already have a catch block in the registerService, so I don't need to have one here too 😛

server.js Outdated
try {
var data = JSON.parse(buffer);
var status = data.build.status;
result = await request(apiUrl, {
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about switching to node-fetch? It's already well established in browsers and supported natively in the latest version of every major browser. That means going forward, it will be the most familiar to contributors. Every full-stack dev will know fetch, even if they write their server code in Go, and node-fetch is in wide enough use on the server that most Node devs will have encountered it too.

Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't want to touch any of the core abstractions (caching, fetching, SVG generation, rasterisation, etc) in this pull request. There's lots of fancy caching stuff in request that I don't want to break. I'd like to keep these pull requests small, and keep those changes separate.

server.js Outdated
handler: async ([repo, branch], request) => {
let apiUrl = 'https://ci.appveyor.com/api/projects/' + repo;
if (branch != null) {
apiUrl += '/branch/' + branch;
}
Copy link
Member

Choose a reason for hiding this comment

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

Have you experimented with services that contain routing within? For example, CRAN/METACRAN has the version and license within one handler. I'm curious how that could translate. I'm thinking about the examples and the handler code itself.

Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't yet. I think I'd just model that by having multiple services registered (one for version, one for license) and pull the shared bits into a separate function that both services use.

server.js Outdated
} catch(e) {
badgeData.text[1] = 'invalid';
sendBadge(format, badgeData);
return {text: 'invalid'};
Copy link
Member

Choose a reason for hiding this comment

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

I’ve been reflecting on the way we deal with malformed responses with a mix of careful (e.g. Array.isArray) and broad (try/catch) checks. A try/catch in particular makes it hard for service users (or contributors/maintainers) to differentiate between programmer errors and malformed upstream responses. When writing new code, it creates gotchas, because the try block swallows up all sorts of things like ReferenceErrors.

There’s also a frankensteining of styles: sometimes we have fanatical checks, other times we rely on the try block, and sometimes it's both at the same time.

I wonder if a declarative approach to validation would improve this, and at the same time reduce the number of branches.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah you mentioned this via email. Validation the JSON via Joi or something similar seems like a reasonable future improvement.

server.js Outdated
if (status === 'success') {
badgeData.text[1] = 'passing';
badgeData.colorscheme = 'brightgreen';
return {text: 'passing', colorscheme: 'brightgreen'};
Copy link
Member

Choose a reason for hiding this comment

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

Really like this interface: a function which returns a badge specification. I also like that this one is written so declaratively.

server.js Outdated
if (status === 'success') {
badgeData.text[1] = 'passing';
badgeData.colorscheme = 'brightgreen';
return {text: 'passing', colorscheme: 'brightgreen'};
} else if (status !== 'running' && status !== 'queued') {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: The logic would be easier to reason about without negation:

if (['queued', 'running'].includes(status)) {
  return {text: status};
} else if (status === 'success') {
  return {text: 'passing', colorscheme: 'brightgreen'};
} else {
  return {text: 'failing', colorscheme: 'red'};
}

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 downside of doing it like if (['queued', 'running'].includes(status)) { is that you have one extra array allocation per call 😃 I could flip it so it reads like if (status === 'running' || status === 'queued') though.

server.js Outdated
params: '/appveyor/ci/gruntjs/grunt/master',
},
],
handler: async ([repo, branch], request) => {
Copy link
Member

Choose a reason for hiding this comment

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

This is a neat way to break down the regex matches! Very cool!

I wanted to suggest a tweak. To avoid mistakes when code is copied and pasted, and when capture groups are added and removed, would it be better to bind the names closer to the regex?

match: {
  fragment: '/appveyor/ci/([^/]+/[^/]+)(?:/(.+))?',
  capture: ['repo', 'branch'],
},
handler: async (params, request) => {
  const { repo, branch } = params;
  ...
}

This would also allow us to validate them automatically. At runtime we could automatically check that the number of identifiers matches the number of capture groups obtained from the regex.

If a handler or a regex is copied without its counterpart, it'll fail obviously when it tries to destructure, instead of confusingly when the labels don't match.

Copy link
Member Author

Choose a reason for hiding this comment

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

To avoid mistakes when code is copied and pasted, and when capture groups are added and removed

Tests should catch breakages like that though, right? 😄

I really really wish JavaScript had named capture groups, then the regex could look like:

/appveyor/ci/(?<repo>[^/]+/[^/]+)(?<branch>:/(.+))?

And matches would be an object with a repo and branch properties.

(I'm using .NET named capture group syntax for this example)

We could import XRegExp which would allow us to use named capture groups, if you like (fun side note - The developer of XRegExp is my manager at Facebook 😛 ). Alternatively, we could just wait until we refactor the URI parsing, which could also fix how this is handled.

I do like your idea though (specifying names of the capture groups as an array. I think I'll go with that for now, I think it's sufficiently clean.

Copy link
Member

Choose a reason for hiding this comment

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

I like how this came out!

Copy link
Member Author

@Daniel15 Daniel15 left a comment

Choose a reason for hiding this comment

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

Thanks so much for your comments! I'll iterate on this and make some changes soon.

server.js Outdated
registerService({
name: 'AppVeyor',
type: 'build',
uri: '/appveyor/ci/([^/]+/[^/]+)(?:/(.+))?',
Copy link
Member Author

Choose a reason for hiding this comment

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

@paulmelnikow - Yeah, I agree. If this was using a simpler format, the examples section below could just be a list of the parameters, rather than the full URL. Example:

uri: '/appveyor/ci/:user/:project/:branch?',
examples: [
  { user: 'gruntjs', project: 'grunt' },
],

server.js Outdated
params: '/appveyor/ci/gruntjs/grunt/master',
},
],
handler: async ([repo, branch], request) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

To avoid mistakes when code is copied and pasted, and when capture groups are added and removed

Tests should catch breakages like that though, right? 😄

I really really wish JavaScript had named capture groups, then the regex could look like:

/appveyor/ci/(?<repo>[^/]+/[^/]+)(?<branch>:/(.+))?

And matches would be an object with a repo and branch properties.

(I'm using .NET named capture group syntax for this example)

We could import XRegExp which would allow us to use named capture groups, if you like (fun side note - The developer of XRegExp is my manager at Facebook 😛 ). Alternatively, we could just wait until we refactor the URI parsing, which could also fix how this is handled.

I do like your idea though (specifying names of the capture groups as an array. I think I'll go with that for now, I think it's sufficiently clean.

server.js Outdated
handler: async ([repo, branch], request) => {
let apiUrl = 'https://ci.appveyor.com/api/projects/' + repo;
if (branch != null) {
apiUrl += '/branch/' + branch;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't yet. I think I'd just model that by having multiple services registered (one for version, one for license) and pull the shared bits into a separate function that both services use.

server.js Outdated
try {
var data = JSON.parse(buffer);
var status = data.build.status;
result = await request(apiUrl, {
Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't want to touch any of the core abstractions (caching, fetching, SVG generation, rasterisation, etc) in this pull request. There's lots of fancy caching stuff in request that I don't want to break. I'd like to keep these pull requests small, and keep those changes separate.

server.js Outdated
});
} catch (e) {
return {text: 'inaccessible'};
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah good idea, the call to the handler in registerService itself could have a try-catch block. I'll do it!

server.js Outdated
if (status === 'success') {
badgeData.text[1] = 'passing';
badgeData.colorscheme = 'brightgreen';
return {text: 'passing', colorscheme: 'brightgreen'};
} else if (status !== 'running' && status !== 'queued') {
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 downside of doing it like if (['queued', 'running'].includes(status)) { is that you have one extra array allocation per call 😃 I could flip it so it reads like if (status === 'running' || status === 'queued') though.

server.js Outdated
} catch(e) {
badgeData.text[1] = 'invalid';
sendBadge(format, badgeData);
return {text: 'invalid'};
Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah you mentioned this via email. Validation the JSON via Joi or something similar seems like a reasonable future improvement.

server.js Outdated
});
}));
},
});
Copy link
Member Author

Choose a reason for hiding this comment

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

Sure! I think it's self-contained enough that don't that won't be too difficult. I was planning on doing that anyways, as my end goal is to separate out all the services from server.js.

server.js Outdated
}
});
}));
registerServices(require('./services/appveyor'));
Copy link
Member Author

Choose a reason for hiding this comment

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

Eventually I imagine we'll have some sort of "auto registration" for all files in the services directory rather than manually doing it like this.

Copy link
Member

Choose a reason for hiding this comment

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

This is pretty easy to do:

glob.sync(`${__dirname}/services/*.js`).map(name => require(name)).forEach(registerServices);

Copy link
Member Author

Choose a reason for hiding this comment

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

@paulmelnikow The issue is that we also need to sort the URLs such that longer strings are registered before shorter ones, or enforce that URLs are not substrings of other URLs (and thus will never conflict). Right now there might be some implicit dependencies between badges based on the registration order :/

Copy link
Member

Choose a reason for hiding this comment

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

Right… hmm, why don't we fix that as we're migrating the badges? Probably the few badges where this matters could go in the same file, so they can be kept in order relative to each other. glob will sort alphabetically, so in any case the results should be consistent.

@@ -0,0 +1,39 @@
const loadLogos = require('../lib/load-logos');

module.exports = [{
Copy link
Member Author

Choose a reason for hiding this comment

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

Services are returned as an array to handle the cases where one service has multiple handlers (eg. NuGet has one URL for release version number, one URL for unstable version number, and URLs for download counts)

Copy link
Member

@paulmelnikow paulmelnikow Sep 30, 2017

Choose a reason for hiding this comment

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

Makes sense. How about allowing a single service to be returned as a shorthand?

Added: lib/badge-data has a toArray function. 😄

@Daniel15
Copy link
Member Author

@badges/shields Any other thoughts on this?

@paulmelnikow
Copy link
Member

Hi! Since I resumed working on Shields, I've been working through the more straightforward PRs and trying to get CI fixed. We're as close as we've ever been to clearing the PR backlog, which would be really great. 🏅

I wrote brand new badges for #1114 and rewrote one for #1112. I encountered a few pain points. The try blocks swallowed errors, the nested request() calls were hard to read, badgeData and query params are clunky, I kept losing my place in the giant server.js, and even though our test setup is much improved, the service tests are opaque when they fail. There are lots of opportunities to improve.

It's a good motivation to pick this up! I'll read again and leave more comments.

@Daniel15
Copy link
Member Author

Thanks @paulmelnikow! I'm happy to iterate on this once I receive feedback 😃

Async functions are pretty reliable in Node.js now that another major version (Node 8) has come out since I originally submitted this PR.

{uri: '/appveyor/ci/gruntjs/grunt'},
{
name: 'Branch',
params: '/appveyor/ci/gruntjs/grunt/master',
Copy link
Member

Choose a reason for hiding this comment

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

Is this a typo? One of these examples has a uri key, and the other a params key, for what looks to be the same thing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it's supposed to be uri for both. I originally wanted to do something like:

params: {
  repo: 'gruntjs/grunt',
  branch: 'master',
}

But it requires us to change how URLs are handled.

const data = JSON.parse(result.buffer);
const status = data.build.status;
if (status === 'success') {
return {text: 'passing', colorscheme: 'brightgreen'};
Copy link
Member

Choose a reason for hiding this comment

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

Loving these plain JS object return values! Seems very testable, which is great.

Could you provide a way for defaultBadgeData and the handler to override the label? And maybe think of some clearer property names here. Since a badge has two pieces of text, text is a bit unclear.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

server.js Outdated
params: '/appveyor/ci/gruntjs/grunt/master',
},
],
handler: async ([repo, branch], request) => {
Copy link
Member

Choose a reason for hiding this comment

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

I like how this came out!

@paulmelnikow
Copy link
Member

Would you consider an object-oriented version, where each service subclasses a base Service?

In my experience, as these handle()-type functions become more complex, it's very natural to grow them with a class. Classes guide service implementers to build out their code in a more consistent way, which helps with readability across the project. If you look at the badge-specific helpers I broke out in #1109 you'll get a sense of what I mean. Each of these services took a different approach to factoring out their shared code.

The constructor provides a clean way to inject stateful dependencies, as you're doing here. This code includes request, though AFAIK we have four of these right now: we also have regularUpdate, githubAuth.request, and the logger.

It also makes it possible to set up helper objects which use the dependencies, and gives us a lot of flexibility about how these could work. In case it's not clear what I mean, I added a sketch below with one idea of a helper object and helper method.

Finally, I'd prefer we could test a service without standing up a server: in other words, unit test them. The test code could call right into the handler instead of indirecting through HTTP calls. These would be more flexible to set up, better isolated, easier to reason about/debug/instrument, and likely a bit faster to start. A Service superclass provides a nice place to put registerServices, and to break out parts as needed to support testing.

class AppVeyor extends Service {
  constructor (context) {
    super(context);
    this.vendor = new JsonVendor(context, {
      validationSchema: Joi.object().keys({
        data: Joi.object().keys({
          status: Joi.equal('success', 'running', 'queued', 'failing').isRequired()
        }).isRequired()
      }),
      notFoundMessage: 'project not found or access denied',
    });
  }

  static getUri({ repo, branch }) {
    ...
  }

  async handler (params) {
    const data = await this.vendor.request(Appveyor.getUri(params), ...);
    ...
  }
}
Object.assign(AppVeyor, {
  dependencies: JsonVendor.dependencies,
  // `name` can default to introspecting from the class name
  type: 'build',
  ...
});

Thoughts?

@Daniel15
Copy link
Member Author

Daniel15 commented Oct 1, 2017

Using classes is an interesting idea. I was trying to keep the code relatively light, but using classes should be fine. I might not have time to get back to this for a few weeks, but I'll definitely come back to it soon.

type: 'build',
uri: {
format: '/appveyor/ci/([^/]+/[^/]+)(?:/(.+))?',
capture: ['repo', 'branch']
Copy link
Member

Choose a reason for hiding this comment

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

I really like this idea. Group names definition is right below regex. Simple and useful implementation of named capturing groups.

Copy link

Choose a reason for hiding this comment

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

I feel like using type as property name might be misleading to developers. In the end, we all know that the type will define the left label. So, why no just call that property leftLabel or title ?

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 fact that it's used as the left label is an implementation detail. type is really the "group" that it falls under on the https://shields.io/ website. I could call it group or category instead maybe?

Copy link

@gempain gempain Oct 27, 2017

Choose a reason for hiding this comment

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

I understand 😄 But I feel like forcing the semantic meaning of the left label as a "group" is too restrictive. What if users want to make tags that have a left label that is not an actual group ? As an example, yesterday I made (okay, it's not a great example, but it was quite fun) a badge that shows the weather in a given town (I haven't PR's it, I mean, not like anyone would really use it 😛 ). I would not consider that "weather" is a type of badge. Anyways, if the goal is to keep it as a "type" semantic, I would name category as I think developers could think that type is related to some sort of generic badge type ? (which I guess is you goal ^^).

Copy link
Member

Choose a reason for hiding this comment

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

Agree category is good, since it suggests it's taxonomical in nature. Often the category is the same as the default label, so I like this way of handling it.

It's not a bad idea to provide a way to override the default label when it's different from the category. For example, the Github stars badge is in the "social" category but the default label is "Stars."

@Daniel15
Copy link
Member Author

Daniel15 commented Oct 18, 2017 via email

server.js Outdated
);
config.handler(namedParams, request.asPromise)
.then(serviceData => {
let text = badgeData.text;
Copy link
Member

Choose a reason for hiding this comment

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

I guess we can have const here.

@paulmelnikow
Copy link
Member

Now that I'm back in the USA, I might try to look at this again over the weekend :)

That would be awesome!

@Daniel15
Copy link
Member Author

@paulmelnikow - I was thinking about this a bit, and am wondering if we do actually need a base class...

A Service superclass provides a nice place to put registerServices, and to break out parts as needed to support testing.

Usually I find that mocking frameworks are sufficient to support testing, as long as the code has been broken out into a number of small modules. JavaScript testing frameworks let you mock anything (so for example we could mock lib/request-handler to totally mock the outbound request mechanism).

Do you still think there's value in having a base class? Would we have a single instance of the class for the entire run time of the app, or would we instantiate one instance per request (and thus allow them to have instance data specific to the one request)?

@paulmelnikow
Copy link
Member

What I meant about breaking out parts of registerServices for testing is that the function does a lot of things. I'd like a way to pass in a URI and maybe query parameters, and get badge data. To do this clearly would require factoring that code out of registerServices, one way or another.

I'm thinking one instance per run of the app. I've looked at a lot of these services and while they need a way to invoke helpers with a parameter or two, none of them involve complex request state.

There's a lot to be said for a common pattern of initializing helper objects and factoring out helper functions. Experience says classes really do help people write more uniform code.

(Sorry, just saw this comment!)

@paulmelnikow paulmelnikow mentioned this pull request Nov 10, 2017
10 tasks
@paulmelnikow
Copy link
Member

Hey! I thought I would check in about this! Do you think you'll have time to look at this soon?

@Daniel15 Daniel15 changed the base branch from node-8 to master December 3, 2017 22:20
@Daniel15 Daniel15 changed the base branch from master to node-8 December 3, 2017 22:20
@Daniel15
Copy link
Member Author

Daniel15 commented Dec 3, 2017

@paulmelnikow - I think I got all your changes! I also added a very basic unit test 😃

Turns out I didn't even need to think about the logo stuff yet, as it seems like the AppVeyor badge no longer shows the logo by default. Because of that, I just removed all the logo-related changes from this PR

assert.equal(mockCamp.route.getCall(0).args[0].toString(), expectedRouteRegex);
});

it('handles the request', async () => {
Copy link
Member

Choose a reason for hiding this comment

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

Aw, love these async tests with async/await!

Copy link
Member Author

@Daniel15 Daniel15 Dec 4, 2017

Choose a reason for hiding this comment

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

Yeah I'm happy that Mocha allows test cases to return promises. Async/await is just a fancy syntax for promises.

I'm used to Jest, so I had to keep the Mocha docs open while writing these tests. They're similar but there's subtle differences (eg. Mocha doesn't have a mocking/stubbing framework built in and you need to use Sinon, whereas Jest includes mocking out-of-the-box).

@paulmelnikow
Copy link
Member

Looks great!

@Daniel15
Copy link
Member Author

Daniel15 commented Dec 4, 2017

Sorry for not fixing the lint issues! Thanks for looking at it :)

@paulmelnikow
Copy link
Member

Would you like the honor of merging this? 😁

@Daniel15
Copy link
Member Author

Daniel15 commented Dec 4, 2017

Sure! Let's do it 😄

@Daniel15 Daniel15 merged commit 1eef462 into badges:node-8 Dec 4, 2017
@paulmelnikow
Copy link
Member

Could you do that with squash instead?

@Daniel15
Copy link
Member Author

Daniel15 commented Dec 4, 2017

Oops, I thought I selected the squash option... Didn't realise it wasn't the default for this repo (I'm used to it being the default option). I can figure out how to squash the commits and force push it.

@paulmelnikow
Copy link
Member

Github doesn't let you set a default. We could disable rebasing though. I wouldn't mind doing that. The times I've used rebase were when two things were done in one PR but separate commits, and the history added a lot of context. Rare enough that it's probably worth not having to think about it!

@Daniel15
Copy link
Member Author

Daniel15 commented Dec 4, 2017

Today I learned how to squash commits :) c7f03fd

paulmelnikow added a commit that referenced this pull request Mar 12, 2018
…1543

This merges the `node-8` branch. The heavy lift was by @Daniel15 with refactoring from me and a patch by @RedSparr0w.

* New API for registering services (#963)
* Disable Node 6 tests on node-8 branch (#1423)
* BaseService: Factor out methods _regex and _namedParamsForMatch (#1425)
    - Adjust test grouping
    - Rename data -> queryParams, text -> message
* BaseService tests: Use Chai (#1450)
* BaseService: make serviceData and badgeData explicit and declarative (#1451)
* fix isValidStyle test (#1544)
* Run tests in Node 9, not Node 6 (#1543)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
developer-experience Dev tooling, test framework, and CI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants