Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions services/dotnet/dotnet-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
function normalizeTargetFramework(targetFramework) {
if (!targetFramework) {
return 'all'
}
if (targetFramework.startsWith('.NETFramework')) {
const version = targetFramework.slice('.NETFramework'.length)
return `net${version.replace(/\./g, '')}`
}
if (targetFramework.startsWith('.NETStandard')) {
return `netstandard${targetFramework.slice('.NETStandard'.length)}`
}
if (targetFramework.startsWith('.NETCoreApp')) {
return `netcoreapp${targetFramework.slice('.NETCoreApp'.length)}`
}
return targetFramework.toLowerCase()
}

function extractTargetFrameworks(dependencyGroups) {
const frameworks = dependencyGroups.map(group =>
normalizeTargetFramework(group.targetFramework),
)
return [...new Set(frameworks)].sort()
}

const REGISTRATION_BASE_URL =
'https://api.nuget.org/v3/registration5-gz-semver2/'

export {
normalizeTargetFramework,
extractTargetFrameworks,
REGISTRATION_BASE_URL,
}
153 changes: 153 additions & 0 deletions services/dotnet/dotnet.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import Joi from 'joi'
import {
BaseJsonService,
InvalidResponse,
NotFound,
pathParams,
} from '../index.js'
import { stripBuildMetadata, selectVersion } from '../nuget/nuget-helpers.js'
import {
extractTargetFrameworks,
REGISTRATION_BASE_URL,
} from './dotnet-helpers.js'

const registrationIndexSchema = Joi.object({
items: Joi.array()
.items(
Joi.object({
'@id': Joi.string().required(),
}),
)
.required(),
}).required()

const catalogEntrySchema = Joi.object({
version: Joi.string().required(),
dependencyGroups: Joi.array()
.items(
Joi.object({
targetFramework: Joi.string().allow(null),
}),
)
.default([]),
})

const registrationPageSchema = Joi.object({
items: Joi.array()
.items(
Joi.object({
'@id': Joi.string(),
catalogEntry: catalogEntrySchema,
items: Joi.array(),
}),
)
.required(),
}).required()

const description = `
Shows the .NET target frameworks supported by a NuGet package, based on the
package's dependency groups in the NuGet registration API.
`

async function collectCatalogEntries(service, url) {
const page = await service._requestJson({
schema: registrationPageSchema,
url,
})
const lastItem = page.items[page.items.length - 1]
if (lastItem.catalogEntry) {
return page.items
}
if (lastItem.items) {
const nestedLast = lastItem.items[lastItem.items.length - 1]
if (nestedLast.catalogEntry) {
return lastItem.items
}
}
return collectCatalogEntries(service, lastItem['@id'])
}

export default class Dotnet extends BaseJsonService {
static category = 'platform-support'

static route = {
base: 'dotnet',
pattern: ':variant(v|vpre)/:packageName',
}

static openApi = {
'/dotnet/{variant}/{packageName}': {
get: {
summary: '.NET Target Framework',
description,
parameters: pathParams(
{
name: 'variant',
example: 'v',
schema: { type: 'variant', enum: ['v', 'vpre'] },
description:
'Latest stable package version (`v`) or latest version including prereleases (`vpre`).',
},
{ name: 'packageName', example: 'Humanizer.Core' },
),
},
},
}

static defaultBadgeData = {
label: 'dotnet',
namedLogo: 'dotnet',
}

static render({ frameworks }) {
return {
message: frameworks.join(' | '),
color: 'blue',
}
}

registrationIndexUrl({ packageName }) {
return `${REGISTRATION_BASE_URL}${packageName.toLowerCase()}/index.json`
}

transform({ catalogEntries, includePrereleases }) {
if (catalogEntries.length === 0) {
throw new NotFound({ prettyMessage: 'package not found' })
}
const versions = catalogEntries.map(entry =>
stripBuildMetadata(entry.catalogEntry.version),
)
const version = selectVersion(versions, includePrereleases)
const catalogEntry = catalogEntries.find(
entry => stripBuildMetadata(entry.catalogEntry.version) === version,
).catalogEntry
const frameworks = extractTargetFrameworks(
catalogEntry.dependencyGroups || [],
)
if (frameworks.length === 0) {
throw new InvalidResponse({
prettyMessage: 'target frameworks missing',
})
}
return frameworks
}

async fetchCatalogEntries({ packageName }) {
const index = await this._requestJson({
schema: registrationIndexSchema,
url: this.registrationIndexUrl({ packageName }),
httpErrors: {
404: 'package not found',
},
})
const lastPageUrl = index.items[index.items.length - 1]['@id']
return collectCatalogEntries(this, lastPageUrl)
}

async handle({ variant, packageName }) {
const includePrereleases = variant === 'vpre'
const catalogEntries = await this.fetchCatalogEntries({ packageName })
const frameworks = this.transform({ catalogEntries, includePrereleases })
return this.constructor.render({ frameworks })
}
}
106 changes: 106 additions & 0 deletions services/dotnet/dotnet.service.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { test, given } from 'sazerac'
import {
normalizeTargetFramework,
extractTargetFrameworks,
REGISTRATION_BASE_URL,
} from './dotnet-helpers.js'
import Dotnet from './dotnet.service.js'

