Skip to content

Commit

Permalink
sources: Reorganize from one long file into several shorter files
Browse files Browse the repository at this point in the history
I found it was starting to get harder to find your way around and ignore
adjacent things that weren't relevant.

No functional code changes; only added lines which aren't identical
copies of removed lines are for require() path adjustments.
  • Loading branch information
tsibley committed Nov 19, 2021
1 parent a4bd2b5 commit f4a6059
Show file tree
Hide file tree
Showing 8 changed files with 895 additions and 835 deletions.
835 changes: 0 additions & 835 deletions src/sources.js

This file was deleted.

148 changes: 148 additions & 0 deletions src/sources/community.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/* eslint no-use-before-define: ["error", {"functions": false, "classes": false}] */
const {fetch} = require("../fetch");
const queryString = require("query-string");
const {NotFound} = require('http-errors');
const utils = require("../utils");
const {Source, Dataset, Narrative} = require("./models");

class CommunitySource extends Source {
constructor(owner, repoName) {
super();

// The GitHub owner and repo names are required.
if (!owner) throw new Error(`Cannot construct a ${this.constructor.name} without an owner`);
if (!repoName) throw new Error(`Cannot construct a ${this.constructor.name} without a repoName`);

this.owner = owner;
[this.repoName, this.branch] = repoName.split(/@/, 2);
this.branchExplicitlyDefined = !!this.branch;

if (!this.repoName) throw new Error(`Cannot construct a ${this.constructor.name} without a repoName after splitting on /@/`);

this.defaultBranch = fetch(`https://api.github.com/repos/${this.owner}/${this.repoName}`)
.then((res) => res.json())
.then((data) => data.default_branch)
.catch(() => {
console.log(`Error interpreting the default branch of ${this.constructor.name} for ${this.owner}/${this.repoName}`);
return "master";
});
if (!this.branch) {
this.branch = this.defaultBranch;
}
}

static get _name() { return "community"; }
get repo() { return `${this.owner}/${this.repoName}`; }
async baseUrl() {
return `https://github.com/${this.repo}/raw/${await this.branch}/`;
}

async repoNameWithBranch() {
const branch = await this.branch;
const defaultBranch = await this.defaultBranch;
if (branch === defaultBranch && !this.branchExplicitlyDefined) {
return this.repoName;
}
return `${this.repoName}@${branch}`;
}

dataset(pathParts) {
return new CommunityDataset(this, pathParts);
}
narrative(pathParts) {
return new CommunityNarrative(this, pathParts);
}

async availableDatasets() {
const qs = queryString.stringify({ref: await this.branch});
const response = await fetch(`https://api.github.com/repos/${this.repo}/contents/auspice?${qs}`);

if (response.status === 404) throw new NotFound();
else if (response.status !== 200 && response.status !== 304) {
utils.warn(`Error fetching available datasets from GitHub for source ${this.name}`, await utils.responseDetails(response));
return [];
}

const filenames = (await response.json())
.filter((file) => file.type === "file")
// remove anything which doesn't start with the repo name, which is required of community datasets
.filter((file) => file.name.startsWith(this.repoName))
.map((file) => file.name);
const pathnames = utils.getDatasetsFromListOfFilenames(filenames)
// strip out the repo name from the start of the pathnames
// as CommunityDataset().baseParts will add this in
.map((pathname) => pathname.replace(`${this.repoName}/`, ""));
return pathnames;
}

async availableNarratives() {
const qs = queryString.stringify({ref: await this.branch});
const response = await fetch(`https://api.github.com/repos/${this.repo}/contents/narratives?${qs}`);

if (response.status !== 200 && response.status !== 304) {
if (response.status !== 404) {
// not found doesn't warrant an error print, it means there are no narratives for this repo
utils.warn(`Error fetching available narratives from GitHub for source ${this.name}`, await utils.responseDetails(response));
}
return [];
}

const files = await response.json();
return files
.filter((file) => file.type === "file")
.filter((file) => file.name !== "README.md")
.filter((file) => file.name.endsWith(".md"))
.filter((file) => file.name.startsWith(this.repoName))
.map((file) => file.name
.replace(this.repoName, "")
.replace(/^_/, "")
.replace(/[.]md$/, "")
.split("_")
.join("/"));
}
async getInfo() {
/* could attempt to fetch a certain file from the repository if we want to implement
this functionality in the future */
const branch = await this.branch;
return {
title: `${this.owner}'s "${this.repoName}" community builds`,
byline: `
Nextstrain community builds for GitHub → ${this.owner}/${this.repoName} (${branch} branch).
The available datasets and narratives in this repository are listed below.
`,
website: null,
showDatasets: true,
showNarratives: true,
/* avatar could be fetched here & sent in base64 or similar, or a link sent. The former (or similar) has the advantage
of private S3 buckets working, else the client will have to make (a) an authenticated request (too much work)
or (b) a subsequent request to nextstrain.org/charon (why not do it at once?) */
avatar: `https://github.com/${this.owner}.png?size=200`
};
}
}

