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

[Scoop] Added scoop-license badge. #10627

Merged
merged 9 commits into from
Oct 25, 2024
70 changes: 70 additions & 0 deletions services/scoop/scoop-base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Joi from 'joi'
import { ConditionalGithubAuthV3Service } from '../github/github-auth-service.js'
import { fetchJsonFromRepo } from '../github/github-common-fetch.js'
import { NotFound } from '../index.js'

const gitHubRepoRegExp =
/https:\/\/github.com\/(?<user>.*?)\/(?<repo>.*?)(\/|$)/

const bucketsSchema = Joi.object()
.pattern(/.+/, Joi.string().pattern(gitHubRepoRegExp).required())
.required()

export default class ScoopBase extends ConditionalGithubAuthV3Service {
async fetch({ app, schema }, queryParams) {
if (!this.buckets) {
this.buckets = await fetchJsonFromRepo(this, {
schema: bucketsSchema,
user: 'ScoopInstaller',
repo: 'Scoop',
branch: 'master',
filename: 'buckets.json',
})
}
const bucket = queryParams.bucket || 'main'
let bucketUrl = this.buckets[bucket]
if (!bucketUrl) {
// Parsing URL here will throw an error if the url is invalid
try {
const url = new URL(decodeURIComponent(bucket))

// Throw errors to go to jump to catch statement
// The error messages here are purely for code readability, and will never reach the user.
if (url.hostname !== 'github.com') {
throw new Error('Not a GitHub URL')
}
const path = url.pathname.split('/').filter(value => value !== '')

if (path.length !== 2) {
throw new Error('Not a valid GitHub Repo')
}

const [user, repo] = path

// Reconstructing the url here ensures that the url will match the regex
bucketUrl = `https://github.com/${user}/${repo}`
} catch (e) {
throw new NotFound({ prettyMessage: `bucket "${bucket}" not found` })
}
}
const {
groups: { user, repo },
} = gitHubRepoRegExp.exec(bucketUrl)
try {
return await fetchJsonFromRepo(this, {
schema,
user,
repo,
branch: 'master',
filename: `bucket/${app}.json`,
})
} catch (error) {
if (error instanceof NotFound) {
throw new NotFound({
prettyMessage: `${app} not found in bucket "${bucket}"`,
})
}
throw error
}
}
}
72 changes: 72 additions & 0 deletions services/scoop/scoop-license.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Joi from 'joi'
import { pathParam, queryParam } from '../index.js'
import { renderLicenseBadge } from '../licenses.js'
import toArray from '../../core/base-service/to-array.js'
import ScoopBase from './scoop-base.js'

const scoopLicenseSchema = Joi.object({
license: Joi.alternatives()
.try(
Joi.string().required(),
Joi.object({
identifier: Joi.string().required(),
}),
)
.required(),
}).required()

const queryParamSchema = Joi.object({
bucket: Joi.string(),
})

export default class ScoopLicense extends ScoopBase {
// The buckets file (https://github.com/lukesampson/scoop/blob/master/buckets.json) changes very rarely.
// Cache it for the lifetime of the current Node.js process.
buckets = null

Copy link
Collaborator

Choose a reason for hiding this comment

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

i think this should move to base and was left out in the refactor

static category = 'license'

static route = {
base: 'scoop/l',
pattern: ':app',
queryParamSchema,
}

static openApi = {
'/scoop/l/{app}': {
get: {
summary: 'Scoop License',
description:
'[Scoop](https://scoop.sh/) is a command-line installer for Windows',
MohanKumarAmbati marked this conversation as resolved.
Show resolved Hide resolved
parameters: [
pathParam({ name: 'app', example: 'ngrok' }),
queryParam({
name: 'bucket',
description:
"App's containing bucket. Can either be a name (e.g `extras`) or a URL to a GitHub Repo (e.g `https://github.com/jewlexx/personal-scoop`)",
example: 'extras',
}),
],
},
},
}

static defaultBadgeData = { label: 'license' }

static render({ licenses }) {
return renderLicenseBadge({ licenses })
}

async handle({ app }, queryParams) {
const { license } = await this.fetch(
{ app, schema: scoopLicenseSchema },
queryParams,
)

const licenses = toArray(license).map(license =>
typeof license === 'string' ? license : license.identifier,
)

return this.constructor.render({ licenses })
}
}
97 changes: 97 additions & 0 deletions services/scoop/scoop-license.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { ServiceTester } from '../tester.js'

export const t = new ServiceTester({
id: 'scoop',
title: 'Scoop License',
})
MohanKumarAmbati marked this conversation as resolved.
Show resolved Hide resolved

t.create('License (valid) - with nested response')
.get('/l/ngrok.json')
MohanKumarAmbati marked this conversation as resolved.
Show resolved Hide resolved
.expectBadge({
label: 'license',
message: 'Shareware',
})

t.create('License (valid) - with string response')
.get('/l/nvs.json')
.expectBadge({
label: 'license',
message: 'MIT',
})

t.create('License (invalid)').get('/l/not-a-real-app.json').expectBadge({
label: 'license',
message: 'not-a-real-app not found in bucket "main"',
})

t.create('License (valid custom bucket)')
.get('/l/atom.json?bucket=extras')
.expectBadge({
label: 'license',
message: 'MIT',
})

