Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Merged
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
3 changes: 3 additions & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@
"ajv": "^8.17.1",
"quick-lru": "^7.0.1",
"tiny-invariant": "^1.3.3"
},
"devDependencies": {
"@dpkit/test": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
{
"log": {
"_recordingName": "validatePackageDescriptor-should validate camtrap dp (#144)",
"creator": {
"comment": "persister:fs",
"name": "Polly.JS",
"version": "6.0.6"
},
"entries": [
{
"_id": "4306f83bec3be19c1183804f9d081277",
"_order": 0,
"cache": {},
"request": {
"bodySize": 0,
"cookies": [],
"headers": [],
"headersSize": 109,
"httpVersion": "HTTP/1.1",
"method": "GET",
"queryString": [],
"url": "https://raw.githubusercontent.com/tdwg/camtrap-dp/refs/tags/1.0.2/example/datapackage.json"
},
"response": {
"bodySize": 2603,
"content": {
"mimeType": "text/plain; charset=utf-8",
"size": 2603,
"text": "{\n \"resources\": [\n {\n \"name\": \"deployments\",\n \"path\": \"deployments.csv\",\n \"profile\": \"tabular-data-resource\",\n \"format\": \"csv\",\n \"mediatype\": \"text/csv\",\n \"encoding\": \"utf-8\",\n \"schema\": \"https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0.2/deployments-table-schema.json\"\n },\n {\n \"name\": \"media\",\n \"path\": \"media.csv\",\n \"profile\": \"tabular-data-resource\",\n \"format\": \"csv\",\n \"mediatype\": \"text/csv\",\n \"encoding\": \"utf-8\",\n \"schema\": \"https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0.2/media-table-schema.json\"\n },\n {\n \"name\": \"observations\",\n \"path\": \"observations.csv\",\n \"profile\": \"tabular-data-resource\",\n \"format\": \"csv\",\n \"mediatype\": \"text/csv\",\n \"encoding\": \"utf-8\",\n \"schema\": \"https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0.2/observations-table-schema.json\"\n },\n {\n \"name\": \"individuals\",\n \"description\": \"Custom table/resource not part of the Camtrap DP model. Included to showcase that extending with more resources is possible.\",\n \"data\": [\n {\n \"id\": 1,\n \"individualName\": \"Reinaert\",\n \"scientificName\": \"Vulpes vulpes\"\n }\n ]\n }\n ],\n \"profile\": \"https://raw.githubusercontent.com/tdwg/camtrap-dp/1.0.2/camtrap-dp-profile.json\",\n \"name\": \"camtrap-dp-example-dataset\",\n \"id\": \"7cca70f5-ef8c-4f86-85fb-8f070937d7ab\",\n \"created\": \"2023-02-06T11:23:03Z\",\n \"title\": \"Sample from: MICA - Muskrat and coypu camera trap observations in Belgium, the Netherlands and Germany\",\n \"contributors\": [\n {\n \"title\": \"Axel Neukermans\",\n \"email\": \"axel.neukermans@inbo.be\",\n \"path\": \"https://orcid.org/0000-0003-0272-9180\",\n \"role\": \"contributor\",\n \"organization\": \"Research Institute for Nature and Forest (INBO)\"\n },\n {\n \"title\": \"Danny Van der beeck\",\n \"email\": \"daniel.vanderbeeck@gmail.com\"\n },\n {\n \"title\": \"Emma Cartuyvels\",\n \"email\": \"emma.cartuyvels@inbo.be\",\n \"role\": \"principalInvestigator\",\n \"organization\": \"Research Institute for Nature and Forest (INBO)\"\n },\n {\n \"title\": \"Peter Desmet\",\n \"email\": \"peter.desmet@inbo.be\",\n \"path\": \"https://orcid.org/0000-0002-8442-8025\",\n \"role\": \"contact\",\n \"organization\": \"Research Institute for Nature and Forest (INBO)\"\n },\n {\n \"title\": \"Research Institute for Nature and Forest (INBO)\",\n \"path\": \"https://inbo.be\",\n \"role\": \"rightsHolder\"\n },\n {\n \"title\": \"Research Institute for Nature and Forest (INBO)\",\n \"path\": \"https://inbo.be\",\n \"role\": \"publisher\"\n }\n ],\n \"description\": \"MICA - Muskrat and coypu camera trap observations in Belgium, the Netherlands and Germany is an occurrence dataset published by the Research Institute of Nature and Forest (INBO). It is part of the LIFE project MICA, in which innovative techniques are tested for a more efficient control of muskrat and coypu populations, both invasive species. This dataset is a sample of the original dataset and serves as an example of a Camera Trap Data Package (Camtrap DP).\",\n \"version\": \"1.0.2\",\n \"keywords\": [\n \"camera traps\",\n \"public awareness campaign\",\n \"flood protection\",\n \"flood control\",\n \"damage prevention\",\n \"animal damage\",\n \"pest control\",\n \"invasive alien species\",\n \"muskrat\",\n \"coypu\"\n ],\n \"image\": \"\",\n \"homepage\": \"https://camtrap-dp.tdwg.org/example/\",\n \"sources\": [\n {\n \"title\": \"Agouti\",\n \"path\": \"https://www.agouti.eu\",\n \"email\": \"agouti@wur.nl\",\n \"version\": \"v3.21\"\n }\n ],\n \"licenses\": [\n {\n \"name\": \"CC0-1.0\",\n \"scope\": \"data\"\n },\n {\n \"path\": \"http://creativecommons.org/licenses/by/4.0/\",\n \"scope\": \"media\"\n }\n ],\n \"bibliographicCitation\": \"Desmet P, Neukermans A, Van der beeck D, Cartuyvels E (2022). Sample from: MICA - Muskrat and coypu camera trap observations in Belgium, the Netherlands and Germany. Version 1.0.2. Research Institute for Nature and Forest (INBO). Dataset. https://camtrap-dp.tdwg.org/example/\",\n \"project\": {\n \"id\": \"86cabc14-d475-4439-98a7-e7b590bed60e\",\n \"title\": \"Management of Invasive Coypu and muskrAt in Europe\",\n \"acronym\": \"MICA\",\n \"description\": \"Invasive alien species such as the coypu and muskrat pose a major threat to biodiversity and cost millions of euros annually. By feeding on rushes and reeds, these animals cause serious damage to the environment in which they live and endangered species suffer from habitat loss. The disappearance of reeds and digging in dikes represents a safety risk for humans in the lowland areas. With the LIFE project MICA (<https://lifemica.eu>), the partners from the participating countries want to develop a transnational plan for the management of coypu and muskrat populations in Europe and aim to reduce their population. The objective of an effective population control of coypu and muskrat is to protect lowlands from flooding, to prevent crop damage and loss of biodiversity. The objective of the project is to serve as a pilot and demonstration project in which ‘best practices’ are tested and new techniques are developed for a more efficient control of muskrat and coypu populations. By involving organisations from Belgium, Germany and the Netherlands, the project also promotes international cooperation and knowledge exchange in the field of muskrat and coypu management.\",\n \"samplingDesign\": \"targeted\",\n \"path\": \"https://lifemica.eu\",\n \"captureMethod\": [\n \"activityDetection\",\n \"timeLapse\"\n ],\n \"individualAnimals\": false,\n \"observationLevel\": [\n \"media\",\n \"event\"\n ]\n },\n \"coordinatePrecision\": 0.001,\n \"spatial\": {\n \"type\": \"Polygon\",\n \"coordinates\": [\n [\n [\n 4.013,\n 50.699\n ],\n [\n 5.659,\n 50.699\n ],\n [\n 5.659,\n 51.496\n ],\n [\n 4.013,\n 51.496\n ],\n [\n 4.013,\n 50.699\n ]\n ]\n ]\n },\n \"temporal\": {\n \"start\": \"2020-05-30\",\n \"end\": \"2021-04-18\"\n },\n \"taxonomic\": [\n {\n \"scientificName\": \"Anas platyrhynchos\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/DGP6\",\n \"taxonRank\": \"species\",\n \"vernacularNames\": {\n \"eng\": \"mallard\",\n \"nld\": \"wilde eend\"\n }\n },\n {\n \"scientificName\": \"Anas strepera\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/DGPL\",\n \"taxonRank\": \"species\",\n \"vernacularNames\": {\n \"eng\": \"gadwall\",\n \"nld\": \"krakeend\"\n }\n },\n {\n \"scientificName\": \"Ardea\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/32FH\",\n \"taxonRank\": \"genus\",\n \"vernacularNames\": {\n \"eng\": \"great herons\",\n \"nld\": \"reigers\"\n }\n },\n {\n \"scientificName\": \"Ardea cinerea\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/GCHS\",\n \"taxonRank\": \"species\",\n \"vernacularNames\": {\n \"eng\": \"grey heron\",\n \"nld\": \"blauwe reiger\"\n }\n },\n {\n \"scientificName\": \"Aves\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/V2\",\n \"taxonRank\": \"class\",\n \"vernacularNames\": {\n \"eng\": \"bird sp.\",\n \"nld\": \"vogel\"\n }\n },\n {\n \"scientificName\": \"Homo sapiens\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/6MB3T\",\n \"taxonRank\": \"species\",\n \"vernacularNames\": {\n \"eng\": \"human\",\n \"nld\": \"mens\"\n }\n },\n {\n \"scientificName\": \"Martes foina\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/3Y9VW\",\n \"taxonRank\": \"species\",\n \"vernacularNames\": {\n \"eng\": \"beech marten\",\n \"nld\": \"steenmarter\"\n }\n },\n {\n \"scientificName\": \"Mustela putorius\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/44QYC\",\n \"taxonRank\": \"species\",\n \"vernacularNames\": {\n \"eng\": \"European polecat\",\n \"nld\": \"bunzing\"\n }\n },\n {\n \"scientificName\": \"Rattus norvegicus\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/4RM67\",\n \"taxonRank\": \"species\",\n \"vernacularNames\": {\n \"eng\": \"brown rat\",\n \"nld\": \"bruine rat\"\n }\n },\n {\n \"scientificName\": \"Vulpes vulpes\",\n \"taxonID\": \"https://www.checklistbank.org/dataset/COL2023/taxon/5BSG3\",\n \"taxonRank\": \"species\",\n \"vernacularNames\": {\n \"eng\": \"red fox\",\n \"nld\": \"vos\"\n }\n }\n ],\n \"relatedIdentifiers\": [\n {\n \"relationType\": \"IsDerivedFrom\",\n \"relatedIdentifier\": \"https://doi.org/10.15468/5tb6ze\",\n \"resourceTypeGeneral\": \"Dataset\",\n \"relatedIdentifierType\": \"DOI\"\n },\n {\n \"relationType\": \"IsSupplementTo\",\n \"relatedIdentifier\": \"https://inbo.github.io/camtrapdp/\",\n \"resourceTypeGeneral\": \"Software\",\n \"relatedIdentifierType\": \"URL\"\n }\n ],\n \"references\": [ ]\n}\n"
},
"cookies": [],
"headers": [
{
"name": "accept-ranges",
"value": "bytes"
},
{
"name": "access-control-allow-origin",
"value": "*"
},
{
"name": "cache-control",
"value": "max-age=300"
},
{
"name": "connection",
"value": "keep-alive"
},
{
"name": "content-encoding",
"value": "gzip"
},
{
"name": "content-length",
"value": "2603"
},
{
"name": "content-security-policy",
"value": "default-src 'none'; style-src 'unsafe-inline'; sandbox"
},
{
"name": "content-type",
"value": "text/plain; charset=utf-8"
},
{
"name": "cross-origin-resource-policy",
"value": "cross-origin"
},
{
"name": "date",
"value": "Sat, 27 Sep 2025 09:29:45 GMT"
},
{
"name": "etag",
"value": "W/\"568d81aa4b4f1148ac5358387a8367b5853f76cb76dfb282d001809dbe43e173\""
},
{
"name": "expires",
"value": "Sat, 27 Sep 2025 09:34:45 GMT"
},
{
"name": "source-age",
"value": "223"
},
{
"name": "strict-transport-security",
"value": "max-age=31536000"
},
{
"name": "vary",
"value": "Authorization,Accept-Encoding"
},
{
"name": "via",
"value": "1.1 varnish"
},
{
"name": "x-cache",
"value": "HIT"
},
{
"name": "x-cache-hits",
"value": "1"
},
{
"name": "x-content-type-options",
"value": "nosniff"
},
{
"name": "x-fastly-request-id",
"value": "992bc82874a7e7d37bbff9f4838c35e697fc63de"
},
{
"name": "x-frame-options",
"value": "deny"
},
{
"name": "x-github-request-id",
"value": "5F1A:36A173:B08AEF:CF6618:68D79B2B"
},
{
"name": "x-served-by",
"value": "cache-lis1490053-LIS"
},
{
"name": "x-timer",
"value": "S1758965386.503800,VS0,VE1"
},
{
"name": "x-xss-protection",
"value": "1; mode=block"
}
],
"headersSize": 902,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2025-09-27T09:29:45.355Z",
"time": 184,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 184
}
}
],
"pages": [],
"version": "1.2"
}
}
27 changes: 20 additions & 7 deletions core/package/validate.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useRecording } from "@dpkit/test"
import { describe, expect, it } from "vitest"
import { loadDescriptor } from "../general/index.ts"
import { validatePackageDescriptor } from "./validate.ts"