describe('.NET service', function () {
test(normalizeTargetFramework, () => {
given(null).expect('all')
given(undefined).expect('all')
given('').expect('all')
given('net8.0').expect('net8.0')
given('.NETFramework4.8').expect('net48')
given('.NETFramework4.5').expect('net45')
given('.NETStandard2.0').expect('netstandard2.0')
given('.NETCoreApp3.1').expect('netcoreapp3.1')
})

test(extractTargetFrameworks, () => {
given([
{ targetFramework: '.NETStandard2.0' },
{ targetFramework: '.NETFramework4.8' },
{ targetFramework: 'net8.0' },
]).expect(['net48', 'net8.0', 'netstandard2.0'])
given([{ targetFramework: null }]).expect(['all'])
given([
{ targetFramework: 'net6.0' },
{ targetFramework: 'net6.0' },
]).expect(['net6.0'])
})

test(Dotnet.prototype.registrationIndexUrl, () => {
given({ packageName: 'Humanizer.Core' }).expect(
`${REGISTRATION_BASE_URL}humanizer.core/index.json`,
)
})

test(Dotnet.prototype.transform, () => {
given({
catalogEntries: [
{
catalogEntry: {
version: '1.0.0',
dependencyGroups: [{ targetFramework: 'net6.0' }],
},
},
],
includePrereleases: false,
}).expect(['net6.0'])

given({
catalogEntries: [
{
catalogEntry: {
version: '1.0.0',
dependencyGroups: [{ targetFramework: 'net6.0' }],
},
},
{
catalogEntry: {
version: '2.0.0-beta1',
dependencyGroups: [{ targetFramework: 'net8.0' }],
},
},
],
includePrereleases: false,
}).expect(['net6.0'])

given({
catalogEntries: [
{
catalogEntry: {
version: '1.0.0',
dependencyGroups: [{ targetFramework: 'net6.0' }],
},
},
{
catalogEntry: {
version: '2.0.0-beta1',
dependencyGroups: [{ targetFramework: 'net8.0' }],
},
},
],
includePrereleases: true,
}).expect(['net8.0'])

given({
catalogEntries: [],
includePrereleases: false,
}).expectError('Not Found: package not found')

given({
catalogEntries: [
{
catalogEntry: {
version: '1.0.0',
dependencyGroups: [],
},
},
],
includePrereleases: false,
}).expectError('Invalid Response')
})
})
33 changes: 33 additions & 0 deletions services/dotnet/dotnet.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()

t.create('target frameworks (stable)')
.get('/v/Humanizer.Core.json')
.expectBadge({
label: 'dotnet',
message: 'net48 | net8.0 | netstandard2.0',
color: 'blue',
})

t.create('target frameworks (prerelease)')
.get('/vpre/Humanizer.Core.json')
.expectBadge({
label: 'dotnet',
message: 'net48 | net8.0 | netstandard2.0',
color: 'blue',
})

t.create('single target framework')
.get('/v/Microsoft.AspNetCore.Mvc.json')
.expectBadge({
label: 'dotnet',
message: 'netstandard2.0',
color: 'blue',
})

t.create('package not found')
.get('/v/not-a-real-package-name.json')
.expectBadge({
label: 'dotnet',
message: 'package not found',
})
Loading