Skip to content

Commit

Permalink
[Scoop] Added scoop-license badge. (#10627)
Browse files Browse the repository at this point in the history
* scoop-license service is added.

* refactored scoop badges - added scoop base

* refactor - description changed to base class.

* refactor - description changed to base class.

* Update services/scoop/scoop-license.tester.js

Co-authored-by: jNullj <15849761+jNullj@users.noreply.github.com>

* refactor - buckets variable is moved to base class, also updated tester with createTestService.

* moved queryParamSchema to base class.

---------

Co-authored-by: jNullj <15849761+jNullj@users.noreply.github.com>
  • Loading branch information
MohanKumarAmbati and jNullj authored Oct 25, 2024
1 parent 28bee86 commit 5e40080
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 73 deletions.
81 changes: 81 additions & 0 deletions services/scoop/scoop-base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 const queryParamSchema = Joi.object({
bucket: Joi.string(),
})

export class ScoopBase 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

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'
63 changes: 63 additions & 0 deletions services/scoop/scoop-license.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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 { queryParamSchema, 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()

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)}"`,
})
82 changes: 9 additions & 73 deletions services/scoop/scoop-version.service.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
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 { queryParamSchema, 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 +20,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 +41,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 })
}
}

0 comments on commit 5e40080

Please sign in to comment.