class CommunityDataset extends Dataset {
get baseParts() {
// We require datasets are in the auspice/ directory and include the repo
// name in the file basename.
return [`auspice/${this.source.repoName}`, ...this.pathParts];
}
get isRequestValidWithoutDataset() {
if (!this.pathParts.length) {
return true;
}
return false;
}
}

class CommunityNarrative extends Narrative {
get baseParts() {
// We require narratives are in the narratives/ directory and include the
// repo name in the file basename.
return [`narratives/${this.source.repoName}`, ...this.pathParts];
}
}

module.exports = {
CommunitySource,
};
71 changes: 71 additions & 0 deletions src/sources/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const {fetch} = require("../fetch");
const queryString = require("query-string");
const {NotFound} = require('http-errors');
const utils = require("../utils");
const {Source} = require("./models");

class CoreSource extends Source {
static get _name() { return "core"; }
async baseUrl() { return "http://data.nextstrain.org/"; }
get repo() { return "nextstrain/narratives"; }
get branch() { return "master"; }

async urlFor(path, method = 'GET') { // eslint-disable-line no-unused-vars
const baseUrl = path.endsWith(".md")
? `https://raw.githubusercontent.com/${this.repo}/${await this.branch}/`
: await this.baseUrl();

const url = new URL(path, baseUrl);
return url.toString();
}

// The computation of these globals should move here.
secondTreeOptions(path) {
return (global.availableDatasets.secondTreeOptions[this.name] || {})[path] || [];
}

availableDatasets() {
return global.availableDatasets[this.name] || [];
}

async availableNarratives() {
const qs = queryString.stringify({ref: this.branch});
const response = await fetch(`https://api.github.com/repos/${this.repo}/contents?${qs}`);

if (response.status === 404) throw new NotFound();
else if (response.status !== 200 && response.status !== 304) {
utils.warn(`Error fetching available narratives from GitHub for source ${this.name}`, await utils.responseDetails(response));
return [];
}

const files = await response.json();
return files
.filter((file) => file.type === "file")
.filter((file) => file.name !== "README.md")
.filter((file) => file.name.endsWith(".md"))
.map((file) => file.name
.replace(/[.]md$/, "")
.split("_")
.join("/"));
}

async getInfo() {
return {
title: `Nextstrain ${this.name} datasets & narratives`,
showDatasets: true,
showNarratives: true,
};
}
}

class CoreStagingSource extends CoreSource {
static get _name() { return "staging"; }
async baseUrl() { return "http://staging.nextstrain.org/"; }
get repo() { return "nextstrain/narratives"; }
get branch() { return "staging"; }
}

module.exports = {
CoreSource,
CoreStagingSource,
};
72 changes: 72 additions & 0 deletions src/sources/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint no-use-before-define: ["error", {"functions": false, "classes": false}] */
const {Source, Dataset, DatasetSubresource, Narrative, NarrativeSubresource} = require("./models");

class UrlDefinedSource extends Source {
static get _name() { return "fetch"; }

constructor(authority) {
super();

if (!authority) throw new Error(`Cannot construct a ${this.constructor.name} without a URL authority`);

this.authority = authority;
}

async baseUrl() {
return `https://${this.authority}`;
}
dataset(pathParts) {
return new UrlDefinedDataset(this, pathParts);
}
narrative(pathParts) {
return new UrlDefinedNarrative(this, pathParts);
}

// available datasets & narratives are unknown when the dataset is specified by the URL
async availableDatasets() { return []; }
async availableNarratives() { return []; }
async getInfo() { return {}; }
}

class UrlDefinedDataset extends Dataset {
get baseName() {
return this.baseParts.join("/");
}
subresource(type) {
return new UrlDefinedDatasetSubresource(this, type);
}
}

class UrlDefinedDatasetSubresource extends DatasetSubresource {
get baseName() {
const type = this.type;
const baseName = this.resource.baseName;

if (type === "main") {
return baseName;
}

return baseName.endsWith(".json")
? `${baseName.replace(/\.json$/, '')}_${type}.json`
: `${baseName}_${type}`;
}
}

class UrlDefinedNarrative extends Narrative {
get baseName() {
return this.baseParts.join("/");
}
subresource(type) {
return new UrlDefinedNarrativeSubresource(this, type);
}
}

class UrlDefinedNarrativeSubresource extends NarrativeSubresource {
get baseName() {
return this.resource.baseName;
}
}

module.exports = {
UrlDefinedSource,
};
Loading

0 comments on commit f4a6059

Please sign in to comment.