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
77 changes: 77 additions & 0 deletions services/scoop/scoop-base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 class ScoopBase extends ConditionalGithubAuthV3Service {
MohanKumarAmbati marked this conversation as resolved.
Show resolved Hide resolved
// 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

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
}
}
}

export const description =
'[Scoop](https://scoop.sh/) is a command-line installer for Windows'
67 changes: 67 additions & 0 deletions services/scoop/scoop-license.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 { description, 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 {
static category = 'license'

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

static openApi = {
'/scoop/l/{app}': {
get: {
summary: 'Scoop License',
description,
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 })
}
}
94 changes: 94 additions & 0 deletions services/scoop/scoop-license.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { createServiceTester } from '../tester.js'

export const t = await createServiceTester()

t.create('License (valid) - with nested response')
.get('/ngrok.json')
.expectBadge({
label: 'license',
message: 'Shareware',
})

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

t.create('License (invalid)').get('/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('/atom.json?bucket=extras')
.expectBadge({
label: 'license',
message: 'MIT',
})

t.create('license (not found in custom bucket)')
.get('/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('/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(`/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(`/sfsu.json?bucket=${validBucketUrlTrailingSlash}`)
.expectBadge({
label: 'license',
message: 'Apache-2.0',
})

t.create('license (not found in custom bucket)')
.get(`/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(`/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(`/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)}"`,
})
79 changes: 9 additions & 70 deletions services/scoop/scoop-version.service.js
Original file line number Diff line number Diff line change
@@ -1,27 +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 { description, 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 {
// 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

export default class ScoopVersion extends ScoopBase {
static category = 'version'

static route = {
Expand All @@ -34,8 +23,7 @@ export default class ScoopVersion extends ConditionalGithubAuthV3Service {
'/scoop/v/{app}': {
get: {
summary: 'Scoop Version',
description:
'[Scoop](https://scoop.sh/) is a command-line installer for Windows',
description,
parameters: [
pathParam({ name: 'app', example: 'ngrok' }),
queryParam({
Expand All @@ -56,60 +44,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))

// 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
const { version } = await this.fetch(
{ app, schema: scoopSchema },
queryParams,
)

// 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