t.create('license (not found in custom bucket)')
.get('/l/not-a-real-app.json?bucket=extras')
.expectBadge({
label: 'license',
message: 'not-a-real-app not found in bucket "extras"',
})

t.create('license (wrong bucket)')
.get('/l/not-a-real-app.json?bucket=not-a-real-bucket')
.expectBadge({
label: 'license',
message: 'bucket "not-a-real-bucket" not found',
})

// version (bucket url)
const validBucketUrl = encodeURIComponent(
'https://github.com/jewlexx/personal-scoop',
)

t.create('license (valid bucket url)')
.get(`/l/sfsu.json?bucket=${validBucketUrl}`)
.expectBadge({
label: 'license',
message: 'Apache-2.0',
})

const validBucketUrlTrailingSlash = encodeURIComponent(
'https://github.com/jewlexx/personal-scoop/',
)

t.create('license (valid bucket url)')
.get(`/l/sfsu.json?bucket=${validBucketUrlTrailingSlash}`)
.expectBadge({
label: 'license',
message: 'Apache-2.0',
})

t.create('license (not found in custom bucket)')
.get(`/l/not-a-real-app.json?bucket=${validBucketUrl}`)
.expectBadge({
label: 'license',
message: `not-a-real-app not found in bucket "${decodeURIComponent(validBucketUrl)}"`,
})

const nonGithubUrl = encodeURIComponent('https://example.com/')

t.create('license (non-github url)')
.get(`/l/not-a-real-app.json?bucket=${nonGithubUrl}`)
.expectBadge({
label: 'license',
message: `bucket "${decodeURIComponent(nonGithubUrl)}" not found`,
})

const nonBucketRepo = encodeURIComponent('https://github.com/jewlexx/sfsu')

t.create('version (non-bucket repo)')
.get(`/l/sfsu.json?bucket=${nonBucketRepo}`)
.expectBadge({
label: 'license',
// !!! Important note here
// It is hard to tell if a repo is actually a scoop bucket, without getting the contents
// As such, a helpful error message here, which would require testing if the url is a valid scoop bucket, is difficult.
message: `sfsu not found in bucket "${decodeURIComponent(nonBucketRepo)}"`,
})
72 changes: 8 additions & 64 deletions services/scoop/scoop-version.service.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { URL } from 'url'
import Joi from 'joi'
import { NotFound, pathParam, queryParam } from '../index.js'
import { ConditionalGithubAuthV3Service } from '../github/github-auth-service.js'
import { fetchJsonFromRepo } from '../github/github-common-fetch.js'
import { pathParam, queryParam } from '../index.js'
import { renderVersionBadge } from '../version.js'
import ScoopBase from './scoop-base.js'

const gitHubRepoRegExp =
/https:\/\/github.com\/(?<user>.*?)\/(?<repo>.*?)(\/|$)/
const bucketsSchema = Joi.object()
.pattern(/.+/, Joi.string().pattern(gitHubRepoRegExp).required())
.required()
const scoopSchema = Joi.object({
version: Joi.string().required(),
}).required()
const queryParamSchema = Joi.object({
bucket: Joi.string(),
})

export default class ScoopVersion extends ConditionalGithubAuthV3Service {
export default class ScoopVersion extends ScoopBase {
// The buckets file (https://github.com/lukesampson/scoop/blob/master/buckets.json) changes very rarely.
// Cache it for the lifetime of the current Node.js process.
buckets = null
Expand Down Expand Up @@ -56,60 +49,11 @@ export default class ScoopVersion extends ConditionalGithubAuthV3Service {
}

async handle({ app }, queryParams) {
if (!this.buckets) {
this.buckets = await fetchJsonFromRepo(this, {
schema: bucketsSchema,
user: 'ScoopInstaller',
repo: 'Scoop',
branch: 'master',
filename: 'buckets.json',
})
}
const bucket = queryParams.bucket || 'main'
let bucketUrl = this.buckets[bucket]
if (!bucketUrl) {
// Parsing URL here will throw an error if the url is invalid
try {
const url = new URL(decodeURIComponent(bucket))
const { version } = await this.fetch(
{ app, schema: scoopSchema },
queryParams,
)

// Throw errors to go to jump to catch statement
// The error messages here are purely for code readability, and will never reach the user.
if (url.hostname !== 'github.com') {
throw new Error('Not a GitHub URL')
}
const path = url.pathname.split('/').filter(value => value !== '')

if (path.length !== 2) {
throw new Error('Not a valid GitHub Repo')
}

const [user, repo] = path

// Reconstructing the url here ensures that the url will match the regex
bucketUrl = `https://github.com/${user}/${repo}`
} catch (e) {
throw new NotFound({ prettyMessage: `bucket "${bucket}" not found` })
}
}
const {
groups: { user, repo },
} = gitHubRepoRegExp.exec(bucketUrl)
try {
const { version } = await fetchJsonFromRepo(this, {
schema: scoopSchema,
user,
repo,
branch: 'master',
filename: `bucket/${app}.json`,
})
return this.constructor.render({ version })
} catch (error) {
if (error instanceof NotFound) {
throw new NotFound({
prettyMessage: `${app} not found in bucket "${bucket}"`,
})
}
throw error
}
return this.constructor.render({ version })
}
}
Loading