useRecording()

describe("validatePackageDescriptor", () => {
it("returns valid result for valid package", async () => {
const descriptor = {
Expand All @@ -13,10 +17,10 @@ describe("validatePackageDescriptor", () => {
],
}

const result = await validatePackageDescriptor(descriptor)
const { valid, errors } = await validatePackageDescriptor(descriptor)

expect(result.valid).toBe(true)
expect(result.errors).toEqual([])
expect(valid).toBe(true)
expect(errors).toEqual([])
})

it("returns validation errors for invalid package", async () => {
Expand All @@ -25,15 +29,24 @@ describe("validatePackageDescriptor", () => {
resources: "not-an-array", // Should be an array
}

const result = await validatePackageDescriptor(descriptor)
const { valid, errors } = await validatePackageDescriptor(descriptor)

expect(result.valid).toBe(false)
expect(result.errors.length).toBeGreaterThan(0)
expect(valid).toBe(false)
expect(errors.length).toBeGreaterThan(0)

const error = result.errors[0]
const error = errors[0]
expect(error).toBeDefined()
if (error) {
expect(error.keyword).toBe("type")
}
})

it("should validate camtrap dp (#144)", async () => {
const { descriptor } = await loadDescriptor(
"https://raw.githubusercontent.com/tdwg/camtrap-dp/refs/tags/1.0.2/example/datapackage.json",
)

const { valid } = await validatePackageDescriptor(descriptor)
expect(valid).toBe(true)
})
})
11 changes: 10 additions & 1 deletion core/resource/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { normalizeDialect } from "../dialect/index.ts"
import { isDescriptor, normalizePath } from "../general/index.ts"
import type { Descriptor } from "../general/index.ts"
import { isRemotePath } from "../general/index.ts"
import { normalizeSchema } from "../schema/index.ts"

export function normalizeResource(
Expand All @@ -23,7 +24,15 @@ export function normalizeResource(
}

function normalizeProfile(descriptor: Descriptor) {
descriptor.$schema = descriptor.$schema ?? descriptor.profile
// We can ignore data-package/data-resource/tabular-data-package/tabular-data-resource
// special value profiles because they are already incorporated into the dpkit's built-in profiles
// https://specs.frictionlessdata.io/profiles/#profile-property
const remoteProfile =
typeof descriptor.profile === "string" && isRemotePath(descriptor.profile)
? descriptor.profile
: undefined

descriptor.$schema = descriptor.$schema ?? remoteProfile
}

function normalizeUrl(descriptor: Descriptor) {
Expand Down
3 changes: 2 additions & 1 deletion dpkit/commands/dialect/explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export const exploreDialectCommand = new Command("explore")
)

if (!dialect || isEmptyObject(dialect)) {
Session.terminate("Dialect is not available")
session.terminate("Dialect is not available")
process.exit(1) // typescript ignore never return type above
}

await session.render(dialect, <DialectGrid dialect={dialect} />)
Expand Down
3 changes: 2 additions & 1 deletion dpkit/commands/dialect/infer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export const inferDialectCommand = new Command("infer")
)

if (isEmptyObject(dialect)) {
Session.terminate("Could not infer dialect")
session.terminate("Could not infer dialect")
process.exit(1) // typescript ignore never return type above
}

await session.render(dialect, <DialectGrid dialect={dialect} />)
Expand Down
3 changes: 2 additions & 1 deletion dpkit/commands/dialect/script.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export const scriptDialectCommand = new Command("script")
)

if (!dialect || isEmptyObject(dialect)) {
Session.terminate("Dialect is not available")
session.terminate("Dialect is not available")
process.exit(1) // typescript ignore never return type above
}

console.log(
Expand Down
8 changes: 6 additions & 2 deletions dpkit/commands/dialect/validate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ export const validateDialectCommand = new Command("validate")
.addOption(params.json)
.addOption(params.debug)
.addOption(params.quit)
.addOption(params.all)

.action(async (path, options) => {
const session = Session.create({
title: "Validate dialect",
json: options.json,
debug: options.debug,
quit: options.quit,
all: options.all,
})

const resource: Partial<Resource> | undefined = !path
Expand All @@ -46,15 +49,16 @@ export const validateDialectCommand = new Command("validate")
: { descriptor: undefined }

if (!descriptor) {
Session.terminate("Dialect is not available")
session.terminate("Dialect is not available")
process.exit(1)
}

const report = await session.task(
"Validating dialect",
validateDialect(descriptor),
)

if (report.errors.length && !options.quit) {
if (report.errors.length) {
const type = await selectErrorType(session, report.errors)
if (type) report.errors = report.errors.filter(e => e.type === type)
}
Expand Down
3 changes: 2 additions & 1 deletion dpkit/commands/file/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export const copyFileCommand = new Command("copy")
const resource = await selectResource(session, options)

if (typeof resource.path !== "string") {
Session.terminate("Only single file resources are supported")
session.terminate("Only single file resources are supported")
process.exit(1) // typescript ignore never return type above
}

path = resource.path
Expand Down
3 changes: 2 additions & 1 deletion dpkit/commands/file/describe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const describeFileCommand = new Command("describe")
const resource = await selectResource(session, options)

if (typeof resource.path !== "string") {
Session.terminate("Only single file resources are supported")
session.terminate("Only single file resources are supported")
process.exit(1) // typescript ignore never return type above
}

path = resource.path
Expand Down
Loading
Loading