Add validator lib

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-11-03 08:19:09 +01:00
committed by Sacha Al Himdani
parent 288c59a5f2
commit f9216d30b2
102 changed files with 7409 additions and 1068 deletions

View File

@@ -1,7 +1,7 @@
import { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui";
import type {
ProcessingActivitySpecialOrCriminalData,
ProcessingActivitySpecialOrCriminalDatum,
ProcessingActivityLawfulBasis,
ProcessingActivityDataProtectionImpactAssessment,
ProcessingActivityTransferImpactAssessment,
@@ -11,7 +11,7 @@ export function SpecialOrCriminalDataOptions() {
const { __ } = useTranslate();
const options: Array<{
value: ProcessingActivitySpecialOrCriminalData;
value: ProcessingActivitySpecialOrCriminalDatum;
label: string;
}> = [
{ value: "YES", label: __("Yes") },
@@ -56,16 +56,19 @@ export function LawfulBasisOptions() {
);
}
export function getLawfulBasisLabel(value: ProcessingActivityLawfulBasis | null | undefined, __: (key: string) => string): string {
export function getLawfulBasisLabel(
value: ProcessingActivityLawfulBasis | null | undefined,
__: (key: string) => string
): string {
if (!value) return "-";
const labels = {
"CONSENT": __("Consent"),
"CONTRACTUAL_NECESSITY": __("Contractual Necessity"),
"LEGAL_OBLIGATION": __("Legal Obligation"),
"LEGITIMATE_INTEREST": __("Legitimate Interest"),
"PUBLIC_TASK": __("Public Task"),
"VITAL_INTERESTS": __("Vital Interests"),
CONSENT: __("Consent"),
CONTRACTUAL_NECESSITY: __("Contractual Necessity"),
LEGAL_OBLIGATION: __("Legal Obligation"),
LEGITIMATE_INTEREST: __("Legitimate Interest"),
PUBLIC_TASK: __("Public Task"),
VITAL_INTERESTS: __("Vital Interests"),
};
return labels[value] || value;
@@ -79,12 +82,18 @@ export function TransferSafeguardsOptions() {
label: string;
}> = [
{ value: "__NONE__", label: __("None") },
{ value: "STANDARD_CONTRACTUAL_CLAUSES", label: __("Standard Contractual Clauses") },
{
value: "STANDARD_CONTRACTUAL_CLAUSES",
label: __("Standard Contractual Clauses"),
},
{ value: "BINDING_CORPORATE_RULES", label: __("Binding Corporate Rules") },
{ value: "ADEQUACY_DECISION", label: __("Adequacy Decision") },
{ value: "DEROGATIONS", label: __("Derogations") },
{ value: "CODES_OF_CONDUCT", label: __("Codes of Conduct") },
{ value: "CERTIFICATION_MECHANISMS", label: __("Certification Mechanisms") },
{
value: "CERTIFICATION_MECHANISMS",
label: __("Certification Mechanisms"),
},
];
return (

View File

@@ -107,6 +107,14 @@ export function useVendorForm(vendorKey: useVendorFormFragment$key) {
id: vendor.id,
...data,
description: data.description || null,
statusPageUrl: data.statusPageUrl || null,
termsOfServiceUrl: data.termsOfServiceUrl || null,
privacyPolicyUrl: data.privacyPolicyUrl || null,
serviceLevelAgreementUrl: data.serviceLevelAgreementUrl || null,
dataProcessingAgreementUrl: data.dataProcessingAgreementUrl || null,
websiteUrl: data.websiteUrl || null,
securityPageUrl: data.securityPageUrl || null,
trustPageUrl: data.trustPageUrl || null,
},
},
}).then(() => {

View File

@@ -3,6 +3,7 @@ import { useMutation } from "react-relay";
import { useConfirm } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { promisifyMutation, sprintf } from "@probo/helpers";
import { useMutationWithToasts } from "../useMutationWithToasts";
export const assetsQuery = graphql`
query AssetGraphListQuery($organizationId: ID!, $snapshotId: ID) {
@@ -197,8 +198,11 @@ export const useCreateAsset = (connectionId: string) => {
};
export const useUpdateAsset = () => {
const [mutate] = useMutation(updateAssetMutation);
const { __ } = useTranslate();
const [mutate] = useMutationWithToasts(updateAssetMutation, {
successMessage: __("Asset updated successfully"),
errorMessage: __("Failed to update asset"),
});
return (input: {
id: string;
@@ -213,7 +217,7 @@ export const useUpdateAsset = () => {
return alert(__("Failed to update asset: asset ID is required"));
}
return promisifyMutation(mutate)({
return mutate({
variables: {
input,
},

View File

@@ -151,7 +151,7 @@ export const useCreateAudit = (connectionId: string) => {
return (input: {
organizationId: string;
frameworkId: string;
name?: string;
name?: string | null;
validFrom?: string;
validUntil?: string;
reportKey?: string;
@@ -187,7 +187,7 @@ export const useUpdateAudit = () => {
return (input: {
id: string;
name?: string;
name?: string | null;
validFrom?: string | null;
validUntil?: string | null;
state?: string;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<d01ca9f47a4ce6e732cffd5deb9150c6>>
* @generated SignedSource<<a804557b48f2dbc4749c8399e708b9fe>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,9 +11,9 @@
import { ConcreteRequest } from 'relay-runtime';
export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS";
export type ProcessingActivitySpecialOrCriminalData = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivitySpecialOrCriminalDatum = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type ProcessingActivityTransferSafeguard = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type CreateProcessingActivityInput = {
consentEvidenceLink?: string | null | undefined;
dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment;
@@ -28,9 +28,9 @@ export type CreateProcessingActivityInput = {
recipients?: string | null | undefined;
retentionPeriod?: string | null | undefined;
securityMeasures?: string | null | undefined;
specialOrCriminalData: ProcessingActivitySpecialOrCriminalData;
specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum;
transferImpactAssessment: ProcessingActivityTransferImpactAssessment;
transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined;
transferSafeguards?: ProcessingActivityTransferSafeguard | null | undefined;
vendorIds?: ReadonlyArray<string> | null | undefined;
};
export type ProcessingActivityGraphCreateMutation$variables = {
@@ -55,9 +55,9 @@ export type ProcessingActivityGraphCreateMutation$data = {
readonly recipients: string | null | undefined;
readonly retentionPeriod: string | null | undefined;
readonly securityMeasures: string | null | undefined;
readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalData;
readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum;
readonly transferImpactAssessment: ProcessingActivityTransferImpactAssessment;
readonly transferSafeguards: ProcessingActivityTransferSafeguards | null | undefined;
readonly transferSafeguards: ProcessingActivityTransferSafeguard | null | undefined;
readonly vendors: {
readonly edges: ReadonlyArray<{
readonly node: {

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5dea7ccaf98a1223d9e4cc1ecdfbb0bc>>
* @generated SignedSource<<736925258373b1116c286fc16d19ad7d>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,9 +11,9 @@
import { ConcreteRequest } from 'relay-runtime';
export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS";
export type ProcessingActivitySpecialOrCriminalData = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivitySpecialOrCriminalDatum = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type ProcessingActivityTransferSafeguard = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL";
export type ProcessingActivityGraphNodeQuery$variables = {
processingActivityId: string;
@@ -39,9 +39,9 @@ export type ProcessingActivityGraphNodeQuery$data = {
readonly retentionPeriod?: string | null | undefined;
readonly securityMeasures?: string | null | undefined;
readonly snapshotId?: string | null | undefined;
readonly specialOrCriminalData?: ProcessingActivitySpecialOrCriminalData;
readonly specialOrCriminalData?: ProcessingActivitySpecialOrCriminalDatum;
readonly transferImpactAssessment?: ProcessingActivityTransferImpactAssessment;
readonly transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined;
readonly transferSafeguards?: ProcessingActivityTransferSafeguard | null | undefined;
readonly updatedAt?: any;
readonly vendors?: {
readonly edges: ReadonlyArray<{

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<08099a83039838c6c79dbf1ca7c99034>>
* @generated SignedSource<<7004a9f42c1d7e16eadf8adbdb613b2a>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,9 +11,9 @@
import { ConcreteRequest } from 'relay-runtime';
export type ProcessingActivityDataProtectionImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityLawfulBasis = "CONSENT" | "CONTRACTUAL_NECESSITY" | "LEGAL_OBLIGATION" | "LEGITIMATE_INTEREST" | "PUBLIC_TASK" | "VITAL_INTERESTS";
export type ProcessingActivitySpecialOrCriminalData = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivitySpecialOrCriminalDatum = "NO" | "POSSIBLE" | "YES";
export type ProcessingActivityTransferImpactAssessment = "NEEDED" | "NOT_NEEDED";
export type ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type ProcessingActivityTransferSafeguard = "ADEQUACY_DECISION" | "BINDING_CORPORATE_RULES" | "CERTIFICATION_MECHANISMS" | "CODES_OF_CONDUCT" | "DEROGATIONS" | "STANDARD_CONTRACTUAL_CLAUSES";
export type UpdateProcessingActivityInput = {
consentEvidenceLink?: string | null | undefined;
dataProtectionImpactAssessment?: ProcessingActivityDataProtectionImpactAssessment | null | undefined;
@@ -28,9 +28,9 @@ export type UpdateProcessingActivityInput = {
recipients?: string | null | undefined;
retentionPeriod?: string | null | undefined;
securityMeasures?: string | null | undefined;
specialOrCriminalData?: ProcessingActivitySpecialOrCriminalData | null | undefined;
specialOrCriminalData?: ProcessingActivitySpecialOrCriminalDatum | null | undefined;
transferImpactAssessment?: ProcessingActivityTransferImpactAssessment | null | undefined;
transferSafeguards?: ProcessingActivityTransferSafeguards | null | undefined;
transferSafeguards?: ProcessingActivityTransferSafeguard | null | undefined;
vendorIds?: ReadonlyArray<string> | null | undefined;
};
export type ProcessingActivityGraphUpdateMutation$variables = {
@@ -52,9 +52,9 @@ export type ProcessingActivityGraphUpdateMutation$data = {
readonly recipients: string | null | undefined;
readonly retentionPeriod: string | null | undefined;
readonly securityMeasures: string | null | undefined;
readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalData;
readonly specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum;
readonly transferImpactAssessment: ProcessingActivityTransferImpactAssessment;
readonly transferSafeguards: ProcessingActivityTransferSafeguards | null | undefined;
readonly transferSafeguards: ProcessingActivityTransferSafeguard | null | undefined;
readonly updatedAt: any;
readonly vendors: {
readonly edges: ReadonlyArray<{

View File

@@ -80,15 +80,11 @@ export default function AssetDetailsPage(props: Props) {
const updateAsset = useUpdateAsset();
const onSubmit = handleSubmit(async (formData) => {
try {
await updateAsset({
id: assetEntry?.id,
...formData,
});
reset(formData);
} catch (error) {
console.error("Failed to update asset:", error);
}
await updateAsset({
id: assetEntry?.id,
...formData,
});
reset(formData);
});
const breadcrumbAssetsUrl = isSnapshotMode && snapshotId

View File

@@ -36,7 +36,7 @@ import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprint
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
const updateAuditSchema = z.object({
name: z.string().optional(),
name: z.string().nullable().optional(),
validFrom: z.string().optional(),
validUntil: z.string().optional(),
state: z.enum(["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "REJECTED", "OUTDATED"]),
@@ -63,7 +63,7 @@ export default function AuditDetailsPage(props: Props) {
const { control, formState, handleSubmit, register, reset } = useFormWithSchema(updateAuditSchema, {
defaultValues: {
name: auditEntry.name || "",
name: auditEntry.name || null,
validFrom: auditEntry.validFrom?.split('T')[0] || "",
validUntil: auditEntry.validUntil?.split('T')[0] || "",
state: auditEntry.state || "NOT_STARTED",
@@ -82,7 +82,7 @@ export default function AuditDetailsPage(props: Props) {
try {
await updateAudit({
id: auditEntry.id,
name: formData.name,
name: formData.name || null,
validFrom: formatDatetime(formData.validFrom) ?? null,
validUntil: formatDatetime(formData.validUntil) ?? null,
state: formData.state,

View File

@@ -79,7 +79,7 @@ export function CreateAuditDialog({
await createAudit({
organizationId,
frameworkId: data.frameworkId,
name: data.name,
name: data.name || null,
validFrom: formatDatetime(data.validFrom),
validUntil: formatDatetime(data.validUntil),
state: data.state,

View File

@@ -28,6 +28,13 @@ const (
AssetTypeVirtual AssetType = "VIRTUAL"
)
func AssetTypes() []AssetType {
return []AssetType{
AssetTypePhysical,
AssetTypeVirtual,
}
}
func (at AssetType) MarshalText() ([]byte, error) {
return []byte(at.String()), nil
}

View File

@@ -29,6 +29,16 @@ const (
AuditStateOutdated AuditState = "OUTDATED"
)
func AuditStates() []AuditState {
return []AuditState{
AuditStateNotStarted,
AuditStateInProgress,
AuditStateCompleted,
AuditStateRejected,
AuditStateOutdated,
}
}
func (as AuditState) String() string {
return string(as)
}

View File

@@ -29,6 +29,15 @@ const (
BusinessImpactCritical BusinessImpact = "CRITICAL"
)
func BusinessImpacts() []BusinessImpact {
return []BusinessImpact{
BusinessImpactLow,
BusinessImpactMedium,
BusinessImpactHigh,
BusinessImpactCritical,
}
}
func (i BusinessImpact) String() string {
return string(i)
}

View File

@@ -25,6 +25,12 @@ const (
ConnectorProtocolOAuth2 ConnectorProtocol = "OAUTH2"
)
func ConnectorProtocols() []ConnectorProtocol {
return []ConnectorProtocol{
ConnectorProtocolOAuth2,
}
}
func (cp ConnectorProtocol) String() string {
return string(cp)
}

View File

@@ -25,6 +25,12 @@ const (
ConnectorProviderSlack ConnectorProvider = "SLACK"
)
func ConnectorProviders() []ConnectorProvider {
return []ConnectorProvider{
ConnectorProviderSlack,
}
}
func (cp ConnectorProvider) String() string {
return string(cp)
}

View File

@@ -27,6 +27,14 @@ const (
ContinualImprovementPriorityHigh ContinualImprovementPriority = "HIGH"
)
func ContinualImprovementPriorities() []ContinualImprovementPriority {
return []ContinualImprovementPriority{
ContinualImprovementPriorityLow,
ContinualImprovementPriorityMedium,
ContinualImprovementPriorityHigh,
}
}
func (cip ContinualImprovementPriority) String() string {
return string(cip)
}

View File

@@ -27,6 +27,14 @@ const (
ContinualImprovementStatusClosed ContinualImprovementStatus = "CLOSED"
)
func ContinualImprovementStatuses() []ContinualImprovementStatus {
return []ContinualImprovementStatus{
ContinualImprovementStatusOpen,
ContinualImprovementStatusInProgress,
ContinualImprovementStatusClosed,
}
}
func (cis ContinualImprovementStatus) String() string {
return string(cis)
}

View File

@@ -26,6 +26,13 @@ const (
ControlStatusExcluded ControlStatus = "EXCLUDED"
)
func ControlStatuses() []ControlStatus {
return []ControlStatus{
ControlStatusIncluded,
ControlStatusExcluded,
}
}
func (cs ControlStatus) String() string {
return string(cs)
}

View File

@@ -22,3 +22,12 @@ const (
DataClassificationConfidential DataClassification = "CONFIDENTIAL"
DataClassificationSecret DataClassification = "SECRET"
)
func DataClassifications() []DataClassification {
return []DataClassification{
DataClassificationPublic,
DataClassificationInternal,
DataClassificationConfidential,
DataClassificationSecret,
}
}

View File

@@ -30,6 +30,16 @@ const (
DataSensitivityCritical DataSensitivity = "CRITICAL"
)
func DataSensitivities() []DataSensitivity {
return []DataSensitivity{
DataSensitivityNone,
DataSensitivityLow,
DataSensitivityMedium,
DataSensitivityHigh,
DataSensitivityCritical,
}
}
func (i DataSensitivity) String() string {
return string(i)
}

View File

@@ -14,6 +14,15 @@ const (
DocumentClassificationSecret DocumentClassification = "SECRET"
)
func DocumentClassifications() []DocumentClassification {
return []DocumentClassification{
DocumentClassificationPublic,
DocumentClassificationInternal,
DocumentClassificationConfidential,
DocumentClassificationSecret,
}
}
func (dc DocumentClassification) String() string {
switch dc {
case DocumentClassificationPublic:

View File

@@ -30,6 +30,15 @@ const (
DocumentTypeProcedure DocumentType = "PROCEDURE"
)
func DocumentTypes() []DocumentType {
return []DocumentType{
DocumentTypeOther,
DocumentTypeISMS,
DocumentTypePolicy,
DocumentTypeProcedure,
}
}
func (dt DocumentType) MarshalText() ([]byte, error) {
return []byte(dt.String()), nil
}

View File

@@ -21,49 +21,49 @@ var (
)
const (
OrganizationEntityType uint16 = iota
FrameworkEntityType
MeasureEntityType
TaskEntityType
EvidenceEntityType
ConnectorEntityType
VendorRiskAssessmentEntityType
VendorEntityType
PeopleEntityType
VendorComplianceReportEntityType
DocumentEntityType
UserEntityType
SessionEntityType
EmailEntityType
ControlEntityType
RiskEntityType
DocumentVersionEntityType
DocumentVersionSignatureEntityType
AssetEntityType
DatumEntityType
AuditEntityType
ReportEntityType
TrustCenterEntityType
TrustCenterAccessEntityType
VendorBusinessAssociateAgreementEntityType
FileEntityType
VendorContactEntityType
VendorDataPrivacyAgreementEntityType
NonconformityEntityType
ObligationEntityType
VendorServiceEntityType
SnapshotEntityType
ContinualImprovementEntityType
ProcessingActivityEntityType
ExportJobEntityType
TrustCenterReferenceEntityType
TrustCenterDocumentAccessEntityType
CustomDomainEntityType
InvitationEntityType
MembershipEntityType
SlackMessageEntityType
TrustCenterFileEntityType
SAMLConfigurationEntityType
UserAPIKeyEntityType
UserAPIKeyMembershipEntityType
OrganizationEntityType uint16 = 0
FrameworkEntityType uint16 = 1
MeasureEntityType uint16 = 2
TaskEntityType uint16 = 3
EvidenceEntityType uint16 = 4
ConnectorEntityType uint16 = 5
VendorRiskAssessmentEntityType uint16 = 6
VendorEntityType uint16 = 7
PeopleEntityType uint16 = 8
VendorComplianceReportEntityType uint16 = 9
DocumentEntityType uint16 = 10
UserEntityType uint16 = 11
SessionEntityType uint16 = 12
EmailEntityType uint16 = 13
ControlEntityType uint16 = 14
RiskEntityType uint16 = 15
DocumentVersionEntityType uint16 = 16
DocumentVersionSignatureEntityType uint16 = 17
AssetEntityType uint16 = 18
DatumEntityType uint16 = 19
AuditEntityType uint16 = 20
ReportEntityType uint16 = 21
TrustCenterEntityType uint16 = 22
TrustCenterAccessEntityType uint16 = 23
VendorBusinessAssociateAgreementEntityType uint16 = 24
FileEntityType uint16 = 25
VendorContactEntityType uint16 = 26
VendorDataPrivacyAgreementEntityType uint16 = 27
NonconformityEntityType uint16 = 28
ObligationEntityType uint16 = 29
VendorServiceEntityType uint16 = 30
SnapshotEntityType uint16 = 31
ContinualImprovementEntityType uint16 = 32
ProcessingActivityEntityType uint16 = 33
ExportJobEntityType uint16 = 34
TrustCenterReferenceEntityType uint16 = 35
TrustCenterDocumentAccessEntityType uint16 = 36
CustomDomainEntityType uint16 = 37
InvitationEntityType uint16 = 38
MembershipEntityType uint16 = 39
SlackMessageEntityType uint16 = 40
TrustCenterFileEntityType uint16 = 41
SAMLConfigurationEntityType uint16 = 42
UserAPIKeyEntityType uint16 = 43
UserAPIKeyMembershipEntityType uint16 = 44
)

View File

@@ -30,6 +30,15 @@ const (
MeasureStateImplemented
)
func MeasureStates() []MeasureState {
return []MeasureState{
MeasureStateNotStarted,
MeasureStateInProgress,
MeasureStateNotApplicable,
MeasureStateImplemented,
}
}
func (ms MeasureState) MarshalText() ([]byte, error) {
return []byte(ms.String()), nil
}

View File

@@ -27,6 +27,14 @@ const (
NonconformityStatusClosed NonconformityStatus = "CLOSED"
)
func NonconformityStatuses() []NonconformityStatus {
return []NonconformityStatus{
NonconformityStatusOpen,
NonconformityStatusInProgress,
NonconformityStatusClosed,
}
}
func (ncs NonconformityStatus) String() string {
return string(ncs)
}

View File

@@ -27,6 +27,14 @@ const (
ObligationStatusCompliant ObligationStatus = "COMPLIANT"
)
func ObligationStatuses() []ObligationStatus {
return []ObligationStatus{
ObligationStatusNonCompliant,
ObligationStatusPartiallyCompliant,
ObligationStatusCompliant,
}
}
func (os ObligationStatus) String() string {
return string(os)
}

View File

@@ -29,6 +29,14 @@ const (
PeopleKindServiceAccount
)
func PeopleKinds() []PeopleKind {
return []PeopleKind{
PeopleKindEmployee,
PeopleKindContractor,
PeopleKindServiceAccount,
}
}
func (ps PeopleKind) MarshalText() ([]byte, error) {
return []byte(ps.String()), nil
}

View File

@@ -20,10 +20,10 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
@@ -36,13 +36,13 @@ type (
Purpose *string `db:"purpose"`
DataSubjectCategory *string `db:"data_subject_category"`
PersonalDataCategory *string `db:"personal_data_category"`
SpecialOrCriminalData ProcessingActivitySpecialOrCriminalData `db:"special_or_criminal_data"`
SpecialOrCriminalData ProcessingActivitySpecialOrCriminalDatum `db:"special_or_criminal_data"`
ConsentEvidenceLink *string `db:"consent_evidence_link"`
LawfulBasis ProcessingActivityLawfulBasis `db:"lawful_basis"`
Recipients *string `db:"recipients"`
Location *string `db:"location"`
InternationalTransfers bool `db:"international_transfers"`
TransferSafeguards *ProcessingActivityTransferSafeguards `db:"transfer_safeguards"`
TransferSafeguard *ProcessingActivityTransferSafeguard `db:"transfer_safeguards"`
RetentionPeriod *string `db:"retention_period"`
SecurityMeasures *string `db:"security_measures"`
DataProtectionImpactAssessment ProcessingActivityDataProtectionImpactAssessment `db:"data_protection_impact_assessment"`
@@ -290,7 +290,7 @@ INSERT INTO processing_activities (
"recipients": p.Recipients,
"location": p.Location,
"international_transfers": p.InternationalTransfers,
"transfer_safeguards": p.TransferSafeguards,
"transfer_safeguards": p.TransferSafeguard,
"retention_period": p.RetentionPeriod,
"security_measures": p.SecurityMeasures,
"data_protection_impact_assessment": p.DataProtectionImpactAssessment,
@@ -351,7 +351,7 @@ WHERE
"recipients": p.Recipients,
"location": p.Location,
"international_transfers": p.InternationalTransfers,
"transfer_safeguards": p.TransferSafeguards,
"transfer_safeguards": p.TransferSafeguard,
"retention_period": p.RetentionPeriod,
"security_measures": p.SecurityMeasures,
"data_protection_impact_assessment": p.DataProtectionImpactAssessment,

View File

@@ -26,6 +26,13 @@ const (
ProcessingActivityDataProtectionImpactAssessmentNotNeeded ProcessingActivityDataProtectionImpactAssessment = "NOT_NEEDED"
)
func ProcessingActivityDataProtectionImpactAssessments() []ProcessingActivityDataProtectionImpactAssessment {
return []ProcessingActivityDataProtectionImpactAssessment{
ProcessingActivityDataProtectionImpactAssessmentNeeded,
ProcessingActivityDataProtectionImpactAssessmentNotNeeded,
}
}
func (p ProcessingActivityDataProtectionImpactAssessment) String() string {
return string(p)
}

View File

@@ -30,6 +30,17 @@ const (
ProcessingActivityLawfulBasisPublicTask ProcessingActivityLawfulBasis = "PUBLIC_TASK"
)
func ProcessingActivityLawfulBases() []ProcessingActivityLawfulBasis {
return []ProcessingActivityLawfulBasis{
ProcessingActivityLawfulBasisLegitimateInterest,
ProcessingActivityLawfulBasisConsent,
ProcessingActivityLawfulBasisContractualNecessity,
ProcessingActivityLawfulBasisLegalObligation,
ProcessingActivityLawfulBasisVitalInterests,
ProcessingActivityLawfulBasisPublicTask,
}
}
func (p ProcessingActivityLawfulBasis) String() string {
return string(p)
}

View File

@@ -19,19 +19,27 @@ import (
"fmt"
)
type ProcessingActivitySpecialOrCriminalData string
type ProcessingActivitySpecialOrCriminalDatum string
const (
ProcessingActivitySpecialOrCriminalDataYes ProcessingActivitySpecialOrCriminalData = "YES"
ProcessingActivitySpecialOrCriminalDataNo ProcessingActivitySpecialOrCriminalData = "NO"
ProcessingActivitySpecialOrCriminalDataPossible ProcessingActivitySpecialOrCriminalData = "POSSIBLE"
ProcessingActivitySpecialOrCriminalDatumYes ProcessingActivitySpecialOrCriminalDatum = "YES"
ProcessingActivitySpecialOrCriminalDatumNo ProcessingActivitySpecialOrCriminalDatum = "NO"
ProcessingActivitySpecialOrCriminalDatumPossible ProcessingActivitySpecialOrCriminalDatum = "POSSIBLE"
)
func (p ProcessingActivitySpecialOrCriminalData) String() string {
func ProcessingActivitySpecialOrCriminalData() []ProcessingActivitySpecialOrCriminalDatum {
return []ProcessingActivitySpecialOrCriminalDatum{
ProcessingActivitySpecialOrCriminalDatumYes,
ProcessingActivitySpecialOrCriminalDatumNo,
ProcessingActivitySpecialOrCriminalDatumPossible,
}
}
func (p ProcessingActivitySpecialOrCriminalDatum) String() string {
return string(p)
}
func (p *ProcessingActivitySpecialOrCriminalData) Scan(value any) error {
func (p *ProcessingActivitySpecialOrCriminalDatum) Scan(value any) error {
var s string
switch v := value.(type) {
case string:
@@ -39,22 +47,22 @@ func (p *ProcessingActivitySpecialOrCriminalData) Scan(value any) error {
case []byte:
s = string(v)
default:
return fmt.Errorf("unsupported type for ProcessingActivitySpecialOrCriminalData: %T", value)
return fmt.Errorf("unsupported type for ProcessingActivitySpecialOrCriminalDatum: %T", value)
}
switch s {
case "YES":
*p = ProcessingActivitySpecialOrCriminalDataYes
*p = ProcessingActivitySpecialOrCriminalDatumYes
case "NO":
*p = ProcessingActivitySpecialOrCriminalDataNo
*p = ProcessingActivitySpecialOrCriminalDatumNo
case "POSSIBLE":
*p = ProcessingActivitySpecialOrCriminalDataPossible
*p = ProcessingActivitySpecialOrCriminalDatumPossible
default:
return fmt.Errorf("invalid ProcessingActivitySpecialOrCriminalData value: %q", s)
return fmt.Errorf("invalid ProcessingActivitySpecialOrCriminalDatum value: %q", s)
}
return nil
}
func (p ProcessingActivitySpecialOrCriminalData) Value() (driver.Value, error) {
func (p ProcessingActivitySpecialOrCriminalDatum) Value() (driver.Value, error) {
return p.String(), nil
}

View File

@@ -26,6 +26,13 @@ const (
ProcessingActivityTransferImpactAssessmentNotNeeded ProcessingActivityTransferImpactAssessment = "NOT_NEEDED"
)
func ProcessingActivityTransferImpactAssessments() []ProcessingActivityTransferImpactAssessment {
return []ProcessingActivityTransferImpactAssessment{
ProcessingActivityTransferImpactAssessmentNeeded,
ProcessingActivityTransferImpactAssessmentNotNeeded,
}
}
func (p ProcessingActivityTransferImpactAssessment) String() string {
return string(p)
}

View File

@@ -19,22 +19,33 @@ import (
"fmt"
)
type ProcessingActivityTransferSafeguards string
type ProcessingActivityTransferSafeguard string
const (
ProcessingActivityTransferSafeguardsStandardContractualClauses ProcessingActivityTransferSafeguards = "STANDARD_CONTRACTUAL_CLAUSES"
ProcessingActivityTransferSafeguardsBindingCorporateRules ProcessingActivityTransferSafeguards = "BINDING_CORPORATE_RULES"
ProcessingActivityTransferSafeguardsAdequacyDecision ProcessingActivityTransferSafeguards = "ADEQUACY_DECISION"
ProcessingActivityTransferSafeguardsDerogations ProcessingActivityTransferSafeguards = "DEROGATIONS"
ProcessingActivityTransferSafeguardsCodesOfConduct ProcessingActivityTransferSafeguards = "CODES_OF_CONDUCT"
ProcessingActivityTransferSafeguardsCertificationMechanisms ProcessingActivityTransferSafeguards = "CERTIFICATION_MECHANISMS"
ProcessingActivityTransferSafeguardStandardContractualClauses ProcessingActivityTransferSafeguard = "STANDARD_CONTRACTUAL_CLAUSES"
ProcessingActivityTransferSafeguardBindingCorporateRules ProcessingActivityTransferSafeguard = "BINDING_CORPORATE_RULES"
ProcessingActivityTransferSafeguardAdequacyDecision ProcessingActivityTransferSafeguard = "ADEQUACY_DECISION"
ProcessingActivityTransferSafeguardDerogations ProcessingActivityTransferSafeguard = "DEROGATIONS"
ProcessingActivityTransferSafeguardCodesOfConduct ProcessingActivityTransferSafeguard = "CODES_OF_CONDUCT"
ProcessingActivityTransferSafeguardCertificationMechanisms ProcessingActivityTransferSafeguard = "CERTIFICATION_MECHANISMS"
)
func (p ProcessingActivityTransferSafeguards) String() string {
func ProcessingActivityTransferSafeguards() []ProcessingActivityTransferSafeguard {
return []ProcessingActivityTransferSafeguard{
ProcessingActivityTransferSafeguardStandardContractualClauses,
ProcessingActivityTransferSafeguardBindingCorporateRules,
ProcessingActivityTransferSafeguardAdequacyDecision,
ProcessingActivityTransferSafeguardDerogations,
ProcessingActivityTransferSafeguardCodesOfConduct,
ProcessingActivityTransferSafeguardCertificationMechanisms,
}
}
func (p ProcessingActivityTransferSafeguard) String() string {
return string(p)
}
func (p *ProcessingActivityTransferSafeguards) Scan(value any) error {
func (p *ProcessingActivityTransferSafeguard) Scan(value any) error {
var s string
switch v := value.(type) {
case string:
@@ -42,28 +53,28 @@ func (p *ProcessingActivityTransferSafeguards) Scan(value any) error {
case []byte:
s = string(v)
default:
return fmt.Errorf("unsupported type for ProcessingActivityTransferSafeguards: %T", value)
return fmt.Errorf("unsupported type for ProcessingActivityTransferSafeguard: %T", value)
}
switch s {
case "STANDARD_CONTRACTUAL_CLAUSES":
*p = ProcessingActivityTransferSafeguardsStandardContractualClauses
*p = ProcessingActivityTransferSafeguardStandardContractualClauses
case "BINDING_CORPORATE_RULES":
*p = ProcessingActivityTransferSafeguardsBindingCorporateRules
*p = ProcessingActivityTransferSafeguardBindingCorporateRules
case "ADEQUACY_DECISION":
*p = ProcessingActivityTransferSafeguardsAdequacyDecision
*p = ProcessingActivityTransferSafeguardAdequacyDecision
case "DEROGATIONS":
*p = ProcessingActivityTransferSafeguardsDerogations
*p = ProcessingActivityTransferSafeguardDerogations
case "CODES_OF_CONDUCT":
*p = ProcessingActivityTransferSafeguardsCodesOfConduct
*p = ProcessingActivityTransferSafeguardCodesOfConduct
case "CERTIFICATION_MECHANISMS":
*p = ProcessingActivityTransferSafeguardsCertificationMechanisms
*p = ProcessingActivityTransferSafeguardCertificationMechanisms
default:
return fmt.Errorf("invalid ProcessingActivityTransferSafeguards value: %q", s)
return fmt.Errorf("invalid ProcessingActivityTransferSafeguard value: %q", s)
}
return nil
}
func (p ProcessingActivityTransferSafeguards) Value() (driver.Value, error) {
func (p ProcessingActivityTransferSafeguard) Value() (driver.Value, error) {
return p.String(), nil
}

View File

@@ -30,6 +30,15 @@ const (
RiskTreatmentTransferred RiskTreatment = "TRANSFERRED"
)
func RiskTreatments() []RiskTreatment {
return []RiskTreatment{
RiskTreatmentMitigated,
RiskTreatmentAccepted,
RiskTreatmentAvoided,
RiskTreatmentTransferred,
}
}
func (rt RiskTreatment) MarshalText() ([]byte, error) {
return []byte(rt.String()), nil
}

View File

@@ -34,6 +34,16 @@ const (
SnapshotsTypeProcessingActivities SnapshotsType = "PROCESSING_ACTIVITIES"
)
func SnapshotsTypes() []SnapshotsType {
return []SnapshotsType{
SnapshotsTypeRisks,
SnapshotsTypeVendors,
SnapshotsTypeAssets,
SnapshotsTypeData,
SnapshotsTypeNonconformities,
}
}
func (st SnapshotsType) String() string {
return string(st)
}

View File

@@ -28,6 +28,13 @@ const (
TaskStateDone
)
func TaskStates() []TaskState {
return []TaskState{
TaskStateTodo,
TaskStateDone,
}
}
func (ts TaskState) MarshalText() ([]byte, error) {
return []byte(ts.String()), nil
}

View File

@@ -27,6 +27,14 @@ const (
TrustCenterVisibilityPublic TrustCenterVisibility = "PUBLIC"
)
func TrustCenterVisibilities() []TrustCenterVisibility {
return []TrustCenterVisibility{
TrustCenterVisibilityNone,
TrustCenterVisibilityPrivate,
TrustCenterVisibilityPublic,
}
}
func (tcv TrustCenterVisibility) String() string {
return string(tcv)
}

View File

@@ -47,6 +47,33 @@ const (
VendorCategoryVersionControl VendorCategory = "VERSION_CONTROL"
)
func VendorCategories() []VendorCategory {
return []VendorCategory{
VendorCategoryAnalytics,
VendorCategoryCloudMonitoring,
VendorCategoryCloudProvider,
VendorCategoryCollaboration,
VendorCategoryCustomerSupport,
VendorCategoryDataStorageAndProcessing,
VendorCategoryDocumentManagement,
VendorCategoryEmployeeManagement,
VendorCategoryEngineering,
VendorCategoryFinance,
VendorCategoryIdentityProvider,
VendorCategoryIT,
VendorCategoryMarketing,
VendorCategoryOfficeOperations,
VendorCategoryOther,
VendorCategoryPasswordManagement,
VendorCategoryProductAndDesign,
VendorCategoryProfessionalServices,
VendorCategoryRecruiting,
VendorCategorySales,
VendorCategorySecurity,
VendorCategoryVersionControl,
}
}
func (i VendorCategory) String() string {
return string(i)
}

View File

@@ -5,10 +5,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type AssetService struct {
@@ -35,6 +36,38 @@ type UpdateAssetRequest struct {
VendorIDs []gid.GID
}
func (car *CreateAssetRequest) Validate() error {
v := validator.New()
v.Check(car.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(car.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(car.Amount, "amount", validator.Required(), validator.Min(1))
v.Check(car.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(car.AssetType, "asset_type", validator.Required(), validator.OneOfSlice(coredata.AssetTypes()))
v.Check(car.DataTypesStored, "data_types_stored", validator.Required(), validator.SafeText(ContentMaxLength))
v.CheckEach(car.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.Required(), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (uar *UpdateAssetRequest) Validate() error {
v := validator.New()
v.Check(uar.ID, "id", validator.Required(), validator.GID(coredata.AssetEntityType))
v.Check(uar.Name, "name", validator.SafeText(NameMaxLength))
v.Check(uar.Amount, "amount", validator.Min(1))
v.Check(uar.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(uar.AssetType, "asset_type", validator.OneOfSlice(coredata.AssetTypes()))
v.Check(uar.DataTypesStored, "data_types_stored", validator.SafeText(ContentMaxLength))
v.CheckEach(uar.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (s AssetService) Get(
ctx context.Context,
assetID gid.GID,
@@ -135,6 +168,10 @@ func (s AssetService) Update(
ctx context.Context,
req UpdateAssetRequest,
) (*coredata.Asset, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
asset := &coredata.Asset{ID: req.ID}
assetVendors := &coredata.AssetVendors{}
@@ -185,6 +222,10 @@ func (s AssetService) Create(
ctx context.Context,
req CreateAssetRequest,
) (*coredata.Asset, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
assetID := gid.New(s.svc.scope.GetTenantID(), coredata.AssetEntityType)
assetVendors := &coredata.AssetVendors{}

View File

@@ -21,11 +21,12 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type AuditService struct {
@@ -52,21 +53,45 @@ type (
TrustCenterVisibility *coredata.TrustCenterVisibility
}
UpdateAuditStateRequest struct {
ID gid.GID
State coredata.AuditState
}
UploadAuditReportRequest struct {
AuditID gid.GID
File File
}
DeleteAuditReportRequest struct {
ID gid.GID
}
)
func (car *CreateAuditRequest) Validate() error {
v := validator.New()
v.Check(car.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(car.FrameworkID, "framework_id", validator.Required(), validator.GID(coredata.FrameworkEntityType))
v.Check(car.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(car.ValidUntil, "valid_until", validator.After(car.ValidFrom))
v.Check(car.State, "state", validator.OneOfSlice(coredata.AuditStates()))
v.Check(car.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (uar *UpdateAuditRequest) Validate() error {
v := validator.New()
v.Check(uar.ID, "id", validator.Required(), validator.GID(coredata.AuditEntityType))
v.Check(uar.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(uar.ValidUntil, "valid_until", validator.After(uar.ValidFrom))
v.Check(uar.State, "state", validator.OneOfSlice(coredata.AuditStates()))
v.Check(uar.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (uarr *UploadAuditReportRequest) Validate() error {
v := validator.New()
v.Check(uarr.AuditID, "audit_id", validator.Required(), validator.GID(coredata.AuditEntityType))
return v.Error()
}
func (s AuditService) Get(
ctx context.Context,
auditID gid.GID,
@@ -111,8 +136,11 @@ func (s *AuditService) Create(
ctx context.Context,
req *CreateAuditRequest,
) (*coredata.Audit, error) {
now := time.Now()
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
audit := &coredata.Audit{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.AuditEntityType),
Name: req.Name,
@@ -166,8 +194,11 @@ func (s *AuditService) Update(
ctx context.Context,
req *UpdateAuditRequest,
) (*coredata.Audit, error) {
audit := &coredata.Audit{}
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
audit := &coredata.Audit{}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {

View File

@@ -22,11 +22,12 @@ import (
"text/template"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
var (
@@ -55,6 +56,15 @@ type (
}
)
func (car *CreateConnectorRequest) Validate() error {
v := validator.New()
v.Check(car.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(car.Provider, "provider", validator.Required(), validator.OneOfSlice(coredata.ConnectorProviders()))
v.Check(car.Protocol, "protocol", validator.Required(), validator.OneOfSlice(coredata.ConnectorProtocols()))
v.Check(car.Connection, "connection", validator.Required())
return v.Error()
}
func (s *ConnectorService) ListForOrganizationID(
ctx context.Context,
organizationID gid.GID,
@@ -88,20 +98,8 @@ func (s *ConnectorService) Create(
ctx context.Context,
req CreateConnectorRequest,
) (*coredata.Connector, error) {
if req.OrganizationID == gid.Nil {
return nil, fmt.Errorf("organization ID is required")
}
if req.Provider == "" {
return nil, fmt.Errorf("connector provider is required")
}
if req.Protocol == "" {
return nil, fmt.Errorf("connector protocol is required")
}
if req.Connection == nil {
return nil, fmt.Errorf("connection configuration is required")
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
id := gid.New(s.svc.scope.GetTenantID(), coredata.ConnectorEntityType)

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type ContinualImprovementService struct {
@@ -53,6 +54,34 @@ type (
}
)
func (ccir *CreateContinualImprovementRequest) Validate() error {
v := validator.New()
v.Check(ccir.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(ccir.ReferenceID, "reference_id", validator.SafeText(NameMaxLength))
v.Check(ccir.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ccir.Source, "source", validator.SafeText(ContentMaxLength))
v.Check(ccir.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(ccir.Status, "status", validator.OneOfSlice(coredata.ContinualImprovementStatuses()))
v.Check(ccir.Priority, "priority", validator.OneOfSlice(coredata.ContinualImprovementPriorities()))
return v.Error()
}
func (ucir *UpdateContinualImprovementRequest) Validate() error {
v := validator.New()
v.Check(ucir.ID, "id", validator.Required(), validator.GID(coredata.ContinualImprovementEntityType))
v.Check(ucir.ReferenceID, "reference_id", validator.SafeText(NameMaxLength))
v.Check(ucir.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ucir.Source, "source", validator.SafeText(ContentMaxLength))
v.Check(ucir.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(ucir.Status, "status", validator.OneOfSlice(coredata.ContinualImprovementStatuses()))
v.Check(ucir.Priority, "priority", validator.OneOfSlice(coredata.ContinualImprovementPriorities()))
return v.Error()
}
func (s ContinualImprovementService) Get(
ctx context.Context,
continualImprovementID gid.GID,
@@ -81,6 +110,10 @@ func (s *ContinualImprovementService) Create(
ctx context.Context,
req *CreateContinualImprovementRequest,
) (*coredata.ContinualImprovement, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
improvement := &coredata.ContinualImprovement{

View File

@@ -23,6 +23,7 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -48,18 +49,35 @@ type (
Status *coredata.ControlStatus
ExclusionJustification *string
}
ConnectControlToMitigationRequest struct {
ControlID gid.GID
MitigationID gid.GID
}
DisconnectControlFromMitigationRequest struct {
ControlID gid.GID
MitigationID gid.GID
}
)
func (ccr *CreateControlRequest) Validate() error {
v := validator.New()
v.Check(ccr.ID, "id", validator.Required(), validator.GID(coredata.ControlEntityType))
v.Check(ccr.FrameworkID, "framework_id", validator.Required(), validator.GID(coredata.FrameworkEntityType))
v.Check(ccr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ccr.Description, "description", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(ccr.SectionTitle, "section_title", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ccr.Status, "status", validator.Required(), validator.OneOfSlice(coredata.ControlStatuses()))
v.Check(ccr.ExclusionJustification, "exclusion_justification", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (ucr *UpdateControlRequest) Validate() error {
v := validator.New()
v.Check(ucr.ID, "id", validator.Required(), validator.GID(coredata.ControlEntityType))
v.Check(ucr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(ucr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ucr.SectionTitle, "section_title", validator.SafeText(TitleMaxLength))
v.Check(ucr.Status, "status", validator.OneOfSlice(coredata.ControlStatuses()))
v.Check(ucr.ExclusionJustification, "exclusion_justification", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s ControlService) CountForDocumentID(
ctx context.Context,
documentID gid.GID,
@@ -706,6 +724,10 @@ func (s ControlService) Create(
ctx context.Context,
req CreateControlRequest,
) (*coredata.Control, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
framework := &coredata.Framework{}
@@ -765,6 +787,10 @@ func (s ControlService) Update(
ctx context.Context,
req UpdateControlRequest,
) (*coredata.Control, error) {
if err := req.Validate(); err != nil {
return nil, err
}
control := &coredata.Control{ID: req.ID}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {

View File

@@ -18,12 +18,13 @@ import (
"context"
"fmt"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/certmanager"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/crypto/cipher"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -40,6 +41,15 @@ type (
}
)
func (ccdr *CreateCustomDomainRequest) Validate() error {
v := validator.New()
v.Check(ccdr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(ccdr.Domain, "domain", validator.Required(), validator.NotEmpty(), validator.Domain())
return v.Error()
}
func NewCustomDomainService(
svc *TenantService,
acmeService *certmanager.ACMEService,
@@ -58,6 +68,10 @@ func (s *CustomDomainService) CreateCustomDomain(
ctx context.Context,
req CreateCustomDomainRequest,
) (*coredata.CustomDomain, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var domain *coredata.CustomDomain
err := s.svc.pg.WithTx(

View File

@@ -19,30 +19,61 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type DatumService struct {
svc *TenantService
type (
DatumService struct {
svc *TenantService
}
CreateDatumRequest struct {
OrganizationID gid.GID
Name string
DataClassification coredata.DataClassification
OwnerID gid.GID
VendorIDs []gid.GID
}
UpdateDatumRequest struct {
ID gid.GID
Name *string
DataClassification *coredata.DataClassification
OwnerID *gid.GID
VendorIDs []gid.GID
}
)
func (cdr *CreateDatumRequest) Validate() error {
v := validator.New()
v.Check(cdr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cdr.Name, "name", validator.Required(), validator.SafeText(NameMaxLength))
v.Check(cdr.DataClassification, "data_classification", validator.Required(), validator.OneOfSlice(coredata.DataClassifications()))
v.Check(cdr.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.CheckEach(cdr.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.Required(), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
type CreateDatumRequest struct {
OrganizationID gid.GID
Name string
DataClassification coredata.DataClassification
OwnerID gid.GID
VendorIDs []gid.GID
}
func (udr *UpdateDatumRequest) Validate() error {
v := validator.New()
type UpdateDatumRequest struct {
ID gid.GID
Name *string
DataClassification *coredata.DataClassification
OwnerID *gid.GID
VendorIDs []gid.GID
v.Check(udr.ID, "id", validator.Required(), validator.GID(coredata.DatumEntityType))
v.Check(udr.Name, "name", validator.SafeText(NameMaxLength))
v.Check(udr.DataClassification, "data_classification", validator.OneOfSlice(coredata.DataClassifications()))
v.Check(udr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.CheckEach(udr.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.Required(), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (s DatumService) Get(
@@ -145,6 +176,10 @@ func (s DatumService) Update(
ctx context.Context,
req UpdateDatumRequest,
) (*coredata.Datum, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
datum := &coredata.Datum{}
datumVendors := &coredata.DatumVendors{}
@@ -189,6 +224,10 @@ func (s DatumService) Create(
ctx context.Context,
req CreateDatumRequest,
) (*coredata.Datum, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
datumID := gid.New(s.svc.scope.GetTenantID(), coredata.DatumEntityType)
datumVendors := &coredata.DatumVendors{}

View File

@@ -26,6 +26,7 @@ import (
"go.probo.inc/probo/pkg/html2pdf"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/validator"
"go.probo.inc/probo/pkg/watermarkpdf"
)
@@ -86,6 +87,42 @@ type (
}
)
func (cdr *CreateDocumentRequest) Validate() error {
v := validator.New()
v.Check(cdr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cdr.Title, "title", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cdr.Content, "content", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(cdr.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(cdr.Classification, "classification", validator.Required(), validator.OneOfSlice(coredata.DocumentClassifications()))
v.Check(cdr.DocumentType, "document_type", validator.Required(), validator.OneOfSlice(coredata.DocumentTypes()))
v.Check(cdr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (udr *UpdateDocumentRequest) Validate() error {
v := validator.New()
v.Check(udr.DocumentID, "document_id", validator.Required(), validator.GID(coredata.DocumentEntityType))
v.Check(udr.Title, "title", validator.SafeText(TitleMaxLength))
v.Check(udr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(udr.Classification, "classification", validator.OneOfSlice(coredata.DocumentClassifications()))
v.Check(udr.DocumentType, "document_type", validator.OneOfSlice(coredata.DocumentTypes()))
v.Check(udr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (udvr *UpdateDocumentVersionRequest) Validate() error {
v := validator.New()
v.Check(udvr.ID, "id", validator.Required(), validator.GID(coredata.DocumentVersionEntityType))
v.Check(udvr.Content, "content", validator.Required(), validator.SafeText(ContentMaxLength))
return v.Error()
}
const (
TokenTypeSigningRequest = "signing_request"
@@ -308,6 +345,10 @@ func (s *DocumentService) Create(
ctx context.Context,
req CreateDocumentRequest,
) (*coredata.Document, *coredata.DocumentVersion, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
now := time.Now()
documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType)
documentVersionID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentVersionEntityType)
@@ -1131,6 +1172,10 @@ func (s *DocumentService) Update(
ctx context.Context,
req UpdateDocumentRequest,
) (*coredata.Document, error) {
if err := req.Validate(); err != nil {
return nil, err
}
document := &coredata.Document{}
people := &coredata.People{}
now := time.Now()

View File

@@ -19,12 +19,13 @@ import (
"fmt"
"time"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -40,6 +41,16 @@ type (
}
)
func (umer *UploadMeasureEvidenceRequest) Validate() error {
v := validator.New()
v.Check(umer.MeasureID, "measure_id", validator.Required(), validator.GID(coredata.MeasureEntityType))
v.Check(umer.URL, "url", validator.URL())
v.Check(umer.File, "file", validator.Required())
return v.Error()
}
func (s EvidenceService) Get(
ctx context.Context,
evidenceID gid.GID,
@@ -68,6 +79,10 @@ func (s EvidenceService) UploadMeasureEvidence(
ctx context.Context,
req UploadMeasureEvidenceRequest,
) (*coredata.Evidence, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
evidenceID := gid.New(s.svc.scope.GetTenantID(), coredata.EvidenceEntityType)

View File

@@ -23,11 +23,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
)
type (

View File

@@ -35,6 +35,7 @@ import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/slug"
"go.probo.inc/probo/pkg/soagen"
"go.probo.inc/probo/pkg/validator"
)
const (
@@ -73,6 +74,26 @@ type (
}
)
func (cfr *CreateFrameworkRequest) Validate() error {
v := validator.New()
v.Check(cfr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cfr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cfr.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (ufr *UpdateFrameworkRequest) Validate() error {
v := validator.New()
v.Check(ufr.ID, "id", validator.Required(), validator.GID(coredata.FrameworkEntityType))
v.Check(ufr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(ufr.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s FrameworkService) RequestExport(
ctx context.Context,
frameworkID gid.GID,
@@ -310,6 +331,10 @@ func (s FrameworkService) Create(
ctx context.Context,
req CreateFrameworkRequest,
) (*coredata.Framework, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
organization := &coredata.Organization{}
@@ -420,6 +445,10 @@ func (s FrameworkService) Update(
ctx context.Context,
req UpdateFrameworkRequest,
) (*coredata.Framework, error) {
if err := req.Validate(); err != nil {
return nil, err
}
framework := &coredata.Framework{ID: req.ID}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {

View File

@@ -24,6 +24,7 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -69,6 +70,29 @@ type (
}
)
func (cmr *CreateMeasureRequest) Validate() error {
v := validator.New()
v.Check(cmr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cmr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cmr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(cmr.Category, "category", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (umr *UpdateMeasureRequest) Validate() error {
v := validator.New()
v.Check(umr.ID, "id", validator.Required(), validator.GID(coredata.MeasureEntityType))
v.Check(umr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(umr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(umr.Category, "category", validator.SafeText(TitleMaxLength))
v.Check(umr.State, "state", validator.OneOfSlice(coredata.MeasureStates()))
return v.Error()
}
func (s MeasureService) CountForRiskID(
ctx context.Context,
riskID gid.GID,
@@ -399,6 +423,10 @@ func (s MeasureService) Update(
ctx context.Context,
req UpdateMeasureRequest,
) (*coredata.Measure, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
measure := &coredata.Measure{ID: req.ID}
err := s.svc.pg.WithTx(
@@ -444,6 +472,10 @@ func (s MeasureService) Create(
ctx context.Context,
req CreateMeasureRequest,
) (*coredata.Measure, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
var measure *coredata.Measure
organization := &coredata.Organization{}

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type NonconformityService struct {
@@ -59,6 +60,36 @@ type (
}
)
func (cnr *CreateNonconformityRequest) Validate() error {
v := validator.New()
v.Check(cnr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cnr.ReferenceID, "reference_id", validator.Required(), validator.SafeText(NameMaxLength))
v.Check(cnr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(cnr.AuditID, "audit_id", validator.Required(), validator.GID(coredata.AuditEntityType))
v.Check(cnr.RootCause, "root_cause", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(cnr.CorrectiveAction, "corrective_action", validator.SafeText(ContentMaxLength))
v.Check(cnr.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(cnr.Status, "status", validator.OneOfSlice(coredata.NonconformityStatuses()))
v.Check(cnr.EffectivenessCheck, "effectiveness_check", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (unr *UpdateNonconformityRequest) Validate() error {
v := validator.New()
v.Check(unr.ID, "id", validator.Required(), validator.GID(coredata.NonconformityEntityType))
v.Check(unr.ReferenceID, "reference_id", validator.SafeText(NameMaxLength))
v.Check(unr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(unr.RootCause, "root_cause", validator.SafeText(ContentMaxLength))
v.Check(unr.CorrectiveAction, "corrective_action", validator.SafeText(ContentMaxLength))
v.Check(unr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(unr.Status, "status", validator.OneOfSlice(coredata.NonconformityStatuses()))
v.Check(unr.EffectivenessCheck, "effectiveness_check", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s NonconformityService) Get(
ctx context.Context,
nonconformityID gid.GID,
@@ -83,6 +114,10 @@ func (s *NonconformityService) Create(
ctx context.Context,
req *CreateNonconformityRequest,
) (*coredata.Nonconformity, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
nonconformity := &coredata.Nonconformity{
@@ -143,6 +178,10 @@ func (s *NonconformityService) Update(
ctx context.Context,
req *UpdateNonconformityRequest,
) (*coredata.Nonconformity, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
nonconformity := &coredata.Nonconformity{}
err := s.svc.pg.WithTx(

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type ObligationService struct {
@@ -57,6 +58,36 @@ type (
}
)
func (cor *CreateObligationRequest) Validate() error {
v := validator.New()
v.Check(cor.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cor.Area, "area", validator.SafeText(TitleMaxLength))
v.Check(cor.Source, "source", validator.SafeText(TitleMaxLength))
v.Check(cor.Requirement, "requirement", validator.SafeText(TitleMaxLength))
v.Check(cor.ActionsToBeImplemented, "actions_to_be_implemented", validator.SafeText(TitleMaxLength))
v.Check(cor.Regulator, "regulator", validator.SafeText(TitleMaxLength))
v.Check(cor.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(cor.Status, "status", validator.OneOfSlice(coredata.ObligationStatuses()))
return v.Error()
}
func (uor *UpdateObligationRequest) Validate() error {
v := validator.New()
v.Check(uor.ID, "id", validator.Required(), validator.GID(coredata.ObligationEntityType))
v.Check(uor.Area, "area", validator.SafeText(NameMaxLength))
v.Check(uor.Source, "source", validator.SafeText(NameMaxLength))
v.Check(uor.Requirement, "requirement", validator.SafeText(NameMaxLength))
v.Check(uor.ActionsToBeImplemented, "actions_to_be_implemented", validator.SafeText(NameMaxLength))
v.Check(uor.Regulator, "regulator", validator.SafeText(NameMaxLength))
v.Check(uor.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(uor.Status, "status", validator.OneOfSlice(coredata.ObligationStatuses()))
return v.Error()
}
func (s ObligationService) Get(
ctx context.Context,
obligationID gid.GID,
@@ -85,6 +116,10 @@ func (s *ObligationService) Create(
ctx context.Context,
req *CreateObligationRequest,
) (*coredata.Obligation, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
obligation := &coredata.Obligation{
@@ -135,6 +170,10 @@ func (s *ObligationService) Update(
ctx context.Context,
req *UpdateObligationRequest,
) (*coredata.Obligation, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
obligation := &coredata.Obligation{}
err := s.svc.pg.WithTx(

View File

@@ -22,12 +22,13 @@ import (
"path/filepath"
"time"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/slug"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
var (
@@ -74,10 +75,37 @@ type (
}
)
func (cor *CreateOrganizationRequest) Validate() error {
v := validator.New()
v.Check(cor.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (uor *UpdateOrganizationRequest) Validate() error {
v := validator.New()
v.Check(uor.ID, "id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(uor.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(uor.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(uor.WebsiteURL, "website_url", validator.SafeText(2048))
v.Check(uor.Email, "email", validator.SafeText(255))
v.Check(uor.HeadquarterAddress, "headquarter_address", validator.SafeText(2048))
v.Check(uor.File, "file", validator.NotEmpty())
v.Check(uor.HorizontalLogoFile, "horizontal_logo_file", validator.NotEmpty())
return v.Error()
}
func (s OrganizationService) Create(
ctx context.Context,
req CreateOrganizationRequest,
) (*coredata.Organization, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
organizationID := gid.New(s.svc.scope.GetTenantID(), coredata.OrganizationEntityType)
@@ -154,6 +182,10 @@ func (s OrganizationService) Update(
ctx context.Context,
req UpdateOrganizationRequest,
) (*coredata.Organization, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
organization := &coredata.Organization{}
err := s.svc.pg.WithTx(

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -30,17 +31,6 @@ type (
svc *TenantService
}
UpdatePeopleRequest struct {
ID gid.GID
Kind *coredata.PeopleKind
FullName *string
PrimaryEmailAddress *string
AdditionalEmailAddresses *[]string
Position **string
ContractStartDate **time.Time
ContractEndDate **time.Time
}
CreatePeopleRequest struct {
OrganizationID gid.GID
FullName string
@@ -51,8 +41,53 @@ type (
ContractStartDate *time.Time
ContractEndDate *time.Time
}
UpdatePeopleRequest struct {
ID gid.GID
Kind *coredata.PeopleKind
FullName *string
PrimaryEmailAddress *string
AdditionalEmailAddresses *[]string
Position **string
ContractStartDate **time.Time
ContractEndDate **time.Time
}
)
func (cpr *CreatePeopleRequest) Validate() error {
v := validator.New()
v.Check(cpr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cpr.FullName, "full_name", validator.Required(), validator.SafeText(NameMaxLength))
v.Check(cpr.PrimaryEmailAddress, "primary_email_address", validator.Required(), validator.NotEmpty(), validator.Email())
v.CheckEach(cpr.AdditionalEmailAddresses, "additional_email_addresses", func(index int, item any) {
v.Check(item, fmt.Sprintf("additional_email_addresses[%d]", index), validator.Required(), validator.NotEmpty(), validator.Email())
})
v.Check(cpr.Kind, "kind", validator.Required(), validator.OneOfSlice(coredata.PeopleKinds()))
v.Check(cpr.Position, "position", validator.SafeText(TitleMaxLength))
v.Check(cpr.ContractStartDate, "contract_start_date", validator.Before(cpr.ContractEndDate))
v.Check(cpr.ContractEndDate, "contract_end_date", validator.After(cpr.ContractStartDate))
return v.Error()
}
func (upr *UpdatePeopleRequest) Validate() error {
v := validator.New()
v.Check(upr.ID, "id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(upr.Kind, "kind", validator.OneOfSlice(coredata.PeopleKinds()))
v.Check(upr.FullName, "full_name", validator.Required(), validator.SafeText(NameMaxLength))
v.Check(upr.PrimaryEmailAddress, "primary_email_address", validator.NotEmpty(), validator.Email())
v.CheckEach(upr.AdditionalEmailAddresses, "additional_email_addresses", func(index int, item any) {
v.Check(item, fmt.Sprintf("additional_email_addresses[%d]", index), validator.Required(), validator.NotEmpty(), validator.Email())
})
v.Check(upr.Position, "position", validator.SafeText(TitleMaxLength))
v.Check(upr.ContractStartDate, "contract_start_date", validator.Before(upr.ContractEndDate))
v.Check(upr.ContractEndDate, "contract_end_date", validator.After(upr.ContractStartDate))
return v.Error()
}
func (s PeopleService) Get(
ctx context.Context,
peopleID gid.GID,
@@ -133,6 +168,10 @@ func (s PeopleService) Update(
ctx context.Context,
req UpdatePeopleRequest,
) (*coredata.People, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
people := &coredata.People{}
err := s.svc.pg.WithTx(
@@ -191,10 +230,8 @@ func (s PeopleService) Create(
ctx context.Context,
req CreatePeopleRequest,
) (*coredata.People, error) {
if req.ContractStartDate != nil && req.ContractEndDate != nil {
if req.ContractEndDate.Before(*req.ContractStartDate) {
return nil, fmt.Errorf("contract end date must be after or equal to start date")
}
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type ProcessingActivityService struct {
@@ -36,13 +37,13 @@ type (
Purpose *string
DataSubjectCategory *string
PersonalDataCategory *string
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalData
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalDatum
ConsentEvidenceLink *string
LawfulBasis coredata.ProcessingActivityLawfulBasis
Recipients *string
Location *string
InternationalTransfers bool
TransferSafeguards *coredata.ProcessingActivityTransferSafeguards
TransferSafeguard *coredata.ProcessingActivityTransferSafeguard
RetentionPeriod *string
SecurityMeasures *string
DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment
@@ -56,13 +57,13 @@ type (
Purpose **string
DataSubjectCategory **string
PersonalDataCategory **string
SpecialOrCriminalData *coredata.ProcessingActivitySpecialOrCriminalData
SpecialOrCriminalData *coredata.ProcessingActivitySpecialOrCriminalDatum
ConsentEvidenceLink **string
LawfulBasis *coredata.ProcessingActivityLawfulBasis
Recipients **string
Location **string
InternationalTransfers *bool
TransferSafeguards **coredata.ProcessingActivityTransferSafeguards
TransferSafeguard **coredata.ProcessingActivityTransferSafeguard
RetentionPeriod **string
SecurityMeasures **string
DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment
@@ -71,6 +72,57 @@ type (
}
)
func (cpar *CreateProcessingActivityRequest) Validate() error {
v := validator.New()
v.Check(cpar.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cpar.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cpar.Purpose, "purpose", validator.SafeText(TitleMaxLength))
v.Check(cpar.DataSubjectCategory, "data_subject_category", validator.SafeText(TitleMaxLength))
v.Check(cpar.PersonalDataCategory, "personal_data_category", validator.SafeText(TitleMaxLength))
v.Check(cpar.SpecialOrCriminalData, "special_or_criminal_data", validator.Required(), validator.OneOfSlice(coredata.ProcessingActivitySpecialOrCriminalData()))
v.Check(cpar.ConsentEvidenceLink, "consent_evidence_link", validator.SafeText(2048))
v.Check(cpar.LawfulBasis, "lawful_basis", validator.Required(), validator.OneOfSlice(coredata.ProcessingActivityLawfulBases()))
v.Check(cpar.Recipients, "recipients", validator.SafeText(TitleMaxLength))
v.Check(cpar.Location, "location", validator.SafeText(TitleMaxLength))
v.Check(cpar.InternationalTransfers, "international_transfers", validator.Required())
v.Check(cpar.TransferSafeguard, "transfer_safeguard", validator.OneOfSlice(coredata.ProcessingActivityTransferSafeguards()))
v.Check(cpar.RetentionPeriod, "retention_period", validator.SafeText(TitleMaxLength))
v.Check(cpar.SecurityMeasures, "security_measures", validator.SafeText(TitleMaxLength))
v.Check(cpar.DataProtectionImpactAssessment, "data_protection_impact_assessment", validator.Required(), validator.OneOfSlice(coredata.ProcessingActivityDataProtectionImpactAssessments()))
v.Check(cpar.TransferImpactAssessment, "transfer_impact_assessment", validator.Required(), validator.OneOfSlice(coredata.ProcessingActivityTransferImpactAssessments()))
v.CheckEach(cpar.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.Required(), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (upar *UpdateProcessingActivityRequest) Validate() error {
v := validator.New()
v.Check(upar.ID, "id", validator.Required(), validator.GID(coredata.ProcessingActivityEntityType))
v.Check(upar.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(upar.Purpose, "purpose", validator.SafeText(TitleMaxLength))
v.Check(upar.DataSubjectCategory, "data_subject_category", validator.SafeText(TitleMaxLength))
v.Check(upar.PersonalDataCategory, "personal_data_category", validator.SafeText(TitleMaxLength))
v.Check(upar.SpecialOrCriminalData, "special_or_criminal_data", validator.OneOfSlice(coredata.ProcessingActivitySpecialOrCriminalData()))
v.Check(upar.ConsentEvidenceLink, "consent_evidence_link", validator.SafeText(2048))
v.Check(upar.LawfulBasis, "lawful_basis", validator.OneOfSlice(coredata.ProcessingActivityLawfulBases()))
v.Check(upar.Recipients, "recipients", validator.SafeText(TitleMaxLength))
v.Check(upar.Location, "location", validator.SafeText(TitleMaxLength))
v.Check(upar.TransferSafeguard, "transfer_safeguards", validator.OneOfSlice(coredata.ProcessingActivityTransferSafeguards()))
v.Check(upar.RetentionPeriod, "retention_period", validator.SafeText(TitleMaxLength))
v.Check(upar.SecurityMeasures, "security_measures", validator.SafeText(TitleMaxLength))
v.Check(upar.DataProtectionImpactAssessment, "data_protection_impact_assessment", validator.OneOfSlice(coredata.ProcessingActivityDataProtectionImpactAssessments()))
v.Check(upar.TransferImpactAssessment, "transfer_impact_assessment", validator.OneOfSlice(coredata.ProcessingActivityTransferImpactAssessments()))
v.CheckEach(upar.VendorIDs, "vendor_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("vendor_ids[%d]", index), validator.GID(coredata.VendorEntityType))
})
return v.Error()
}
func (s ProcessingActivityService) Get(
ctx context.Context,
processingActivityID gid.GID,
@@ -111,7 +163,7 @@ func (s *ProcessingActivityService) Create(
Recipients: req.Recipients,
Location: req.Location,
InternationalTransfers: req.InternationalTransfers,
TransferSafeguards: req.TransferSafeguards,
TransferSafeguard: req.TransferSafeguard,
RetentionPeriod: req.RetentionPeriod,
SecurityMeasures: req.SecurityMeasures,
DataProtectionImpactAssessment: req.DataProtectionImpactAssessment,
@@ -193,8 +245,8 @@ func (s *ProcessingActivityService) Update(
if req.InternationalTransfers != nil {
processingActivity.InternationalTransfers = *req.InternationalTransfers
}
if req.TransferSafeguards != nil {
processingActivity.TransferSafeguards = *req.TransferSafeguards
if req.TransferSafeguard != nil {
processingActivity.TransferSafeguard = *req.TransferSafeguard
}
if req.RetentionPeriod != nil {
processingActivity.RetentionPeriod = *req.RetentionPeriod

View File

@@ -23,6 +23,7 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -59,6 +60,42 @@ type (
}
)
func (crr *CreateRiskRequest) Validate() error {
v := validator.New()
v.Check(crr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(crr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(crr.Description, "description", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(crr.Category, "category", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(crr.Treatment, "treatment", validator.Required(), validator.OneOfSlice(coredata.RiskTreatments()))
v.Check(crr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(crr.InherentLikelihood, "inherent_likelihood", validator.Required(), validator.Min(1), validator.Max(5))
v.Check(crr.InherentImpact, "inherent_impact", validator.Required(), validator.Min(1), validator.Max(5))
v.Check(crr.ResidualLikelihood, "residual_likelihood", validator.Min(1), validator.Max(5))
v.Check(crr.ResidualImpact, "residual_impact", validator.Min(1), validator.Max(5))
v.Check(crr.Note, "note", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (urr *UpdateRiskRequest) Validate() error {
v := validator.New()
v.Check(urr.ID, "id", validator.Required(), validator.GID(coredata.RiskEntityType))
v.Check(urr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(urr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(urr.Category, "category", validator.SafeText(TitleMaxLength))
v.Check(urr.Treatment, "treatment", validator.OneOfSlice(coredata.RiskTreatments()))
v.Check(urr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(urr.InherentLikelihood, "inherent_likelihood", validator.Min(1), validator.Max(5))
v.Check(urr.InherentImpact, "inherent_impact", validator.Min(1), validator.Max(5))
v.Check(urr.ResidualLikelihood, "residual_likelihood", validator.Min(1), validator.Max(5))
v.Check(urr.ResidualImpact, "residual_impact", validator.Min(1), validator.Max(5))
v.Check(urr.Note, "note", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s RiskService) CountForMeasureID(
ctx context.Context,
measureID gid.GID,

View File

@@ -20,6 +20,9 @@ import (
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
"go.probo.inc/probo/pkg/agents"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
@@ -30,9 +33,12 @@ import (
"go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/html2pdf"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
)
const (
NameMaxLength = 100
TitleMaxLength = 1000
ContentMaxLength = 5000
)
type ExportService interface {

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type SnapshotService struct {
@@ -45,6 +46,28 @@ type (
}
)
func (csr *CreateSnapshotRequest) Validate() error {
v := validator.New()
v.Check(csr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(csr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(csr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(csr.Type, "type", validator.Required(), validator.OneOfSlice(coredata.SnapshotsTypes()))
return v.Error()
}
func (usr *UpdateSnapshotRequest) Validate() error {
v := validator.New()
v.Check(usr.ID, "id", validator.Required(), validator.GID(coredata.SnapshotEntityType))
v.Check(usr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(usr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(usr.Type, "type", validator.OneOfSlice(coredata.SnapshotsTypes()))
return v.Error()
}
func (s *SnapshotService) Get(
ctx context.Context,
snapshotID gid.GID,

View File

@@ -24,6 +24,7 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -51,10 +52,39 @@ type (
}
)
func (ctr *CreateTaskRequest) Validate() error {
v := validator.New()
v.Check(ctr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(ctr.MeasureID, "measure_id", validator.GID(coredata.MeasureEntityType))
v.Check(ctr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ctr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ctr.TimeEstimate, "time_estimate", validator.RangeDuration(0, 1000*time.Hour))
v.Check(ctr.AssignedToID, "assigned_to_id", validator.GID(coredata.PeopleEntityType))
return v.Error()
}
func (utr *UpdateTaskRequest) Validate() error {
v := validator.New()
v.Check(utr.TaskID, "task_id", validator.Required(), validator.GID(coredata.TaskEntityType))
v.Check(utr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(utr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(utr.TimeEstimate, "time_estimate", validator.RangeDuration(0, 1000*time.Hour))
v.Check(utr.State, "state", validator.OneOfSlice(coredata.TaskStates()))
return v.Error()
}
func (s TaskService) Create(
ctx context.Context,
req CreateTaskRequest,
) (*coredata.Task, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
taskID := gid.New(s.svc.scope.GetTenantID(), coredata.TaskEntityType)
@@ -180,6 +210,9 @@ func (s TaskService) Update(
ctx context.Context,
req UpdateTaskRequest,
) (*coredata.Task, error) {
if err := req.Validate(); err != nil {
return nil, err
}
task := &coredata.Task{}

View File

@@ -17,7 +17,6 @@ package probo
import (
"context"
"fmt"
"net/mail"
"net/url"
"time"
@@ -27,6 +26,7 @@ import (
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -49,16 +49,40 @@ type (
TrustCenterFileIDs []gid.GID
}
DeleteTrustCenterAccessRequest struct {
ID gid.GID
}
TrustCenterAccessData struct {
TrustCenterID gid.GID `json:"trust_center_id"`
Email string `json:"email"`
}
)
func (ctcar *CreateTrustCenterAccessRequest) Validate() error {
v := validator.New()
v.Check(ctcar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(ctcar.Email, "email", validator.Required(), validator.Email())
v.Check(ctcar.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (utcar *UpdateTrustCenterAccessRequest) Validate() error {
v := validator.New()
v.Check(utcar.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterAccessEntityType))
v.Check(utcar.Name, "name", validator.SafeText(TitleMaxLength))
v.CheckEach(utcar.DocumentIDs, "document_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("document_ids[%d]", index), validator.Required(), validator.GID(coredata.DocumentEntityType))
})
v.CheckEach(utcar.ReportIDs, "report_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("report_ids[%d]", index), validator.Required(), validator.GID(coredata.ReportEntityType))
})
v.CheckEach(utcar.TrustCenterFileIDs, "trust_center_file_ids", func(index int, item any) {
v.Check(item, fmt.Sprintf("trust_center_file_ids[%d]", index), validator.Required(), validator.GID(coredata.TrustCenterFileEntityType))
})
return v.Error()
}
func (s TrustCenterAccessService) ListForTrustCenterID(
ctx context.Context,
trustCenterID gid.GID,
@@ -239,18 +263,12 @@ func (s TrustCenterAccessService) Create(
ctx context.Context,
req *CreateTrustCenterAccessRequest,
) (*coredata.TrustCenterAccess, error) {
if _, err := mail.ParseAddress(req.Email); err != nil {
return nil, fmt.Errorf("invalid email address")
}
if req.Name == "" {
return nil, fmt.Errorf("name is required")
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
var access *coredata.TrustCenterAccess
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
@@ -285,14 +303,13 @@ func (s TrustCenterAccessService) Update(
ctx context.Context,
req *UpdateTrustCenterAccessRequest,
) (*coredata.TrustCenterAccess, error) {
now := time.Now()
var access *coredata.TrustCenterAccess
if req.Name != nil && *req.Name == "" {
return nil, fmt.Errorf("name is required")
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
var access *coredata.TrustCenterAccess
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
@@ -344,14 +361,14 @@ func (s TrustCenterAccessService) Update(
func (s TrustCenterAccessService) Delete(
ctx context.Context,
req *DeleteTrustCenterAccessRequest,
trustCenterAccessID gid.GID,
) error {
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
access := &coredata.TrustCenterAccess{}
if err := access.LoadByID(ctx, tx, s.svc.scope, req.ID); err != nil {
if err := access.LoadByID(ctx, tx, s.svc.scope, trustCenterAccessID); err != nil {
return fmt.Errorf("cannot load trust center access: %w", err)
}

View File

@@ -25,12 +25,13 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -53,12 +54,31 @@ type (
Category *string
TrustCenterVisibility *coredata.TrustCenterVisibility
}
DeleteTrustCenterFileRequest struct {
ID gid.GID
}
)
func (ctcfr *CreateTrustCenterFileRequest) Validate() error {
v := validator.New()
v.Check(ctcfr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(ctcfr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ctcfr.Category, "category", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ctcfr.File, "file", validator.Required())
v.Check(ctcfr.TrustCenterVisibility, "trust_center_visibility", validator.Required(), validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (utcfr *UpdateTrustCenterFileRequest) Validate() error {
v := validator.New()
v.Check(utcfr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterFileEntityType))
v.Check(utcfr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(utcfr.Category, "category", validator.SafeText(TitleMaxLength))
v.Check(utcfr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities()))
return v.Error()
}
func (s TrustCenterFileService) ListForOrganizationID(
ctx context.Context,
organizationID gid.GID,
@@ -134,8 +154,8 @@ func (s TrustCenterFileService) Create(
ctx context.Context,
req *CreateTrustCenterFileRequest,
) (*coredata.TrustCenterFile, error) {
if req.Name == "" {
return nil, fmt.Errorf("name is required")
if err := req.Validate(); err != nil {
return nil, err
}
// Validate file
@@ -197,14 +217,14 @@ func (s TrustCenterFileService) Update(
ctx context.Context,
req *UpdateTrustCenterFileRequest,
) (*coredata.TrustCenterFile, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
var file *coredata.TrustCenterFile
if req.Name != nil && *req.Name == "" {
return nil, fmt.Errorf("name is required")
}
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
@@ -242,14 +262,14 @@ func (s TrustCenterFileService) Update(
func (s TrustCenterFileService) Delete(
ctx context.Context,
req *DeleteTrustCenterFileRequest,
trustCenterFileID gid.GID,
) error {
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
file := &coredata.TrustCenterFile{}
if err := file.LoadByID(ctx, tx, s.svc.scope, req.ID); err != nil {
if err := file.LoadByID(ctx, tx, s.svc.scope, trustCenterFileID); err != nil {
return fmt.Errorf("cannot load trust center file: %w", err)
}

View File

@@ -31,6 +31,7 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -54,12 +55,30 @@ type (
LogoFile *File
Rank *int
}
DeleteTrustCenterReferenceRequest struct {
ID gid.GID
}
)
func (ctcrr *CreateTrustCenterReferenceRequest) Validate() error {
v := validator.New()
v.Check(ctcrr.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(ctcrr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(ctcrr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ctcrr.WebsiteURL, "website_url", validator.Required(), validator.SafeText(2048))
return v.Error()
}
func (utcrr *UpdateTrustCenterReferenceRequest) Validate() error {
v := validator.New()
v.Check(utcrr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterReferenceEntityType))
v.Check(utcrr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(utcrr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(utcrr.WebsiteURL, "website_url", validator.SafeText(2048))
return v.Error()
}
func (s TrustCenterReferenceService) ListForTrustCenterID(
ctx context.Context,
trustCenterID gid.GID,
@@ -132,12 +151,8 @@ func (s TrustCenterReferenceService) Create(
ctx context.Context,
req *CreateTrustCenterReferenceRequest,
) (*coredata.TrustCenterReference, error) {
if req.Name == "" {
return nil, fmt.Errorf("name is required")
}
if req.WebsiteURL == "" {
return nil, fmt.Errorf("website URL is required")
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
@@ -185,19 +200,14 @@ func (s TrustCenterReferenceService) Update(
ctx context.Context,
req *UpdateTrustCenterReferenceRequest,
) (*coredata.TrustCenterReference, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
var reference *coredata.TrustCenterReference
var newFileID *gid.GID
if req.Name != nil && *req.Name == "" {
return nil, fmt.Errorf("name is required")
}
if req.WebsiteURL != nil && *req.WebsiteURL == "" {
return nil, fmt.Errorf("website URL is required")
}
var logoKey string
err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error {
@@ -254,12 +264,12 @@ func (s TrustCenterReferenceService) Update(
func (s TrustCenterReferenceService) Delete(
ctx context.Context,
req *DeleteTrustCenterReferenceRequest,
trustCenterReferenceID gid.GID,
) error {
err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error {
reference := &coredata.TrustCenterReference{}
if err := reference.LoadByID(ctx, tx, s.svc.scope, req.ID); err != nil {
if err := reference.LoadByID(ctx, tx, s.svc.scope, trustCenterReferenceID); err != nil {
return fmt.Errorf("cannot load trust center reference: %w", err)
}

View File

@@ -25,10 +25,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -48,12 +49,27 @@ type (
File io.Reader
FileName string
}
DeleteTrustCenterNDARequest struct {
TrustCenterID gid.GID
}
)
func (utcr *UpdateTrustCenterRequest) Validate() error {
v := validator.New()
v.Check(utcr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(utcr.Slug, "slug", validator.SafeText(NameMaxLength))
v.Check(utcr.NonDisclosureAgreementFileID, "non_disclosure_agreement_file_id", validator.GID(coredata.FileEntityType))
return v.Error()
}
func (utcndar *UploadTrustCenterNDARequest) Validate() error {
v := validator.New()
v.Check(utcndar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(utcndar.FileName, "file_name", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s TrustCenterService) Get(
ctx context.Context,
trustCenterID gid.GID,
@@ -124,6 +140,10 @@ func (s TrustCenterService) Update(
ctx context.Context,
req *UpdateTrustCenterRequest,
) (*coredata.TrustCenter, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
var trustCenter *coredata.TrustCenter
var file *coredata.File
@@ -170,6 +190,10 @@ func (s TrustCenterService) UploadNDA(
ctx context.Context,
req *UploadTrustCenterNDARequest,
) (*coredata.TrustCenter, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
objectKey, err := uuid.NewV7()
if err != nil {
return nil, nil, fmt.Errorf("cannot generate object key: %w", err)
@@ -249,7 +273,7 @@ func (s TrustCenterService) UploadNDA(
func (s TrustCenterService) DeleteNDA(
ctx context.Context,
req *DeleteTrustCenterNDARequest,
trustCenterID gid.GID,
) (*coredata.TrustCenter, *coredata.File, error) {
var trustCenter *coredata.TrustCenter
@@ -257,7 +281,7 @@ func (s TrustCenterService) DeleteNDA(
ctx,
func(conn pg.Conn) error {
trustCenter = &coredata.TrustCenter{}
if err := trustCenter.LoadByID(ctx, conn, s.svc.scope, req.TrustCenterID); err != nil {
if err := trustCenter.LoadByID(ctx, conn, s.svc.scope, trustCenterID); err != nil {
return fmt.Errorf("cannot load trust center: %w", err)
}
@@ -276,7 +300,7 @@ func (s TrustCenterService) DeleteNDA(
return nil, nil, err
}
return trustCenter, nil, nil // File is nil after deletion
return trustCenter, nil, nil
}
func (s TrustCenterService) GenerateNDAFileURL(

View File

@@ -25,10 +25,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -49,6 +50,23 @@ type (
}
)
func (vbaacr *VendorBusinessAssociateAgreementCreateRequest) Validate() error {
v := validator.New()
v.Check(vbaacr.FileName, "file_name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(vbaacr.ValidUntil, "valid_until", validator.After(vbaacr.ValidFrom))
return v.Error()
}
func (vbaaur *VendorBusinessAssociateAgreementUpdateRequest) Validate() error {
v := validator.New()
v.Check(vbaaur.ValidUntil, "valid_until", validator.After(vbaaur.ValidFrom))
return v.Error()
}
func (s VendorBusinessAssociateAgreementService) GetByVendorID(
ctx context.Context,
vendorID gid.GID,
@@ -85,6 +103,10 @@ func (s VendorBusinessAssociateAgreementService) Upload(
vendorID gid.GID,
req *VendorBusinessAssociateAgreementCreateRequest,
) (*coredata.VendorBusinessAssociateAgreement, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
objectKey, err := uuid.NewV7()
if err != nil {
return nil, nil, fmt.Errorf("cannot generate object key: %w", err)
@@ -255,6 +277,10 @@ func (s VendorBusinessAssociateAgreementService) Update(
vendorID gid.GID,
req *VendorBusinessAssociateAgreementUpdateRequest,
) (*coredata.VendorBusinessAssociateAgreement, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
existingAgreement := &coredata.VendorBusinessAssociateAgreement{}
file := &coredata.File{}

View File

@@ -19,11 +19,12 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filevalidation"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -40,6 +41,14 @@ type (
}
)
func (vcrcr *VendorComplianceReportCreateRequest) Validate() error {
v := validator.New()
v.Check(vcrcr.ReportName, "report_name", validator.Required(), validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s VendorComplianceReportService) ListForVendorID(
ctx context.Context,
vendorID gid.GID,
@@ -66,6 +75,10 @@ func (s VendorComplianceReportService) Upload(
vendorID gid.GID,
req *VendorComplianceReportCreateRequest,
) (*coredata.VendorComplianceReport, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendor, err := s.svc.Vendors.Get(ctx, vendorID)
if err != nil {
return nil, fmt.Errorf("cannot get vendor: %w", err)

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -47,6 +48,30 @@ type (
}
)
func (cvcr *CreateVendorContactRequest) Validate() error {
v := validator.New()
v.Check(cvcr.VendorID, "vendor_id", validator.Required(), validator.GID(coredata.VendorEntityType))
v.Check(cvcr.FullName, "full_name", validator.SafeText(TitleMaxLength))
v.Check(cvcr.Email, "email", validator.Email())
v.Check(cvcr.Phone, "phone", validator.SafeText(NameMaxLength))
v.Check(cvcr.Role, "role", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (uvcr *UpdateVendorContactRequest) Validate() error {
v := validator.New()
v.Check(uvcr.ID, "id", validator.Required(), validator.GID(coredata.VendorContactEntityType))
v.Check(uvcr.FullName, "full_name", validator.SafeText(TitleMaxLength))
v.Check(uvcr.Email, "email", validator.Email())
v.Check(uvcr.Phone, "phone", validator.SafeText(NameMaxLength))
v.Check(uvcr.Role, "role", validator.SafeText(TitleMaxLength))
return v.Error()
}
func (s VendorContactService) Get(
ctx context.Context,
vendorContactID gid.GID,
@@ -102,6 +127,10 @@ func (s VendorContactService) Create(
ctx context.Context,
req CreateVendorContactRequest,
) (*coredata.VendorContact, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
vendorContact := &coredata.VendorContact{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorContactEntityType),
@@ -136,6 +165,10 @@ func (s VendorContactService) Update(
ctx context.Context,
req UpdateVendorContactRequest,
) (*coredata.VendorContact, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendorContact := &coredata.VendorContact{}
err := s.svc.pg.WithTx(

View File

@@ -25,10 +25,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -49,6 +50,23 @@ type (
}
)
func (vdpacr *VendorDataPrivacyAgreementCreateRequest) Validate() error {
v := validator.New()
v.Check(vdpacr.FileName, "file_name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(vdpacr.ValidUntil, "valid_until", validator.After(vdpacr.ValidFrom))
return v.Error()
}
func (vdpaur *VendorDataPrivacyAgreementUpdateRequest) Validate() error {
v := validator.New()
v.Check(vdpaur.ValidUntil, "valid_until", validator.After(vdpaur.ValidFrom))
return v.Error()
}
func (s VendorDataPrivacyAgreementService) GetByVendorID(
ctx context.Context,
vendorID gid.GID,
@@ -85,6 +103,10 @@ func (s VendorDataPrivacyAgreementService) Upload(
vendorID gid.GID,
req *VendorDataPrivacyAgreementCreateRequest,
) (*coredata.VendorDataPrivacyAgreement, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
objectKey, err := uuid.NewV7()
if err != nil {
return nil, nil, fmt.Errorf("cannot generate object key: %w", err)
@@ -253,6 +275,10 @@ func (s VendorDataPrivacyAgreementService) Update(
vendorID gid.GID,
req *VendorDataPrivacyAgreementUpdateRequest,
) (*coredata.VendorDataPrivacyAgreement, *coredata.File, error) {
if err := req.Validate(); err != nil {
return nil, nil, err
}
existingAgreement := &coredata.VendorDataPrivacyAgreement{}
file := &coredata.File{}

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -91,6 +92,67 @@ type (
}
)
func (cvr *CreateVendorRequest) Validate() error {
v := validator.New()
v.Check(cvr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(cvr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cvr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(cvr.HeadquarterAddress, "headquarter_address", validator.SafeText(ContentMaxLength))
v.Check(cvr.LegalName, "legal_name", validator.SafeText(TitleMaxLength))
v.Check(cvr.WebsiteURL, "website_url", validator.SafeText(2048))
v.Check(cvr.Category, "category", validator.OneOfSlice(coredata.VendorCategories()))
v.Check(cvr.PrivacyPolicyURL, "privacy_policy_url", validator.SafeText(2048))
v.Check(cvr.ServiceLevelAgreementURL, "service_level_agreement_url", validator.SafeText(2048))
v.Check(cvr.DataProcessingAgreementURL, "data_processing_agreement_url", validator.SafeText(2048))
v.Check(cvr.BusinessAssociateAgreementURL, "business_associate_agreement_url", validator.SafeText(2048))
v.Check(cvr.SubprocessorsListURL, "subprocessors_list_url", validator.SafeText(2048))
v.Check(cvr.SecurityPageURL, "security_page_url", validator.SafeText(2048))
v.Check(cvr.TrustPageURL, "trust_page_url", validator.SafeText(2048))
v.Check(cvr.TermsOfServiceURL, "terms_of_service_url", validator.SafeText(2048))
v.Check(cvr.StatusPageURL, "status_page_url", validator.SafeText(2048))
v.Check(cvr.BusinessOwnerID, "business_owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(cvr.SecurityOwnerID, "security_owner_id", validator.GID(coredata.PeopleEntityType))
return v.Error()
}
func (uvr *UpdateVendorRequest) Validate() error {
v := validator.New()
v.Check(uvr.ID, "id", validator.Required(), validator.GID(coredata.VendorEntityType))
v.Check(uvr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(uvr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(uvr.HeadquarterAddress, "headquarter_address", validator.SafeText(ContentMaxLength))
v.Check(uvr.LegalName, "legal_name", validator.SafeText(TitleMaxLength))
v.Check(uvr.WebsiteURL, "website_url", validator.SafeText(2048))
v.Check(uvr.Category, "category", validator.OneOfSlice(coredata.VendorCategories()))
v.Check(uvr.PrivacyPolicyURL, "privacy_policy_url", validator.SafeText(2048))
v.Check(uvr.ServiceLevelAgreementURL, "service_level_agreement_url", validator.SafeText(2048))
v.Check(uvr.DataProcessingAgreementURL, "data_processing_agreement_url", validator.SafeText(2048))
v.Check(uvr.BusinessAssociateAgreementURL, "business_associate_agreement_url", validator.SafeText(2048))
v.Check(uvr.SubprocessorsListURL, "subprocessors_list_url", validator.SafeText(2048))
v.Check(uvr.SecurityPageURL, "security_page_url", validator.SafeText(2048))
v.Check(uvr.TrustPageURL, "trust_page_url", validator.SafeText(2048))
v.Check(uvr.TermsOfServiceURL, "terms_of_service_url", validator.SafeText(2048))
v.Check(uvr.StatusPageURL, "status_page_url", validator.SafeText(2048))
v.Check(uvr.BusinessOwnerID, "business_owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(uvr.SecurityOwnerID, "security_owner_id", validator.GID(coredata.PeopleEntityType))
return v.Error()
}
func (cvrar *CreateVendorRiskAssessmentRequest) Validate() error {
v := validator.New()
v.Check(cvrar.VendorID, "vendor_id", validator.Required(), validator.GID(coredata.VendorEntityType))
v.Check(cvrar.DataSensitivity, "data_sensitivity", validator.Required(), validator.OneOfSlice(coredata.DataSensitivities()))
v.Check(cvrar.BusinessImpact, "business_impact", validator.Required(), validator.OneOfSlice(coredata.BusinessImpacts()))
v.Check(cvrar.Notes, "notes", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s VendorService) CountForOrganizationID(
ctx context.Context,
organizationID gid.GID,
@@ -208,6 +270,10 @@ func (s VendorService) Update(
ctx context.Context,
req UpdateVendorRequest,
) (*coredata.Vendor, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendor := &coredata.Vendor{}
err := s.svc.pg.WithTx(
@@ -373,6 +439,10 @@ func (s VendorService) Create(
ctx context.Context,
req CreateVendorRequest,
) (*coredata.Vendor, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
vendor := &coredata.Vendor{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorEntityType),
@@ -542,6 +612,10 @@ func (s VendorService) CreateRiskAssessment(
ctx context.Context,
req CreateVendorRiskAssessmentRequest,
) (*coredata.VendorRiskAssessment, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendorRiskAssessmentID := gid.New(s.svc.scope.GetTenantID(), coredata.VendorRiskAssessmentEntityType)
now := time.Now()

View File

@@ -19,10 +19,11 @@ import (
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/validator"
)
type (
@@ -43,6 +44,26 @@ type (
}
)
func (cvsr *CreateVendorServiceRequest) Validate() error {
v := validator.New()
v.Check(cvsr.VendorID, "vendor_id", validator.Required(), validator.GID(coredata.VendorEntityType))
v.Check(cvsr.Name, "name", validator.Required(), validator.SafeText(TitleMaxLength))
v.Check(cvsr.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (uvsr *UpdateVendorServiceRequest) Validate() error {
v := validator.New()
v.Check(uvsr.ID, "id", validator.Required(), validator.GID(coredata.VendorServiceEntityType))
v.Check(uvsr.Name, "name", validator.SafeText(TitleMaxLength))
v.Check(uvsr.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s VendorServiceService) Get(
ctx context.Context,
vendorServiceID gid.GID,
@@ -98,6 +119,10 @@ func (s VendorServiceService) Create(
ctx context.Context,
req CreateVendorServiceRequest,
) (*coredata.VendorService, error) {
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
vendorService := &coredata.VendorService{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.VendorServiceEntityType),
@@ -130,6 +155,10 @@ func (s VendorServiceService) Update(
ctx context.Context,
req UpdateVendorServiceRequest,
) (*coredata.VendorService, error) {
if err := req.Validate(); err != nil {
return nil, err
}
vendorService := &coredata.VendorService{}
err := s.svc.pg.WithTx(

View File

@@ -43,25 +43,16 @@ enum OrderDirection
enum MeasureState
@goModel(model: "go.probo.inc/probo/pkg/coredata.MeasureState") {
NOT_STARTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MeasureStateNotStarted"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureStateNotStarted")
IN_PROGRESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MeasureStateInProgress"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureStateInProgress")
NOT_APPLICABLE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MeasureStateNotApplicable"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureStateNotApplicable")
IMPLEMENTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MeasureStateImplemented"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureStateImplemented")
}
enum TaskState
@goModel(model: "go.probo.inc/probo/pkg/coredata.TaskState") {
enum TaskState @goModel(model: "go.probo.inc/probo/pkg/coredata.TaskState") {
TODO @goEnum(value: "go.probo.inc/probo/pkg/coredata.TaskStateTodo")
DONE @goEnum(value: "go.probo.inc/probo/pkg/coredata.TaskStateDone")
}
@@ -69,43 +60,27 @@ enum TaskState
enum EvidenceState
@goModel(model: "go.probo.inc/probo/pkg/coredata.EvidenceState") {
FULFILLED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.EvidenceStateFulfilled"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.EvidenceStateFulfilled")
REQUESTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.EvidenceStateRequested"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.EvidenceStateRequested")
}
enum PeopleKind
@goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleKind") {
EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindEmployee")
enum PeopleKind @goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleKind") {
EMPLOYEE @goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindEmployee")
CONTRACTOR
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.PeopleKindContractor"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindContractor")
SERVICE_ACCOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.PeopleKindServiceAccount"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindServiceAccount")
}
enum InvitationStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationStatus") {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationStatusPending"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusPending")
ACCEPTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationStatusAccepted"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusAccepted")
EXPIRED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired")
}
enum Role @goModel(model: "go.probo.inc/probo/pkg/coredata.Role") {
@@ -117,12 +92,9 @@ enum Role @goModel(model: "go.probo.inc/probo/pkg/coredata.Role") {
enum DocumentStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentStatus") {
DRAFT
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusDraft")
DRAFT @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusDraft")
PUBLISHED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentStatusPublished"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusPublished")
}
enum EvidenceType
@@ -134,49 +106,28 @@ enum EvidenceType
enum RiskTreatment
@goModel(model: "go.probo.inc/probo/pkg/coredata.RiskTreatment") {
MITIGATED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentMitigated"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentMitigated")
ACCEPTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentAccepted"
)
AVOIDED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentAvoided"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentAccepted")
AVOIDED @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentAvoided")
TRANSFERRED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentTransferred"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskTreatmentTransferred")
}
enum AuditState
@goModel(model: "go.probo.inc/probo/pkg/coredata.AuditState") {
enum AuditState @goModel(model: "go.probo.inc/probo/pkg/coredata.AuditState") {
NOT_STARTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AuditStateNotStarted"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateNotStarted")
IN_PROGRESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AuditStateInProgress"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateInProgress")
COMPLETED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateCompleted")
REJECTED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateRejected")
OUTDATED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateOutdated")
REJECTED @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateRejected")
OUTDATED @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateOutdated")
}
enum SAMLEnforcementPolicy
@goModel(
model: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicy"
) {
OFF
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOff"
)
@goModel(model: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicy") {
OFF @goEnum(value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOff")
OPTIONAL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
@@ -190,21 +141,14 @@ enum SAMLEnforcementPolicy
enum UserAuthMethod
@goModel(model: "go.probo.inc/probo/pkg/coredata.UserAuthMethod") {
PASSWORD
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodPassword"
)
SAML
@goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodSAML")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodPassword")
SAML @goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodSAML")
}
enum TrustCenterVisibility
@goModel(
model: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibility"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibility") {
NONE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityNone"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityNone")
PRIVATE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityPrivate"
@@ -216,21 +160,14 @@ enum TrustCenterVisibility
}
enum NonconformityStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.NonconformityStatus"
) {
OPEN
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusOpen"
)
@goModel(model: "go.probo.inc/probo/pkg/coredata.NonconformityStatus") {
OPEN @goEnum(value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusOpen")
IN_PROGRESS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusInProgress"
)
CLOSED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusClosed"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.NonconformityStatusClosed")
}
enum ObligationStatus
@@ -244,9 +181,7 @@ enum ObligationStatus
value: "go.probo.inc/probo/pkg/coredata.ObligationStatusPartiallyCompliant"
)
COMPLIANT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ObligationStatusCompliant"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationStatusCompliant")
}
enum ContinualImprovementStatus
@@ -285,21 +220,21 @@ enum ContinualImprovementPriority
)
}
enum ProcessingActivitySpecialOrCriminalData
enum ProcessingActivitySpecialOrCriminalDatum
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalData"
model: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDatum"
) {
YES
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDataYes"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDatumYes"
)
NO
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDataNo"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDatumNo"
)
POSSIBLE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDataPossible"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivitySpecialOrCriminalDatumPossible"
)
}
@@ -333,33 +268,33 @@ enum ProcessingActivityLawfulBasis
)
}
enum ProcessingActivityTransferSafeguards
enum ProcessingActivityTransferSafeguard
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguards"
model: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguard"
) {
STANDARD_CONTRACTUAL_CLAUSES
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsStandardContractualClauses"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardStandardContractualClauses"
)
BINDING_CORPORATE_RULES
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsBindingCorporateRules"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardBindingCorporateRules"
)
ADEQUACY_DECISION
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsAdequacyDecision"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardAdequacyDecision"
)
DEROGATIONS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsDerogations"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardDerogations"
)
CODES_OF_CONDUCT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsCodesOfConduct"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardCodesOfConduct"
)
CERTIFICATION_MECHANISMS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardsCertificationMechanisms"
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityTransferSafeguardCertificationMechanisms"
)
}
@@ -395,47 +330,29 @@ enum ProcessingActivityTransferImpactAssessment
enum UserOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.UserOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.UserOrderFieldCreatedAt"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.UserOrderFieldCreatedAt")
}
enum PeopleOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleOrderField") {
FULL_NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldFullName"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldFullName")
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldCreatedAt"
)
KIND
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldKind"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldCreatedAt")
KIND @goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleOrderFieldKind")
}
enum VendorOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.VendorOrderField") {
NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldName"
)
NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldName")
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldCreatedAt"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldCreatedAt")
UPDATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldUpdatedAt"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldUpdatedAt")
}
enum FrameworkOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.FrameworkOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.FrameworkOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.FrameworkOrderFieldCreatedAt"
@@ -445,9 +362,7 @@ enum FrameworkOrderField
enum ControlOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.ControlOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ControlOrderFieldCreatedAt"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ControlOrderFieldCreatedAt")
SECTION_TITLE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ControlOrderFieldSectionTitle"
@@ -457,13 +372,8 @@ enum ControlOrderField
enum MeasureOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.MeasureOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MeasureOrderFieldCreatedAt"
)
NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MeasureOrderFieldName"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureOrderFieldCreatedAt")
NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.MeasureOrderFieldName")
}
enum TaskOrderField
@@ -474,9 +384,7 @@ enum TaskOrderField
enum DocumentOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentOrderField") {
TITLE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentOrderFieldTitle"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentOrderFieldTitle")
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentOrderFieldCreatedAt"
@@ -490,23 +398,14 @@ enum DocumentOrderField
enum RiskOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.RiskOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldCreatedAt"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldCreatedAt")
UPDATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldUpdatedAt"
)
NAME
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldName")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldUpdatedAt")
NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldName")
CATEGORY
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldCategory"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldCategory")
TREATMENT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldTreatment"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldTreatment")
INHERENT_RISK_SCORE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.RiskOrderFieldInherentRiskScore"
@@ -541,9 +440,7 @@ enum VendorComplianceReportOrderField
}
enum VendorContactOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.VendorContactOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.VendorContactOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorContactOrderFieldCreatedAt"
@@ -559,9 +456,7 @@ enum VendorContactOrderField
}
enum VendorServiceOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.VendorServiceOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.VendorServiceOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorServiceOrderFieldCreatedAt"
@@ -573,13 +468,9 @@ enum VendorServiceOrderField
}
enum OrganizationOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.OrganizationOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.OrganizationOrderField") {
NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.OrganizationOrderFieldName"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.OrganizationOrderFieldName")
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.OrganizationOrderFieldCreatedAt"
@@ -592,41 +483,25 @@ enum OrganizationOrderField
enum DataSensitivity
@goModel(model: "go.probo.inc/probo/pkg/coredata.DataSensitivity") {
NONE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityNone")
LOW
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityLow")
MEDIUM
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DataSensitivityMedium"
)
HIGH
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityHigh")
NONE @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityNone")
LOW @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityLow")
MEDIUM @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityMedium")
HIGH @goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityHigh")
CRITICAL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DataSensitivityCritical"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataSensitivityCritical")
}
enum BusinessImpact
@goModel(model: "go.probo.inc/probo/pkg/coredata.BusinessImpact") {
LOW @goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactLow")
MEDIUM
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.BusinessImpactMedium"
)
HIGH
@goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactHigh")
MEDIUM @goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactMedium")
HIGH @goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactHigh")
CRITICAL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.BusinessImpactCritical"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.BusinessImpactCritical")
}
enum DocumentVersionOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderField") {
VERSION
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderFieldVersion"
@@ -893,9 +768,7 @@ enum CountryCode
enum VendorCategory
@goModel(model: "go.probo.inc/probo/pkg/coredata.VendorCategory") {
ANALYTICS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryAnalytics"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryAnalytics")
CLOUD_MONITORING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryCloudMonitoring"
@@ -925,28 +798,21 @@ enum VendorCategory
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEmployeeManagement"
)
ENGINEERING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEngineering"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEngineering")
FINANCE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryFinance"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryFinance")
IDENTITY_PROVIDER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryIdentityProvider"
)
IT @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryIT")
MARKETING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryMarketing"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryMarketing")
OFFICE_OPERATIONS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOfficeOperations"
)
OTHER
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOther")
OTHER @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOther")
PASSWORD_MANAGEMENT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryPasswordManagement"
@@ -960,15 +826,10 @@ enum VendorCategory
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryProfessionalServices"
)
RECRUITING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryRecruiting"
)
SALES
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategorySales")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryRecruiting")
SALES @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategorySales")
SECURITY
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategorySecurity"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategorySecurity")
VERSION_CONTROL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryVersionControl"
@@ -977,21 +838,15 @@ enum VendorCategory
enum DocumentType
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentType") {
OTHER
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeOther")
OTHER @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeOther")
ISMS @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeISMS")
POLICY
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePolicy")
POLICY @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePolicy")
PROCEDURE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentTypeProcedure"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeProcedure")
}
enum DocumentClassification
@goModel(
model: "go.probo.inc/probo/pkg/coredata.DocumentClassification"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentClassification") {
PUBLIC
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentClassificationPublic"
@@ -1010,34 +865,23 @@ enum DocumentClassification
)
}
enum AssetType
@goModel(model: "go.probo.inc/probo/pkg/coredata.AssetType") {
PHYSICAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetTypePhysical")
VIRTUAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetTypeVirtual")
enum AssetType @goModel(model: "go.probo.inc/probo/pkg/coredata.AssetType") {
PHYSICAL @goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetTypePhysical")
VIRTUAL @goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetTypeVirtual")
}
enum AssetOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.AssetOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AssetOrderFieldCreatedAt"
)
AMOUNT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AssetOrderFieldAmount"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetOrderFieldCreatedAt")
AMOUNT @goEnum(value: "go.probo.inc/probo/pkg/coredata.AssetOrderFieldAmount")
}
enum DatumOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.DatumOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldCreatedAt"
)
NAME
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldName")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldCreatedAt")
NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldName")
DATA_CLASSIFICATION
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DatumOrderFieldDataClassification"
@@ -1047,59 +891,38 @@ enum DatumOrderField
enum DataClassification
@goModel(model: "go.probo.inc/probo/pkg/coredata.DataClassification") {
PUBLIC
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DataClassificationPublic"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataClassificationPublic")
INTERNAL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DataClassificationInternal"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataClassificationInternal")
CONFIDENTIAL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DataClassificationConfidential"
)
SECRET
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DataClassificationSecret"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DataClassificationSecret")
}
enum ControlStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.ControlStatus") {
INCLUDED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ControlStatusIncluded"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ControlStatusIncluded")
EXCLUDED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ControlStatusExcluded"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ControlStatusExcluded")
}
enum AuditOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.AuditOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldCreatedAt"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldCreatedAt")
VALID_FROM
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldValidFrom"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldValidFrom")
VALID_UNTIL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldValidUntil"
)
STATE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldState"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldValidUntil")
STATE @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditOrderFieldState")
}
enum NonconformityOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.NonconformityOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.NonconformityOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.NonconformityOrderFieldCreatedAt"
@@ -1123,9 +946,7 @@ enum NonconformityOrderField
}
enum ObligationOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ObligationOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.ObligationOrderField") {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldCreatedAt"
@@ -1139,9 +960,7 @@ enum ObligationOrderField
value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldDueDate"
)
STATUS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldStatus"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationOrderFieldStatus")
}
enum ContinualImprovementOrderField
@@ -1227,9 +1046,7 @@ enum TrustCenterReferenceOrderField
}
enum TrustCenterFileOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.TrustCenterFileOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrustCenterFileOrderField") {
NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrustCenterFileOrderFieldName"
@@ -1246,24 +1063,16 @@ enum TrustCenterFileOrderField
enum SnapshotsType
@goModel(model: "go.probo.inc/probo/pkg/coredata.SnapshotsType") {
RISKS
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks")
VENDORS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeVendors"
)
ASSETS
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeAssets")
DATA
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeData")
RISKS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks")
VENDORS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeVendors")
ASSETS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeAssets")
DATA @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeData")
NONCONFORMITIES
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeNonconformities"
)
OBLIGATIONS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeObligations"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeObligations")
CONTINUAL_IMPROVEMENTS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeContinualImprovements"
@@ -1280,20 +1089,12 @@ enum SnapshotOrderField
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldCreatedAt"
)
NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldName"
)
TYPE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldType"
)
NAME @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldName")
TYPE @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldType")
}
enum MembershipOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField") {
FULL_NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName"
@@ -1303,9 +1104,7 @@ enum MembershipOrderField
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress"
)
ROLE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldRole"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldRole")
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldCreatedAt"
@@ -1313,21 +1112,15 @@ enum MembershipOrderField
}
enum InvitationOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField") {
FULL_NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldFullName"
)
EMAIL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldEmail"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldEmail")
ROLE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldRole"
)
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldRole")
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt"
@@ -2347,13 +2140,13 @@ type ProcessingActivity implements Node {
purpose: String
dataSubjectCategory: String
personalDataCategory: String
specialOrCriminalData: ProcessingActivitySpecialOrCriminalData!
specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum!
consentEvidenceLink: String
lawfulBasis: ProcessingActivityLawfulBasis!
recipients: String
location: String
internationalTransfers: Boolean!
transferSafeguards: ProcessingActivityTransferSafeguards
transferSafeguards: ProcessingActivityTransferSafeguard
retentionPeriod: String
securityMeasures: String
dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment!
@@ -3920,13 +3713,13 @@ input CreateProcessingActivityInput {
purpose: String
dataSubjectCategory: String
personalDataCategory: String
specialOrCriminalData: ProcessingActivitySpecialOrCriminalData!
specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum!
consentEvidenceLink: String
lawfulBasis: ProcessingActivityLawfulBasis!
recipients: String
location: String
internationalTransfers: Boolean!
transferSafeguards: ProcessingActivityTransferSafeguards
transferSafeguards: ProcessingActivityTransferSafeguard
retentionPeriod: String
securityMeasures: String
dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment!
@@ -3940,13 +3733,13 @@ input UpdateProcessingActivityInput {
purpose: String @goField(omittable: true)
dataSubjectCategory: String @goField(omittable: true)
personalDataCategory: String @goField(omittable: true)
specialOrCriminalData: ProcessingActivitySpecialOrCriminalData
specialOrCriminalData: ProcessingActivitySpecialOrCriminalDatum
consentEvidenceLink: String
lawfulBasis: ProcessingActivityLawfulBasis
recipients: String @goField(omittable: true)
location: String @goField(omittable: true)
internationalTransfers: Boolean
transferSafeguards: ProcessingActivityTransferSafeguards
transferSafeguards: ProcessingActivityTransferSafeguard
@goField(omittable: true)
retentionPeriod: String @goField(omittable: true)
securityMeasures: String @goField(omittable: true)
@@ -4789,9 +4582,7 @@ type DeleteSnapshotPayload {
}
enum SSLStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatus"
) {
@goModel(model: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatus") {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusPending"

File diff suppressed because it is too large Load Diff

View File

@@ -69,7 +69,7 @@ func NewProcessingActivity(par *coredata.ProcessingActivity) *ProcessingActivity
Recipients: par.Recipients,
Location: par.Location,
InternationalTransfers: par.InternationalTransfers,
TransferSafeguards: par.TransferSafeguards,
TransferSafeguards: par.TransferSafeguard,
RetentionPeriod: par.RetentionPeriod,
SecurityMeasures: par.SecurityMeasures,
DataProtectionImpactAssessment: par.DataProtectionImpactAssessment,

View File

@@ -440,13 +440,13 @@ type CreateProcessingActivityInput struct {
Purpose *string `json:"purpose,omitempty"`
DataSubjectCategory *string `json:"dataSubjectCategory,omitempty"`
PersonalDataCategory *string `json:"personalDataCategory,omitempty"`
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalData `json:"specialOrCriminalData"`
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalDatum `json:"specialOrCriminalData"`
ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"`
LawfulBasis coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis"`
Recipients *string `json:"recipients,omitempty"`
Location *string `json:"location,omitempty"`
InternationalTransfers bool `json:"internationalTransfers"`
TransferSafeguards *coredata.ProcessingActivityTransferSafeguards `json:"transferSafeguards,omitempty"`
TransferSafeguard *coredata.ProcessingActivityTransferSafeguard `json:"transferSafeguards,omitempty"`
RetentionPeriod *string `json:"retentionPeriod,omitempty"`
SecurityMeasures *string `json:"securityMeasures,omitempty"`
DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment"`
@@ -1518,13 +1518,13 @@ type ProcessingActivity struct {
Purpose *string `json:"purpose,omitempty"`
DataSubjectCategory *string `json:"dataSubjectCategory,omitempty"`
PersonalDataCategory *string `json:"personalDataCategory,omitempty"`
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalData `json:"specialOrCriminalData"`
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalDatum `json:"specialOrCriminalData"`
ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"`
LawfulBasis coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis"`
Recipients *string `json:"recipients,omitempty"`
Location *string `json:"location,omitempty"`
InternationalTransfers bool `json:"internationalTransfers"`
TransferSafeguards *coredata.ProcessingActivityTransferSafeguards `json:"transferSafeguards,omitempty"`
TransferSafeguards *coredata.ProcessingActivityTransferSafeguard `json:"transferSafeguards,omitempty"`
RetentionPeriod *string `json:"retentionPeriod,omitempty"`
SecurityMeasures *string `json:"securityMeasures,omitempty"`
DataProtectionImpactAssessment coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment"`
@@ -2022,23 +2022,23 @@ type UpdatePeoplePayload struct {
}
type UpdateProcessingActivityInput struct {
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
Purpose graphql.Omittable[*string] `json:"purpose,omitempty"`
DataSubjectCategory graphql.Omittable[*string] `json:"dataSubjectCategory,omitempty"`
PersonalDataCategory graphql.Omittable[*string] `json:"personalDataCategory,omitempty"`
SpecialOrCriminalData *coredata.ProcessingActivitySpecialOrCriminalData `json:"specialOrCriminalData,omitempty"`
ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"`
LawfulBasis *coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis,omitempty"`
Recipients graphql.Omittable[*string] `json:"recipients,omitempty"`
Location graphql.Omittable[*string] `json:"location,omitempty"`
InternationalTransfers *bool `json:"internationalTransfers,omitempty"`
TransferSafeguards graphql.Omittable[*coredata.ProcessingActivityTransferSafeguards] `json:"transferSafeguards,omitempty"`
RetentionPeriod graphql.Omittable[*string] `json:"retentionPeriod,omitempty"`
SecurityMeasures graphql.Omittable[*string] `json:"securityMeasures,omitempty"`
DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment,omitempty"`
TransferImpactAssessment *coredata.ProcessingActivityTransferImpactAssessment `json:"transferImpactAssessment,omitempty"`
VendorIds []gid.GID `json:"vendorIds,omitempty"`
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
Purpose graphql.Omittable[*string] `json:"purpose,omitempty"`
DataSubjectCategory graphql.Omittable[*string] `json:"dataSubjectCategory,omitempty"`
PersonalDataCategory graphql.Omittable[*string] `json:"personalDataCategory,omitempty"`
SpecialOrCriminalData *coredata.ProcessingActivitySpecialOrCriminalDatum `json:"specialOrCriminalData,omitempty"`
ConsentEvidenceLink *string `json:"consentEvidenceLink,omitempty"`
LawfulBasis *coredata.ProcessingActivityLawfulBasis `json:"lawfulBasis,omitempty"`
Recipients graphql.Omittable[*string] `json:"recipients,omitempty"`
Location graphql.Omittable[*string] `json:"location,omitempty"`
InternationalTransfers *bool `json:"internationalTransfers,omitempty"`
TransferSafeguards graphql.Omittable[*coredata.ProcessingActivityTransferSafeguard] `json:"transferSafeguards,omitempty"`
RetentionPeriod graphql.Omittable[*string] `json:"retentionPeriod,omitempty"`
SecurityMeasures graphql.Omittable[*string] `json:"securityMeasures,omitempty"`
DataProtectionImpactAssessment *coredata.ProcessingActivityDataProtectionImpactAssessment `json:"dataProtectionImpactAssessment,omitempty"`
TransferImpactAssessment *coredata.ProcessingActivityTransferImpactAssessment `json:"transferImpactAssessment,omitempty"`
VendorIds []gid.GID `json:"vendorIds,omitempty"`
}
type UpdateProcessingActivityPayload struct {

View File

@@ -1384,9 +1384,7 @@ func (r *mutationResolver) UploadTrustCenterNda(ctx context.Context, input types
func (r *mutationResolver) DeleteTrustCenterNda(ctx context.Context, input types.DeleteTrustCenterNDAInput) (*types.DeleteTrustCenterNDAPayload, error) {
prb := r.ProboService(ctx, input.TrustCenterID.TenantID())
trustCenter, file, err := prb.TrustCenters.DeleteNDA(ctx, &probo.DeleteTrustCenterNDARequest{
TrustCenterID: input.TrustCenterID,
})
trustCenter, file, err := prb.TrustCenters.DeleteNDA(ctx, input.TrustCenterID)
if err != nil {
panic(fmt.Errorf("cannot delete trust center NDA: %w", err))
}
@@ -1443,9 +1441,7 @@ func (r *mutationResolver) UpdateTrustCenterAccess(ctx context.Context, input ty
func (r *mutationResolver) DeleteTrustCenterAccess(ctx context.Context, input types.DeleteTrustCenterAccessInput) (*types.DeleteTrustCenterAccessPayload, error) {
prb := r.ProboService(ctx, input.ID.TenantID())
err := prb.TrustCenterAccesses.Delete(ctx, &probo.DeleteTrustCenterAccessRequest{
ID: input.ID,
})
err := prb.TrustCenterAccesses.Delete(ctx, input.ID)
if err != nil {
panic(fmt.Errorf("cannot delete trust center access: %w", err))
}
@@ -1515,9 +1511,7 @@ func (r *mutationResolver) UpdateTrustCenterReference(ctx context.Context, input
func (r *mutationResolver) DeleteTrustCenterReference(ctx context.Context, input types.DeleteTrustCenterReferenceInput) (*types.DeleteTrustCenterReferencePayload, error) {
prb := r.ProboService(ctx, input.ID.TenantID())
err := prb.TrustCenterReferences.Delete(ctx, &probo.DeleteTrustCenterReferenceRequest{
ID: input.ID,
})
err := prb.TrustCenterReferences.Delete(ctx, input.ID)
if err != nil {
panic(fmt.Errorf("cannot delete trust center reference: %w", err))
}
@@ -1589,9 +1583,7 @@ func (r *mutationResolver) GetTrustCenterFile(ctx context.Context, input types.G
func (r *mutationResolver) DeleteTrustCenterFile(ctx context.Context, input types.DeleteTrustCenterFileInput) (*types.DeleteTrustCenterFilePayload, error) {
prb := r.ProboService(ctx, input.ID.TenantID())
err := prb.TrustCenterFiles.Delete(ctx, &probo.DeleteTrustCenterFileRequest{
ID: input.ID,
})
err := prb.TrustCenterFiles.Delete(ctx, input.ID)
if err != nil {
panic(fmt.Errorf("cannot delete trust center file: %w", err))
}
@@ -2858,7 +2850,7 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
if err != nil {
var errNoChanges *coredata.ErrDocumentVersionNoChanges
if errors.As(err, &errNoChanges) {
return nil, gqlutils.Invalid(errNoChanges)
return nil, gqlutils.Invalid(errNoChanges, nil)
}
panic(fmt.Errorf("cannot publish document version: %w", err))
}
@@ -3568,7 +3560,7 @@ func (r *mutationResolver) CreateProcessingActivity(ctx context.Context, input t
Recipients: input.Recipients,
Location: input.Location,
InternationalTransfers: input.InternationalTransfers,
TransferSafeguards: input.TransferSafeguards,
TransferSafeguard: input.TransferSafeguard,
RetentionPeriod: input.RetentionPeriod,
SecurityMeasures: input.SecurityMeasures,
DataProtectionImpactAssessment: input.DataProtectionImpactAssessment,
@@ -3601,7 +3593,7 @@ func (r *mutationResolver) UpdateProcessingActivity(ctx context.Context, input t
Recipients: UnwrapOmittable(input.Recipients),
Location: UnwrapOmittable(input.Location),
InternationalTransfers: input.InternationalTransfers,
TransferSafeguards: UnwrapOmittable(input.TransferSafeguards),
TransferSafeguard: UnwrapOmittable(input.TransferSafeguards),
RetentionPeriod: UnwrapOmittable(input.RetentionPeriod),
SecurityMeasures: UnwrapOmittable(input.SecurityMeasures),
DataProtectionImpactAssessment: input.DataProtectionImpactAssessment,

View File

@@ -30,9 +30,7 @@ func Unauthorized() *gqlerror.Error {
}
func AuthenticationRequired(details map[string]any) *gqlerror.Error {
extensions := map[string]any{
"code": "AUTHENTICATION_REQUIRED",
}
extensions := map[string]any{"code": "AUTHENTICATION_REQUIRED"}
maps.Copy(extensions, details)
return &gqlerror.Error{
@@ -59,11 +57,14 @@ func Conflict(err error) *gqlerror.Error {
}
}
func Invalid(err error) *gqlerror.Error {
func Invalid(err error, details map[string]any) *gqlerror.Error {
extensions := map[string]any{"code": "INVALID_REQUEST"}
if details != nil {
maps.Copy(extensions, details)
}
return &gqlerror.Error{
Message: err.Error(),
Extensions: map[string]any{
"code": "INVALID",
},
Message: err.Error(),
Extensions: extensions,
}
}

View File

@@ -24,6 +24,7 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/validator"
)
func RecoverFunc(ctx context.Context, err any) error {
@@ -50,6 +51,27 @@ func RecoverFunc(ctx context.Context, err any) error {
})
}
var errValidations validator.ValidationErrors
if errors.As(asError(err), &errValidations) {
gqlErrors := gqlerror.List{}
for _, err := range errValidations {
gqlErrors = append(
gqlErrors,
Invalid(
err,
map[string]any{
"cause": err.Code,
"field": err.Field,
"value": err.Value,
},
),
)
}
return gqlErrors
}
var tenantAccessErr *authz.TenantAccessError
if errTyped, ok := err.(error); ok && errors.As(errTyped, &tenantAccessErr) {
return Unauthorized()
@@ -65,5 +87,6 @@ func asError(err any) error {
if e, ok := err.(error); ok {
return e
}
return errors.New("unknown panic")
}

View File

@@ -0,0 +1,201 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
// CustomType simulates types like gid.GID
type CustomType string
func TestCheckEach_EmptyTypedSlice(t *testing.T) {
v := New()
// Simulate what happens with []gid.GID{} (empty slice of custom type)
emptySlice := []CustomType{}
v.CheckEach(emptySlice, "items", func(index int, item any) {
// This callback should never be called for an empty slice
t.Error("callback should not be called for empty slice")
})
if v.HasErrors() {
t.Errorf("unexpected error for empty slice: %v", v.Error())
}
}
func TestCheckEach_NonEmptyTypedSlice(t *testing.T) {
v := New()
// Simulate what happens with []gid.GID{"abc", "def"}
slice := []CustomType{"abc", "def"}
callCount := 0
v.CheckEach(slice, "items", func(index int, item any) {
callCount++
// Verify the item is the correct type
str, ok := item.(CustomType)
if !ok {
t.Errorf("expected CustomType, got %T", item)
}
if index == 0 && str != "abc" {
t.Errorf("expected 'abc', got %s", str)
}
if index == 1 && str != "def" {
t.Errorf("expected 'def', got %s", str)
}
})
if callCount != 2 {
t.Errorf("expected callback to be called 2 times, got %d", callCount)
}
if v.HasErrors() {
t.Errorf("unexpected error: %v", v.Error())
}
}
func TestCheckEach_NilTypedSlice(t *testing.T) {
v := New()
// Simulate what happens with var x []gid.GID (nil slice)
var nilSlice []CustomType
v.CheckEach(nilSlice, "items", func(index int, item any) {
// This callback should never be called for a nil slice
t.Error("callback should not be called for nil slice")
})
if v.HasErrors() {
t.Errorf("unexpected error for nil slice: %v", v.Error())
}
}
func TestCheckEach_PointerToNonEmptySlice(t *testing.T) {
v := New()
// Simulate what happens with *[]gid.GID (pointer to slice)
slice := []CustomType{"abc", "def", "ghi"}
ptrToSlice := &slice
callCount := 0
v.CheckEach(ptrToSlice, "items", func(index int, item any) {
callCount++
str, ok := item.(CustomType)
if !ok {
t.Errorf("expected CustomType, got %T", item)
}
expectedValues := []CustomType{"abc", "def", "ghi"}
if str != expectedValues[index] {
t.Errorf("at index %d: expected %s, got %s", index, expectedValues[index], str)
}
})
if callCount != 3 {
t.Errorf("expected callback to be called 3 times, got %d", callCount)
}
if v.HasErrors() {
t.Errorf("unexpected error for pointer to slice: %v", v.Error())
}
}
func TestCheckEach_PointerToEmptySlice(t *testing.T) {
v := New()
// Simulate what happens with *[]gid.GID{} (pointer to empty slice)
slice := []CustomType{}
ptrToSlice := &slice
v.CheckEach(ptrToSlice, "items", func(index int, item any) {
t.Error("callback should not be called for empty slice")
})
if v.HasErrors() {
t.Errorf("unexpected error for pointer to empty slice: %v", v.Error())
}
}
func TestCheckEach_NilPointerToSlice(t *testing.T) {
v := New()
// Simulate what happens with var x *[]gid.GID (nil pointer to slice)
var nilPtrToSlice *[]CustomType
v.CheckEach(nilPtrToSlice, "items", func(index int, item any) {
t.Error("callback should not be called for nil pointer to slice")
})
if v.HasErrors() {
t.Errorf("unexpected error for nil pointer to slice: %v", v.Error())
}
}
func TestCheckEach_DoublePointerToSlice(t *testing.T) {
v := New()
// Simulate what happens with **[]gid.GID (double pointer to slice)
slice := []CustomType{"x", "y"}
ptrToSlice := &slice
doublePtrToSlice := &ptrToSlice
callCount := 0
v.CheckEach(doublePtrToSlice, "items", func(index int, item any) {
callCount++
str, ok := item.(CustomType)
if !ok {
t.Errorf("expected CustomType, got %T", item)
}
expectedValues := []CustomType{"x", "y"}
if str != expectedValues[index] {
t.Errorf("at index %d: expected %s, got %s", index, expectedValues[index], str)
}
})
if callCount != 2 {
t.Errorf("expected callback to be called 2 times, got %d", callCount)
}
if v.HasErrors() {
t.Errorf("unexpected error for double pointer to slice: %v", v.Error())
}
}
func TestCheckEach_NonSliceValue(t *testing.T) {
v := New()
// Pass a non-slice value
notASlice := "this is a string"
v.CheckEach(notASlice, "items", func(index int, item any) {
t.Error("callback should not be called for non-slice value")
})
if !v.HasErrors() {
t.Error("expected error for non-slice value")
}
errors := v.Errors()
if len(errors) != 1 {
t.Errorf("expected 1 error, got %d", len(errors))
}
if errors[0].Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, errors[0].Code)
}
if errors[0].Message != "expected a slice" {
t.Errorf("expected message 'expected a slice', got '%s'", errors[0].Message)
}
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator_test
import (
"testing"
"go.probo.inc/probo/pkg/validator"
)
func TestDoublePointerValidation(t *testing.T) {
t.Run("valid double pointer string", func(t *testing.T) {
v := validator.New()
str := "hello"
ptr := &str
doublePtr := &ptr
v.Check(doublePtr, "name", validator.Required(), validator.NotEmpty(), validator.MaxLen(1000))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Error())
}
})
t.Run("invalid double pointer string - empty", func(t *testing.T) {
v := validator.New()
str := ""
ptr := &str
doublePtr := &ptr
v.Check(doublePtr, "name", validator.Required(), validator.NotEmpty())
if !v.HasErrors() {
t.Error("expected errors for empty string")
}
})
t.Run("invalid double pointer string - too long", func(t *testing.T) {
v := validator.New()
str := "this is a very long string that exceeds the maximum length"
ptr := &str
doublePtr := &ptr
v.Check(doublePtr, "name", validator.Required(), validator.MaxLen(10))
if !v.HasErrors() {
t.Error("expected errors for string exceeding max length")
}
})
t.Run("optional double pointer - nil outer pointer", func(t *testing.T) {
v := validator.New()
var doublePtr **string = nil
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
if v.HasErrors() {
t.Errorf("expected no errors for nil optional field, got: %v", v.Error())
}
})
t.Run("optional double pointer - nil inner pointer", func(t *testing.T) {
v := validator.New()
var ptr *string = nil
doublePtr := &ptr
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
if v.HasErrors() {
t.Errorf("expected no errors for nil optional field, got: %v", v.Error())
}
})
t.Run("optional double pointer - valid value", func(t *testing.T) {
v := validator.New()
str := "hello"
ptr := &str
doublePtr := &ptr
v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Error())
}
})
}

107
pkg/validator/errors.go Normal file
View File

@@ -0,0 +1,107 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"strings"
)
type ErrorCode string
const (
ErrorCodeRequired ErrorCode = "REQUIRED"
ErrorCodeInvalidFormat ErrorCode = "INVALID_FORMAT"
ErrorCodeOutOfRange ErrorCode = "OUT_OF_RANGE"
ErrorCodeTooShort ErrorCode = "TOO_SHORT"
ErrorCodeTooLong ErrorCode = "TOO_LONG"
ErrorCodeInvalidEmail ErrorCode = "INVALID_EMAIL"
ErrorCodeInvalidURL ErrorCode = "INVALID_URL"
ErrorCodeInvalidEnum ErrorCode = "INVALID_ENUM"
ErrorCodeInvalidGID ErrorCode = "INVALID_GID"
ErrorCodeUnsafeContent ErrorCode = "UNSAFE_CONTENT"
ErrorCodeCustom ErrorCode = "CUSTOM"
)
type ValidationError struct {
Field string
Code ErrorCode
Message string
Value any
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s %s", e.Field, e.Message)
}
type ValidationErrors []*ValidationError
func (ve ValidationErrors) Error() string {
if len(ve) == 0 {
return ""
}
var messages []string
for _, err := range ve {
messages = append(messages, err.Error())
}
return strings.Join(messages, "; ")
}
func (ve ValidationErrors) HasErrors() bool {
return len(ve) > 0
}
func (ve ValidationErrors) Fields() []string {
fields := make([]string, 0, len(ve))
for _, err := range ve {
fields = append(fields, err.Field)
}
return fields
}
func (ve ValidationErrors) ByField(field string) ValidationErrors {
var errors ValidationErrors
for _, err := range ve {
if err.Field == field {
errors = append(errors, err)
}
}
return errors
}
func (ve ValidationErrors) ByCode(code ErrorCode) ValidationErrors {
var errors ValidationErrors
for _, err := range ve {
if err.Code == code {
errors = append(errors, err)
}
}
return errors
}
func (ve ValidationErrors) First() *ValidationError {
if len(ve) == 0 {
return nil
}
return ve[0]
}
func newValidationError(code ErrorCode, message string) *ValidationError {
return &ValidationError{
Code: code,
Message: message,
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
// AssetType simulates coredata.AssetType
type AssetType string
const (
AssetTypePhysical AssetType = "PHYSICAL"
AssetTypeVirtual AssetType = "VIRTUAL"
)
func (at AssetType) String() string {
return string(at)
}
func TestOneOf_CustomStringType(t *testing.T) {
tests := []struct {
name string
value any
allowed []string
expectError bool
}{
{
name: "valid custom type - physical",
value: AssetTypePhysical,
allowed: []string{"PHYSICAL", "VIRTUAL"},
expectError: false,
},
{
name: "valid custom type - virtual",
value: AssetTypeVirtual,
allowed: []string{"PHYSICAL", "VIRTUAL"},
expectError: false,
},
{
name: "invalid custom type",
value: AssetType("INVALID"),
allowed: []string{"PHYSICAL", "VIRTUAL"},
expectError: true,
},
{
name: "custom type not in allowed list",
value: AssetTypePhysical,
allowed: []string{"VIRTUAL"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := New()
v.Check(tt.value, "asset_type", OneOfSlice(tt.allowed))
if tt.expectError {
if !v.HasErrors() {
t.Error("expected error but got none")
}
} else {
if v.HasErrors() {
t.Errorf("unexpected error: %v", v.Error())
}
}
})
}
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"go.probo.inc/probo/pkg/gid"
)
// CustomStringType simulates coredata.AssetType
type CustomStringType string
func (c CustomStringType) String() string {
return string(c)
}
func TestOptional_WithGIDPointer(t *testing.T) {
tenantID := gid.NewTenantID()
tests := []struct {
name string
value *gid.GID
expectError bool
}{
{
name: "nil pointer - should skip validation",
value: nil,
expectError: false,
},
{
name: "valid GID pointer",
value: func() *gid.GID {
g := gid.New(tenantID, 100)
return &g
}(),
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := New()
v.Check(tt.value, "owner_id", GID(100))
if tt.expectError {
if !v.HasErrors() {
t.Error("expected error but got none")
}
} else {
if v.HasErrors() {
t.Errorf("unexpected error: %v", v.Error())
}
}
})
}
}
func TestOptional_WithCustomTypePointer(t *testing.T) {
tests := []struct {
name string
value *CustomStringType
expectError bool
}{
{
name: "nil pointer - should skip validation",
value: nil,
expectError: false,
},
{
name: "valid custom type pointer",
value: func() *CustomStringType {
v := CustomStringType("VALID")
return &v
}(),
expectError: false,
},
{
name: "invalid custom type pointer",
value: func() *CustomStringType {
v := CustomStringType("INVALID")
return &v
}(),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := New()
v.Check(tt.value, "asset_type", OneOf("VALID", "ANOTHER"))
if tt.expectError {
if !v.HasErrors() {
t.Error("expected error but got none")
}
} else {
if v.HasErrors() {
t.Errorf("unexpected error: %v", v.Error())
}
}
})
}
}

149
pkg/validator/validation.go Normal file
View File

@@ -0,0 +1,149 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"reflect"
)
type Validator struct {
errors ValidationErrors
}
func New() *Validator {
return &Validator{
errors: ValidationErrors{},
}
}
func (v *Validator) Check(value any, field string, validators ...ValidatorFunc) {
if len(validators) == 0 {
return
}
// Dereference pointer values to get the actual value for validation
actualValue := value
if value != nil {
val := reflect.ValueOf(value)
// Dereference all pointer levels
for val.Kind() == reflect.Ptr && !val.IsNil() {
val = val.Elem()
actualValue = val.Interface()
}
// If we ended up with a nil pointer at any level, set actualValue to nil
if val.Kind() == reflect.Ptr && val.IsNil() {
actualValue = nil
}
}
for _, validator := range validators {
if err := validator(actualValue); err != nil {
v.errors = append(v.errors, &ValidationError{
Field: field,
Code: err.Code,
Message: err.Message,
Value: value,
})
}
}
}
func (v *Validator) CheckEach(items any, field string, fn func(index int, item any)) {
if items == nil {
return
}
if slice, ok := items.([]any); ok {
for i, item := range slice {
fn(i, item)
}
return
}
val := reflect.ValueOf(items)
// Dereference pointer levels to get to the actual slice
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return
}
val = val.Elem()
}
if val.Kind() != reflect.Slice {
v.errors = append(v.errors, &ValidationError{
Field: field,
Code: ErrorCodeInvalidFormat,
Message: "expected a slice",
Value: items,
})
return
}
for i := 0; i < val.Len(); i++ {
fn(i, val.Index(i).Interface())
}
}
func (v *Validator) CheckNested(field string, fn func(v *Validator)) {
nestedValidator := New()
fn(nestedValidator)
for _, err := range nestedValidator.errors {
prefixedErr := &ValidationError{
Field: fmt.Sprintf("%s.%s", field, err.Field),
Code: err.Code,
Message: err.Message,
Value: err.Value,
}
v.errors = append(v.errors, prefixedErr)
}
}
func (v *Validator) HasErrors() bool {
return len(v.errors) > 0
}
func (v *Validator) Errors() ValidationErrors {
return v.errors
}
func (v *Validator) Error() error {
if len(v.errors) == 0 {
return nil
}
return v.errors
}
type ValidatorFunc func(value any) *ValidationError
// dereferenceValue recursively dereferences all pointer levels.
// Returns the final dereferenced value and a boolean indicating if any pointer in the chain was nil.
func dereferenceValue(value any) (any, bool) {
if value == nil {
return nil, true
}
val := reflect.ValueOf(value)
// Dereference all pointer levels
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil, true
}
val = val.Elem()
}
return val.Interface(), false
}

View File

@@ -0,0 +1,425 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"time"
)
func BenchmarkValidate_SingleField(b *testing.B) {
email := "test@example.com"
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&email, "email", Required(), Email())
}
}
func BenchmarkValidate_MultipleFields(b *testing.B) {
email := "test@example.com"
password := "password123"
age := 25
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&email, "email", Required(), Email())
v.Check(&password, "password", Required(), MinLen(8))
v.Check(&age, "age", Min(18), Max(120))
}
}
func BenchmarkValidate_OptionalField(b *testing.B) {
var website *string
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(website, "website", URL())
}
}
func BenchmarkValidate_NestedStruct(b *testing.B) {
type Address struct {
City string
ZipCode string
}
type User struct {
Name string
Address Address
}
user := User{
Name: "John Doe",
Address: Address{
City: "New York",
ZipCode: "10001",
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&user.Name, "name", Required())
v.CheckNested("address", func(av *Validator) {
av.Check(&user.Address.City, "city", Required())
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, ""))
})
}
}
func BenchmarkValidate_ArrayValidation(b *testing.B) {
type Item struct {
Name string
Price int
}
items := []Item{
{Name: "Item 1", Price: 100},
{Name: "Item 2", Price: 200},
{Name: "Item 3", Price: 300},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
for j, item := range items {
v.CheckNested("items[0]", func(iv *Validator) {
iv.Check(&item.Name, "name", Required())
iv.Check(&item.Price, "price", Min(0))
_ = j
})
}
}
}
func BenchmarkEmail(b *testing.B) {
email := "test@example.com"
validator := Email()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&email)
}
}
func BenchmarkURL(b *testing.B) {
urlStr := "https://example.com"
validator := URL()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&urlStr)
}
}
func BenchmarkUUID(b *testing.B) {
uuid := "550e8400-e29b-41d4-a716-446655440000"
validator := UUID()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&uuid)
}
}
func BenchmarkMinLen(b *testing.B) {
str := "hello world"
validator := MinLen(5)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkMin(b *testing.B) {
num := 42
validator := Min(18)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&num)
}
}
func BenchmarkMinFloat(b *testing.B) {
num := 99.99
validator := MinFloat(0.01)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&num)
}
}
func BenchmarkMaxFloat(b *testing.B) {
num := 50.50
validator := MaxFloat(99.99)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&num)
}
}
func BenchmarkRangeFloat(b *testing.B) {
num := 50.50
validator := RangeFloat(0.01, 99.99)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&num)
}
}
func BenchmarkNotEmpty(b *testing.B) {
str := "hello world"
validator := NotEmpty()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkPattern(b *testing.B) {
zipCode := "12345"
validator := Pattern(`^\d{5}$`, "")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&zipCode)
}
}
func BenchmarkValidate_WithErrors(b *testing.B) {
email := "invalid-email"
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&email, "email", Required(), Email())
if !v.HasErrors() {
b.Fatal("expected validation error")
}
}
}
func BenchmarkValidate_ComplexForm(b *testing.B) {
type Address struct {
Street string
City string
ZipCode string
}
type User struct {
Email string
Name string
Age int
Website *string
PhoneNumber *string
Price float64
Address Address
}
website := "https://example.com"
user := User{
Email: "user@example.com",
Name: "John Doe",
Age: 30,
Website: &website,
PhoneNumber: nil,
Price: 99.99,
Address: Address{
Street: "123 Main St",
City: "New York",
ZipCode: "10001",
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
v := New()
v.Check(&user.Email, "email", Required(), Email())
v.Check(&user.Name, "name", Required(), MinLen(2))
v.Check(&user.Age, "age", Min(18), Max(120))
v.Check(user.Website, "website", URL())
v.Check(user.PhoneNumber, "phoneNumber", MinLen(10))
v.Check(&user.Price, "price", MinFloat(0.01))
v.CheckNested("address", func(av *Validator) {
av.Check(&user.Address.Street, "street", Required())
av.Check(&user.Address.City, "city", Required())
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, ""))
})
}
}
func BenchmarkMinItems(b *testing.B) {
items := []string{"a", "b", "c"}
validator := MinItems(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&items)
}
}
func BenchmarkMaxItems(b *testing.B) {
items := []string{"a", "b", "c"}
validator := MaxItems(5)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&items)
}
}
func BenchmarkUniqueItems(b *testing.B) {
items := []string{"a", "b", "c"}
validator := UniqueItems()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&items)
}
}
func BenchmarkAlphaNumeric(b *testing.B) {
str := "abc123DEF456"
validator := AlphaNumeric()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkNoSpaces(b *testing.B) {
str := "hello-world-test"
validator := NoSpaces()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkSlug(b *testing.B) {
str := "hello-world-123"
validator := Slug()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkAfter(b *testing.B) {
now := time.Now()
future := now.Add(24 * time.Hour)
validator := After(now)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&future)
}
}
func BenchmarkBefore(b *testing.B) {
now := time.Now()
past := now.Add(-24 * time.Hour)
validator := Before(now)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&past)
}
}
func BenchmarkFutureDate(b *testing.B) {
future := time.Now().Add(24 * time.Hour)
validator := FutureDate()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&future)
}
}
func BenchmarkPastDate(b *testing.B) {
past := time.Now().Add(-24 * time.Hour)
validator := PastDate()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&past)
}
}
func BenchmarkEqualTo(b *testing.B) {
str1 := "password"
str2 := "password"
validator := EqualTo(&str2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str1)
}
}
func BenchmarkNotEqualTo(b *testing.B) {
str1 := "password"
str2 := "different"
validator := NotEqualTo(&str2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str1)
}
}
func BenchmarkDomain(b *testing.B) {
str := "api.example.com"
validator := Domain()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkHTTPUrl(b *testing.B) {
str := "http://api.example.com/v1/users"
validator := HTTPUrl()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}
func BenchmarkHTTPSUrl(b *testing.B) {
str := "https://api.example.com/v1/users"
validator := HTTPSUrl()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(&str)
}
}

View File

@@ -0,0 +1,451 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"testing"
"go.gearno.de/x/ref"
)
func TestValidator_Validate(t *testing.T) {
t.Run("single field validation", func(t *testing.T) {
v := New()
email := "test@example.com"
v.Check(&email, "email", Required(), Email())
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("multiple field validations", func(t *testing.T) {
v := New()
email := ""
password := "123"
v.Check(&email, "email", Required(), Email())
v.Check(&password, "password", Required(), MinLen(8))
if !v.HasErrors() {
t.Error("expected validation errors")
}
errors := v.Errors()
// email: 1 error (Required), password: 1 error (MinLen - Required passes because it's not empty)
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d: %v", len(errors), errors)
}
})
t.Run("collect multiple errors for same field", func(t *testing.T) {
v := New()
value := "abc"
v.Check(&value, "password", MinLen(8), MaxLen(5))
errors := v.Errors()
// Both MinLen and MaxLen will fail (too short and somehow conflicts, but logically MinLen will fail)
if len(errors) < 1 {
t.Errorf("expected at least 1 error, got %d", len(errors))
}
})
}
func TestValidator_CheckNested(t *testing.T) {
v := New()
v.CheckNested("user", func(nv *Validator) {
email := "invalid"
nv.Check(&email, "email", Email())
nv.CheckNested("address", func(av *Validator) {
city := ""
av.Check(&city, "city", Required())
})
})
if !v.HasErrors() {
t.Error("expected validation errors")
}
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d", len(errors))
}
// Check field paths
expectedFields := map[string]bool{
"user.email": true,
"user.address.city": true,
}
for _, err := range errors {
if !expectedFields[err.Field] {
t.Errorf("unexpected field path: %s", err.Field)
}
}
}
func TestValidator_Error(t *testing.T) {
t.Run("no errors", func(t *testing.T) {
v := New()
if v.Error() != nil {
t.Errorf("expected nil error, got: %v", v.Error())
}
})
t.Run("with errors", func(t *testing.T) {
v := New()
email := ""
v.Check(&email, "email", Required())
err := v.Error()
if err == nil {
t.Error("expected error, got nil")
}
})
}
func TestValidationErrors_Methods(t *testing.T) {
errors := ValidationErrors{
{Field: "email", Code: ErrorCodeInvalidEmail, Message: "invalid email"},
{Field: "password", Code: ErrorCodeTooShort, Message: "too short"},
{Field: "email", Code: ErrorCodeRequired, Message: "required"},
}
t.Run("Fields", func(t *testing.T) {
fields := errors.Fields()
if len(fields) != 3 {
t.Errorf("expected 3 fields, got %d", len(fields))
}
})
t.Run("ByField", func(t *testing.T) {
emailErrors := errors.ByField("email")
if len(emailErrors) != 2 {
t.Errorf("expected 2 email errors, got %d", len(emailErrors))
}
})
t.Run("ByCode", func(t *testing.T) {
requiredErrors := errors.ByCode(ErrorCodeRequired)
if len(requiredErrors) != 1 {
t.Errorf("expected 1 required error, got %d", len(requiredErrors))
}
})
t.Run("First", func(t *testing.T) {
first := errors.First()
if first == nil {
t.Error("expected first error")
}
if first.Field != "email" {
t.Errorf("expected first field to be 'email', got '%s'", first.Field)
}
})
t.Run("Error", func(t *testing.T) {
errorStr := errors.Error()
if errorStr == "" {
t.Error("expected non-empty error string")
}
})
}
func TestOptionalFieldExample(t *testing.T) {
type CreateUserRequest struct {
Email string
Name string
Website *string
PhoneNumber *string
Age *int
}
website := "not-a-url"
req := CreateUserRequest{
Email: "user@example.com",
Name: "John Doe",
Website: &website,
PhoneNumber: nil,
Age: nil,
}
v := New()
v.Check(&req.Email, "email", Required(), Email())
v.Check(&req.Name, "name", Required(), MinLen(2))
v.Check(req.Website, "website", URL())
v.Check(req.PhoneNumber, "phoneNumber", MinLen(10))
v.Check(req.Age, "age", Min(18), Max(120))
if !v.HasErrors() {
t.Fatal("expected validation errors")
}
errors := v.Errors()
websiteErr := errors.ByField("website")
if len(websiteErr) != 1 {
t.Errorf("expected 1 website error, got %d", len(websiteErr))
}
phoneErr := errors.ByField("phoneNumber")
if len(phoneErr) != 0 {
t.Errorf("expected 0 phoneNumber errors (nil should be skipped), got %d", len(phoneErr))
}
ageErr := errors.ByField("age")
if len(ageErr) != 0 {
t.Errorf("expected 0 age errors (nil should be skipped), got %d", len(ageErr))
}
t.Logf("Optional field validation errors: %s", errors.Error())
}
func TestRealWorldExample(t *testing.T) {
// Simulate a user registration form
type Address struct {
City string
ZipCode string
}
type User struct {
Email string
Password string
Age int
Website *string
Address Address
}
user := User{
Email: "invalid-email",
Password: "123",
Age: 15,
Website: ref.Ref("not-a-url"),
Address: Address{
City: "",
ZipCode: "12345",
},
}
v := New()
// Validate user fields
v.Check(&user.Email, "email", Required(), Email())
v.Check(&user.Password, "password", Required(), MinLen(8))
v.Check(&user.Age, "age", Min(18), Max(120))
v.Check(user.Website, "website", URL())
// Validate nested address
v.CheckNested("address", func(av *Validator) {
av.Check(&user.Address.City, "city", Required())
av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, "must be 5 digits"))
})
if !v.HasErrors() {
t.Fatal("expected validation errors")
}
errors := v.Errors()
expectedErrors := map[string]ErrorCode{
"email": ErrorCodeInvalidEmail,
"password": ErrorCodeTooShort,
"age": ErrorCodeOutOfRange,
"website": ErrorCodeInvalidURL,
"address.city": ErrorCodeRequired,
}
// Check that we have the expected errors
for field, expectedCode := range expectedErrors {
found := false
for _, err := range errors {
if err.Field == field && err.Code == expectedCode {
found = true
break
}
}
if !found {
t.Errorf("expected error for field '%s' with code '%s'", field, expectedCode)
}
}
// Print errors for debugging
t.Logf("Validation errors: %s", errors.Error())
}
func TestArrayValidation(t *testing.T) {
type Item struct {
Name string
Price int
}
items := []Item{
{Name: "", Price: -10},
{Name: "Valid", Price: 100},
{Name: "X", Price: 10},
}
v := New()
// Validate each item
for i, item := range items {
field := fmt.Sprintf("items[%d]", i)
v.CheckNested(field, func(iv *Validator) {
iv.Check(&item.Name, "name", Required(), MinLen(2))
iv.Check(&item.Price, "price", Min(0))
})
}
if !v.HasErrors() {
t.Fatal("expected validation errors")
}
errors := v.Errors()
// Check for specific field paths
expectedFields := []string{
"items[0].name",
"items[0].price",
"items[2].name",
}
for _, expectedField := range expectedFields {
found := false
for _, err := range errors {
if err.Field == expectedField {
found = true
break
}
}
if !found {
t.Errorf("expected error for field '%s'", expectedField)
}
}
t.Logf("Array validation errors: %s", errors.Error())
}
func TestDuplicateValidators(t *testing.T) {
t.Run("duplicate MinLen creates two errors", func(t *testing.T) {
v := New()
name := "abc"
v.Check(&name, "name", MinLen(5), MinLen(5))
if !v.HasErrors() {
t.Error("expected validation errors")
}
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors (one per MinLen), got %d", len(errors))
}
if errors[0].Message != "must be at least 5 characters" {
t.Errorf("unexpected first error: %s", errors[0].Message)
}
if errors[1].Message != "must be at least 5 characters" {
t.Errorf("unexpected second error: %s", errors[1].Message)
}
})
t.Run("duplicate Required creates two errors", func(t *testing.T) {
v := New()
name := ""
v.Check(&name, "name", Required(), Required())
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d", len(errors))
}
})
t.Run("duplicate Email creates two errors", func(t *testing.T) {
v := New()
email := "invalid"
v.Check(&email, "email", Email(), Email())
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d", len(errors))
}
})
t.Run("same validator with different parameters", func(t *testing.T) {
v := New()
name := "test"
v.Check(&name, "name", MinLen(5), MinLen(10))
errors := v.Errors()
if len(errors) != 2 {
t.Errorf("expected 2 errors, got %d", len(errors))
}
if errors[0].Message != "must be at least 5 characters" {
t.Errorf("unexpected first error: %s", errors[0].Message)
}
if errors[1].Message != "must be at least 10 characters" {
t.Errorf("unexpected second error: %s", errors[1].Message)
}
})
}
func TestStandardErrorPattern(t *testing.T) {
// Simulates a typical validation function
validateUser := func(email, password string) error {
v := New()
v.Check(&email, "email", Required(), Email())
v.Check(&password, "password", Required(), MinLen(8))
return v.Error()
}
t.Run("valid data returns nil", func(t *testing.T) {
err := validateUser("user@example.com", "password123")
if err != nil {
t.Errorf("expected nil, got: %v", err)
}
})
t.Run("invalid data returns ValidationErrors as error", func(t *testing.T) {
err := validateUser("", "123")
if err == nil {
t.Fatal("expected validation errors")
}
// Standard error handling
t.Logf("validation failed: %v", err)
// Can get detailed errors if needed
if validationErrs, ok := err.(ValidationErrors); ok {
for _, e := range validationErrs {
t.Logf(" - %s: %s (code: %s)", e.Field, e.Message, e.Code)
}
// Can use helper methods
emailErrs := validationErrs.ByField("email")
if len(emailErrs) != 1 {
t.Errorf("expected 1 email error, got %d", len(emailErrs))
}
passwordErrs := validationErrs.ByField("password")
if len(passwordErrs) != 1 {
t.Errorf("expected 1 password error, got %d", len(passwordErrs))
}
} else {
t.Error("expected ValidationErrors type")
}
})
}

View File

@@ -0,0 +1,109 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"reflect"
)
// MinItems validates that a slice or array has at least the specified minimum number of items.
func MinItems(min int) ValidatorFunc {
return func(value any) *ValidationError {
v := reflect.ValueOf(value)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
}
if v.Len() < min {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must contain at least %d items", min),
)
}
return nil
}
}
// MaxItems validates that a slice or array does not exceed the specified maximum number of items.
func MaxItems(max int) ValidatorFunc {
return func(value any) *ValidationError {
v := reflect.ValueOf(value)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
}
if v.Len() > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must contain at most %d items", max),
)
}
return nil
}
}
// UniqueItems validates that all items in a slice or array are unique.
func UniqueItems() ValidatorFunc {
return func(value any) *ValidationError {
v := reflect.ValueOf(value)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array")
}
if v.Len() == 0 {
return nil
}
elemType := v.Type().Elem()
if !elemType.Comparable() {
return newValidationError(ErrorCodeInvalidFormat, "cannot validate uniqueness for non-comparable types")
}
seen := make(map[any]bool)
for i := 0; i < v.Len(); i++ {
item := v.Index(i).Interface()
if seen[item] {
return newValidationError(ErrorCodeInvalidFormat, "items must be unique")
}
seen[item] = true
}
return nil
}
}

View File

@@ -0,0 +1,220 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
func TestMinItems(t *testing.T) {
t.Run("valid slice", func(t *testing.T) {
items := []string{"a", "b", "c"}
err := MinItems(2)(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("exact minimum", func(t *testing.T) {
items := []int{1, 2}
err := MinItems(2)(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("too few items", func(t *testing.T) {
items := []string{"a"}
err := MinItems(2)(&items)
if err == nil {
t.Error("expected validation error")
}
if err.Code != ErrorCodeOutOfRange {
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
}
})
t.Run("nil slice", func(t *testing.T) {
var items *[]string
err := MinItems(2)(items)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
t.Run("non-slice value", func(t *testing.T) {
value := "not a slice"
err := MinItems(2)(&value)
if err == nil || err.Code != ErrorCodeInvalidFormat {
t.Error("expected invalid format error")
}
})
}
func TestMaxItems(t *testing.T) {
t.Run("valid slice", func(t *testing.T) {
items := []string{"a", "b"}
err := MaxItems(5)(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("exact maximum", func(t *testing.T) {
items := []int{1, 2, 3}
err := MaxItems(3)(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("too many items", func(t *testing.T) {
items := []string{"a", "b", "c", "d"}
err := MaxItems(2)(&items)
if err == nil {
t.Error("expected validation error")
}
if err.Code != ErrorCodeOutOfRange {
t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code)
}
})
t.Run("nil slice", func(t *testing.T) {
var items *[]string
err := MaxItems(2)(items)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestUniqueItems(t *testing.T) {
t.Run("unique items", func(t *testing.T) {
items := []string{"a", "b", "c"}
err := UniqueItems()(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("duplicate items", func(t *testing.T) {
items := []string{"a", "b", "a"}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
})
t.Run("unique integers", func(t *testing.T) {
items := []int{1, 2, 3}
err := UniqueItems()(&items)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("duplicate integers", func(t *testing.T) {
items := []int{1, 2, 1}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("nil slice", func(t *testing.T) {
var items *[]string
err := UniqueItems()(items)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
t.Run("empty slice", func(t *testing.T) {
items := []string{}
err := UniqueItems()(&items)
if err != nil {
t.Errorf("expected no error for empty slice, got: %v", err)
}
})
t.Run("non-comparable type - slice of slices", func(t *testing.T) {
items := [][]int{{1, 2}, {3, 4}}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error for non-comparable type")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
if err.Message != "cannot validate uniqueness for non-comparable types" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("non-comparable type - slice of maps", func(t *testing.T) {
items := []map[string]int{{"a": 1}, {"b": 2}}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error for non-comparable type")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
})
t.Run("non-comparable type - struct with slice field", func(t *testing.T) {
type NonComparable struct {
Items []int
}
items := []NonComparable{{Items: []int{1, 2}}, {Items: []int{3, 4}}}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error for non-comparable type")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
})
t.Run("comparable struct with unique values", func(t *testing.T) {
type ComparableStruct struct {
ID int
Name string
}
items := []ComparableStruct{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}}
err := UniqueItems()(&items)
if err != nil {
t.Errorf("expected no error for comparable structs, got: %v", err)
}
})
t.Run("comparable struct with duplicate values", func(t *testing.T) {
type ComparableStruct struct {
ID int
Name string
}
items := []ComparableStruct{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}, {ID: 1, Name: "a"}}
err := UniqueItems()(&items)
if err == nil {
t.Error("expected validation error for duplicate comparable structs")
}
if err.Code != ErrorCodeInvalidFormat {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code)
}
})
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"reflect"
"strings"
)
// Required validates that a field has a value.
// For strings, it also checks that the value is not empty or just whitespace.
// For slices, it checks that the slice is not empty.
func Required() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return newValidationError(ErrorCodeRequired, "field is required")
}
switch v := actualValue.(type) {
case string:
if strings.TrimSpace(v) == "" {
return newValidationError(ErrorCodeRequired, "field is required")
}
default:
rv := reflect.ValueOf(actualValue)
if rv.Kind() == reflect.Slice && rv.Len() == 0 {
return newValidationError(ErrorCodeRequired, "field is required")
}
}
return nil
}
}
// NotEmpty validates that a field is not empty.
// Similar to Required, but can be used independently.
func NotEmpty() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
switch v := actualValue.(type) {
case string:
if strings.TrimSpace(v) == "" {
return newValidationError(ErrorCodeRequired, "field cannot be empty")
}
default:
rv := reflect.ValueOf(actualValue)
if rv.Kind() == reflect.Slice && rv.Len() == 0 {
return newValidationError(ErrorCodeRequired, "field cannot be empty")
}
}
return nil
}
}

View File

@@ -0,0 +1,270 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
func TestOptionalByDefault(t *testing.T) {
t.Run("nil value skips validation by default", func(t *testing.T) {
v := New()
v.Check(nil, "field", MinLen(5))
if v.HasErrors() {
t.Errorf("expected no errors for nil (optional by default), got: %v", v.Errors())
}
})
t.Run("nil pointer skips validation by default", func(t *testing.T) {
v := New()
var str *string
v.Check(str, "field", MinLen(5))
if v.HasErrors() {
t.Errorf("expected no errors for nil pointer (optional by default), got: %v", v.Errors())
}
})
t.Run("valid value passes validation", func(t *testing.T) {
v := New()
str := "hello world"
v.Check(&str, "field", MinLen(5))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("invalid value fails validation", func(t *testing.T) {
v := New()
str := "hi"
v.Check(&str, "field", MinLen(5))
if !v.HasErrors() {
t.Error("expected validation error")
}
})
t.Run("multiple validators", func(t *testing.T) {
v := New()
str := "hello"
v.Check(&str, "field", MinLen(3), MaxLen(10))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("empty string is not nil and gets validated", func(t *testing.T) {
v := New()
str := ""
v.Check(&str, "field", MinLen(5))
if !v.HasErrors() {
t.Error("expected validation error for empty string")
}
})
t.Run("Required() validates nil values", func(t *testing.T) {
v := New()
var str *string
v.Check(str, "field", Required())
if !v.HasErrors() {
t.Error("expected validation error for nil with Required()")
}
})
}
func TestRequired(t *testing.T) {
t.Run("valid string", func(t *testing.T) {
str := "hello"
err := Required()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := Required()(&str)
if err == nil {
t.Error("expected validation error")
}
if err.Code != ErrorCodeRequired {
t.Errorf("expected error code %s, got %s", ErrorCodeRequired, err.Code)
}
})
t.Run("whitespace string", func(t *testing.T) {
str := " "
err := Required()(&str)
if err == nil {
t.Error("expected validation error for whitespace")
}
})
t.Run("nil string pointer", func(t *testing.T) {
var str *string
err := Required()(str)
if err == nil {
t.Error("expected validation error for nil pointer")
}
})
t.Run("valid string pointer", func(t *testing.T) {
str := "hello"
err := Required()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("nil interface", func(t *testing.T) {
err := Required()(nil)
if err == nil {
t.Error("expected validation error for nil")
}
})
t.Run("zero int", func(t *testing.T) {
num := 0
err := Required()(&num)
if err != nil {
t.Errorf("expected no error for zero int, got: %v", err)
}
})
t.Run("positive int", func(t *testing.T) {
num := 42
err := Required()(&num)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("nil int pointer", func(t *testing.T) {
var num *int
err := Required()(num)
if err == nil {
t.Error("expected validation error for nil int pointer")
}
})
t.Run("valid int pointer", func(t *testing.T) {
num := 42
err := Required()(&num)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("empty slice", func(t *testing.T) {
slice := []any{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty slice")
}
})
t.Run("non-empty slice", func(t *testing.T) {
slice := []any{1, 2, 3}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("empty string slice", func(t *testing.T) {
slice := []string{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty []string slice")
}
if err.Code != ErrorCodeRequired {
t.Errorf("expected error code %s, got %s", ErrorCodeRequired, err.Code)
}
})
t.Run("non-empty string slice", func(t *testing.T) {
slice := []string{"a", "b", "c"}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error for non-empty []string, got: %v", err)
}
})
t.Run("empty int slice", func(t *testing.T) {
slice := []int{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty []int slice")
}
if err.Code != ErrorCodeRequired {
t.Errorf("expected error code %s, got %s", ErrorCodeRequired, err.Code)
}
})
t.Run("non-empty int slice", func(t *testing.T) {
slice := []int{1, 2, 3}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error for non-empty []int, got: %v", err)
}
})
t.Run("empty custom type slice", func(t *testing.T) {
type CustomType struct {
ID int
}
slice := []CustomType{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty custom type slice")
}
if err.Code != ErrorCodeRequired {
t.Errorf("expected error code %s, got %s", ErrorCodeRequired, err.Code)
}
})
t.Run("non-empty custom type slice", func(t *testing.T) {
type CustomType struct {
ID int
}
slice := []CustomType{{ID: 1}, {ID: 2}}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error for non-empty custom type slice, got: %v", err)
}
})
t.Run("empty pointer slice", func(t *testing.T) {
slice := []*string{}
err := Required()(slice)
if err == nil {
t.Error("expected validation error for empty []*string slice")
}
})
t.Run("non-empty pointer slice", func(t *testing.T) {
str1, str2 := "a", "b"
slice := []*string{&str1, &str2}
err := Required()(slice)
if err != nil {
t.Errorf("expected no error for non-empty []*string, got: %v", err)
}
})
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"reflect"
"time"
)
// EqualTo validates that a value equals another value using deep equality.
// Special handling for time.Time to compare instants rather than internal structure.
func EqualTo(other any) ValidatorFunc {
return func(value any) *ValidationError {
if !areEqual(value, other) {
return newValidationError(ErrorCodeInvalidFormat, "values must match")
}
return nil
}
}
// NotEqualTo validates that a value does not equal another value using deep equality.
// Special handling for time.Time to compare instants rather than internal structure.
func NotEqualTo(other any) ValidatorFunc {
return func(value any) *ValidationError {
if areEqual(value, other) {
return newValidationError(ErrorCodeInvalidFormat, "values must not match")
}
return nil
}
}
// areEqual compares two values for equality with special handling for time.Time.
func areEqual(a, b any) bool {
// Dereference both values
aVal, aIsNil := dereferenceValue(a)
bVal, bIsNil := dereferenceValue(b)
// If both are nil, they're equal
if aIsNil && bIsNil {
return true
}
// If only one is nil, they're not equal
if aIsNil || bIsNil {
return false
}
// Special handling for time.Time
aTime, aIsTime := aVal.(time.Time)
bTime, bIsTime := bVal.(time.Time)
if aIsTime && bIsTime {
// Use time.Time.Equal() which compares the instant, ignoring location and monotonic clock
return aTime.Equal(bTime)
}
// Fall back to reflect.DeepEqual for all other types
return reflect.DeepEqual(aVal, bVal)
}

View File

@@ -0,0 +1,182 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"time"
)
func TestEqualTo(t *testing.T) {
t.Run("equal strings", func(t *testing.T) {
str1 := "password"
str2 := "password"
err := EqualTo(&str2)(&str1)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("different strings", func(t *testing.T) {
str1 := "password"
str2 := "different"
err := EqualTo(&str2)(&str1)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("equal integers", func(t *testing.T) {
num1 := 42
num2 := 42
err := EqualTo(&num2)(&num1)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("different integers", func(t *testing.T) {
num1 := 42
num2 := 43
err := EqualTo(&num2)(&num1)
if err == nil {
t.Error("expected validation error")
}
})
}
func TestNotEqualTo(t *testing.T) {
t.Run("different strings", func(t *testing.T) {
str1 := "password"
str2 := "different"
err := NotEqualTo(&str2)(&str1)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("equal strings", func(t *testing.T) {
str1 := "password"
str2 := "password"
err := NotEqualTo(&str2)(&str1)
if err == nil {
t.Error("expected validation error")
}
})
}
func TestEqualTo_TimeComparison(t *testing.T) {
t.Run("same instant same location", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
err := EqualTo(time2)(time1)
if err != nil {
t.Errorf("expected no error for same instant, got: %v", err)
}
})
t.Run("same instant different location", func(t *testing.T) {
// Create the same instant in different time zones
utcTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
est, _ := time.LoadLocation("America/New_York")
estTime := time.Date(2025, 11, 5, 7, 0, 0, 0, est) // 7am EST = 12pm UTC
err := EqualTo(utcTime)(estTime)
if err != nil {
t.Errorf("expected no error for same instant in different locations, got: %v", err)
}
})
t.Run("different instants same location", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 13, 0, 0, 0, time.UTC)
err := EqualTo(time2)(time1)
if err == nil {
t.Error("expected validation error for different instants")
}
})
t.Run("pointer to time same instant", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
err := EqualTo(&time2)(&time1)
if err != nil {
t.Errorf("expected no error for pointer to same instant, got: %v", err)
}
})
t.Run("nil time pointers", func(t *testing.T) {
var time1 *time.Time
var time2 *time.Time
err := EqualTo(time2)(time1)
if err != nil {
t.Errorf("expected no error for nil time pointers, got: %v", err)
}
})
t.Run("one nil one non-nil time pointer", func(t *testing.T) {
var time1 *time.Time
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
err := EqualTo(&time2)(time1)
if err == nil {
t.Error("expected validation error for nil vs non-nil time")
}
})
t.Run("same instant with monotonic clock difference", func(t *testing.T) {
// Simulate times with different monotonic clock data
baseTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time1 := baseTime
time.Sleep(1 * time.Millisecond) // Advances monotonic clock
time2 := baseTime
// Even though monotonic clocks differ, the instants are the same
err := EqualTo(time2)(time1)
if err != nil {
t.Errorf("expected no error despite monotonic clock difference, got: %v", err)
}
})
}
func TestNotEqualTo_TimeComparison(t *testing.T) {
t.Run("different instants", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 13, 0, 0, 0, time.UTC)
err := NotEqualTo(time2)(time1)
if err != nil {
t.Errorf("expected no error for different instants, got: %v", err)
}
})
t.Run("same instant same location", func(t *testing.T) {
time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
err := NotEqualTo(time2)(time1)
if err == nil {
t.Error("expected validation error for same instant")
}
})
t.Run("same instant different location", func(t *testing.T) {
utcTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC)
est, _ := time.LoadLocation("America/New_York")
estTime := time.Date(2025, 11, 5, 7, 0, 0, 0, est)
err := NotEqualTo(utcTime)(estTime)
if err == nil {
t.Error("expected validation error for same instant in different locations")
}
})
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
// Custom creates a custom validator with a specified error code, message, and validation function.
// The validation function should return true if the value is valid, false otherwise.
func Custom(code ErrorCode, message string, fn func(value any) bool) ValidatorFunc {
return func(value any) *ValidationError {
if !fn(value) {
return newValidationError(code, message)
}
return nil
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
)
func TestCustom(t *testing.T) {
validator := Custom(ErrorCodeCustom, "value must be positive", func(value any) bool {
if num, ok := value.(int); ok {
return num > 0
}
return false
})
tests := []struct {
name string
value any
wantError bool
}{
{"valid positive", 5, false},
{"invalid zero", 0, true},
{"invalid negative", -5, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Custom() error = %v, wantError %v", err, tt.wantError)
}
})
}
}

View File

@@ -0,0 +1,255 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"net/url"
"regexp"
"go.probo.inc/probo/pkg/gid"
)
var (
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
gidRegex = regexp.MustCompile(`^gid://[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+$`)
uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
)
// Email validates that a string is a valid email address.
func Email() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidEmail, "value must be a string")
}
if str == "" {
return nil
}
if !emailRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidEmail, "invalid email address")
}
return nil
}
}
// URL validates that a string is a valid URL with http or https scheme.
func URL() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidURL, "value must be a string")
}
if str == "" {
return nil
}
parsedURL, err := url.Parse(str)
if err != nil {
return newValidationError(ErrorCodeInvalidURL, "invalid URL format")
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return newValidationError(ErrorCodeInvalidURL, "URL must use http or https scheme")
}
if parsedURL.Host == "" {
return newValidationError(ErrorCodeInvalidURL, "URL must have a host")
}
return nil
}
}
// HTTPUrl validates that a string is a valid HTTP URL (not HTTPS).
func HTTPUrl() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidURL, "value must be a string")
}
if str == "" {
return nil
}
parsedURL, err := url.Parse(str)
if err != nil {
return newValidationError(ErrorCodeInvalidURL, "invalid URL format")
}
if parsedURL.Scheme != "http" {
return newValidationError(ErrorCodeInvalidURL, "URL must use http scheme")
}
if parsedURL.Host == "" {
return newValidationError(ErrorCodeInvalidURL, "URL must have a host")
}
return nil
}
}
// HTTPSUrl validates that a string is a valid HTTPS URL (not HTTP).
func HTTPSUrl() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidURL, "value must be a string")
}
if str == "" {
return nil
}
parsedURL, err := url.Parse(str)
if err != nil {
return newValidationError(ErrorCodeInvalidURL, "invalid URL format")
}
if parsedURL.Scheme != "https" {
return newValidationError(ErrorCodeInvalidURL, "URL must use https scheme")
}
if parsedURL.Host == "" {
return newValidationError(ErrorCodeInvalidURL, "URL must have a host")
}
return nil
}
}
// UUID validates that a string is a valid UUID.
func UUID() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if !uuidRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "invalid UUID format")
}
return nil
}
}
// GID validates that a string is a valid GID using gid.ParseGID.
// Optionally validates the entity type if provided.
//
// Example usage:
// - GID() validates any GID format
// - GID(100) validates GID with entity type 100
// - GID(100, 200) validates GID with entity type 100 or 200
func GID(entityTypes ...uint16) ValidatorFunc {
return func(value any) *ValidationError {
if value == nil {
return nil
}
var gidValue gid.GID
switch v := value.(type) {
case gid.GID:
gidValue = v
case *gid.GID:
if v == nil {
return nil
}
gidValue = *v
default:
return newValidationError(ErrorCodeInvalidGID, "value must be a GID")
}
if len(entityTypes) > 0 {
parsedEntityType := gidValue.EntityType()
valid := false
for _, expected := range entityTypes {
if parsedEntityType == expected {
valid = true
break
}
}
if !valid {
return newValidationError(ErrorCodeInvalidGID, "GID has invalid entity type")
}
}
return nil
}
}
// Domain validates that a string is a valid domain name.
func Domain() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if len(str) > 253 {
return newValidationError(ErrorCodeInvalidFormat, "domain name too long (max 253 characters)")
}
if !domainRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "invalid domain name format")
}
return nil
}
}

View File

@@ -0,0 +1,432 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"strings"
"testing"
"go.probo.inc/probo/pkg/gid"
)
func TestEmail(t *testing.T) {
tests := []struct {
name string
value any
wantError bool
}{
{"valid email", "test@example.com", false},
{"valid email with plus", "test+tag@example.com", false},
{"invalid email no @", "testexample.com", true},
{"invalid email no domain", "test@", true},
{"invalid email no TLD", "test@example", true},
{"empty string", "", false}, // Empty is allowed, use Required() to enforce
{"nil pointer", (*string)(nil), false}, // Skip validation
{"non-string", 123, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Email()(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Email() error = %v, wantError %v", err, tt.wantError)
}
if err != nil && err.Code != ErrorCodeInvalidEmail {
t.Errorf("Expected error code %s, got %s", ErrorCodeInvalidEmail, err.Code)
}
})
}
}
func TestURL(t *testing.T) {
tests := []struct {
name string
value any
wantError bool
}{
{"valid http URL", "http://example.com", false},
{"valid https URL", "https://example.com", false},
{"valid URL with path", "https://example.com/path", false},
{"invalid scheme", "ftp://example.com", true},
{"no scheme", "example.com", true},
{"no host", "https://", true},
{"empty string", "", false}, // Empty is allowed
{"nil pointer", (*string)(nil), false},
{"non-string", 123, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := URL()(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("URL() error = %v, wantError %v", err, tt.wantError)
}
if err != nil && err.Code != ErrorCodeInvalidURL {
t.Errorf("Expected error code %s, got %s", ErrorCodeInvalidURL, err.Code)
}
})
}
}
func TestHTTPUrl(t *testing.T) {
t.Run("valid http URL", func(t *testing.T) {
str := "http://example.com"
err := HTTPUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid http URL with path", func(t *testing.T) {
str := "http://example.com/path/to/resource"
err := HTTPUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid http URL with query", func(t *testing.T) {
str := "http://example.com?foo=bar"
err := HTTPUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - https scheme", func(t *testing.T) {
str := "https://example.com"
err := HTTPUrl()(&str)
if err == nil {
t.Error("expected validation error for https")
}
if err.Message != "URL must use http scheme" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - no scheme", func(t *testing.T) {
str := "example.com"
err := HTTPUrl()(&str)
if err == nil {
t.Error("expected validation error for missing scheme")
}
})
t.Run("invalid - no host", func(t *testing.T) {
str := "http://"
err := HTTPUrl()(&str)
if err == nil {
t.Error("expected validation error for missing host")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := HTTPUrl()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := HTTPUrl()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestHTTPSUrl(t *testing.T) {
t.Run("valid https URL", func(t *testing.T) {
str := "https://example.com"
err := HTTPSUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid https URL with path", func(t *testing.T) {
str := "https://example.com/path/to/resource"
err := HTTPSUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid https URL with query", func(t *testing.T) {
str := "https://api.example.com/v1/users?page=1"
err := HTTPSUrl()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - http scheme", func(t *testing.T) {
str := "http://example.com"
err := HTTPSUrl()(&str)
if err == nil {
t.Error("expected validation error for http")
}
if err.Message != "URL must use https scheme" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - ftp scheme", func(t *testing.T) {
str := "ftp://example.com"
err := HTTPSUrl()(&str)
if err == nil {
t.Error("expected validation error for ftp")
}
})
t.Run("invalid - no scheme", func(t *testing.T) {
str := "example.com"
err := HTTPSUrl()(&str)
if err == nil {
t.Error("expected validation error for missing scheme")
}
})
t.Run("invalid - no host", func(t *testing.T) {
str := "https://"
err := HTTPSUrl()(&str)
if err == nil {
t.Error("expected validation error for missing host")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := HTTPSUrl()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := HTTPSUrl()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestDomain(t *testing.T) {
t.Run("valid domain", func(t *testing.T) {
str := "example.com"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid subdomain", func(t *testing.T) {
str := "api.example.com"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid nested subdomain", func(t *testing.T) {
str := "api.v1.example.com"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid domain with hyphens", func(t *testing.T) {
str := "my-api.example-site.com"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("single word domain", func(t *testing.T) {
str := "localhost"
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - starts with hyphen", func(t *testing.T) {
str := "-example.com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for domain starting with hyphen")
}
})
t.Run("invalid - ends with hyphen", func(t *testing.T) {
str := "example-.com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for domain ending with hyphen")
}
})
t.Run("invalid - contains underscore", func(t *testing.T) {
str := "example_site.com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for underscore")
}
})
t.Run("invalid - contains spaces", func(t *testing.T) {
str := "example site.com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for spaces")
}
})
t.Run("invalid - empty label", func(t *testing.T) {
str := "example..com"
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for empty label")
}
})
t.Run("invalid - too long", func(t *testing.T) {
str := strings.Repeat("a", 254)
err := Domain()(&str)
if err == nil {
t.Error("expected validation error for domain too long")
}
if err.Message != "domain name too long (max 253 characters)" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := Domain()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := Domain()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestGID(t *testing.T) {
// Create a valid GID for testing
tenantID := gid.TenantID([8]byte{1, 2, 3, 4, 5, 6, 7, 8})
validGID := gid.New(tenantID, 100)
t.Run("valid GID type - no entity type validation", func(t *testing.T) {
err := GID()(validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid GID type - with matching entity type", func(t *testing.T) {
err := GID(100)(validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid GID type - with multiple entity types", func(t *testing.T) {
err := GID(100, 200, 300)(validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - wrong entity type", func(t *testing.T) {
err := GID(200)(validGID)
if err == nil {
t.Error("expected validation error for wrong entity type")
}
if err.Code != ErrorCodeInvalidGID {
t.Errorf("expected error code %s, got %s", ErrorCodeInvalidGID, err.Code)
}
if err.Message != "GID has invalid entity type" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - wrong entity type with multiple options", func(t *testing.T) {
err := GID(200, 300)(validGID)
if err == nil {
t.Error("expected validation error for wrong entity type")
}
})
t.Run("valid - entity type matches one of multiple options", func(t *testing.T) {
err := GID(99, 100, 101)(validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("nil GID pointer", func(t *testing.T) {
var gidPtr *gid.GID
err := GID()(gidPtr)
if err != nil {
t.Errorf("expected no error for nil GID pointer, got: %v", err)
}
})
t.Run("valid GID pointer", func(t *testing.T) {
err := GID()(&validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid GID pointer with entity type validation", func(t *testing.T) {
err := GID(100)(&validGID)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("non-GID type", func(t *testing.T) {
err := GID()(123)
if err == nil {
t.Error("expected validation error for non-GID type")
}
if err.Message != "value must be a GID" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("string type not supported", func(t *testing.T) {
err := GID()("some-string")
if err == nil {
t.Error("expected validation error for string type")
}
if err.Message != "value must be a GID" {
t.Errorf("unexpected error message: %s", err.Message)
}
})
}

View File

@@ -0,0 +1,203 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import "fmt"
// Min validates that a number is at least the specified minimum value.
func Min(min int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num int
switch v := actualValue.(type) {
case int:
num = v
case int32:
num = int(v)
case int64:
num = int(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num < min {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be at least %d", min),
)
}
return nil
}
}
// Max validates that a number does not exceed the specified maximum value.
func Max(max int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num int
switch v := actualValue.(type) {
case int:
num = v
case int32:
num = int(v)
case int64:
num = int(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be at most %d", max),
)
}
return nil
}
}
// Range validates that a number is within the specified range (inclusive).
func Range(min, max int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num int
switch v := actualValue.(type) {
case int:
num = v
case int32:
num = int(v)
case int64:
num = int(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num < min || num > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be between %d and %d", min, max),
)
}
return nil
}
}
// MinFloat validates that a floating-point number is at least the specified minimum value.
func MinFloat(min float64) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num float64
switch v := actualValue.(type) {
case float32:
num = float64(v)
case float64:
num = v
case int:
num = float64(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num < min {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be at least %g", min),
)
}
return nil
}
}
// MaxFloat validates that a floating-point number does not exceed the specified maximum value.
func MaxFloat(max float64) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num float64
switch v := actualValue.(type) {
case float32:
num = float64(v)
case float64:
num = v
case int:
num = float64(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be at most %g", max),
)
}
return nil
}
}
// RangeFloat validates that a floating-point number is within the specified range (inclusive).
func RangeFloat(min, max float64) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var num float64
switch v := actualValue.(type) {
case float32:
num = float64(v)
case float64:
num = v
case int:
num = float64(v)
default:
return newValidationError(ErrorCodeInvalidFormat, "value must be a number")
}
if num < min || num > max {
return newValidationError(
ErrorCodeOutOfRange,
fmt.Sprintf("must be between %g and %g", min, max),
)
}
return nil
}
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"go.gearno.de/x/ref"
)
func TestMin(t *testing.T) {
tests := []struct {
name string
value any
min int
wantError bool
}{
{"valid int", 10, 5, false},
{"exact min", 5, 5, false},
{"below min", 3, 5, true},
{"valid int pointer", ref.Ref(10), 5, false},
{"nil pointer", (*int)(nil), 5, false}, // Skip validation
{"non-numeric", "test", 5, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Min(tt.min)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Min() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestMax(t *testing.T) {
tests := []struct {
name string
value any
max int
wantError bool
}{
{"valid int", 5, 10, false},
{"exact max", 10, 10, false},
{"above max", 15, 10, true},
{"valid int pointer", ref.Ref(5), 10, false},
{"nil pointer", (*int)(nil), 10, false}, // Skip validation
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Max(tt.max)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Max() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestRange(t *testing.T) {
tests := []struct {
name string
value any
min int
max int
wantError bool
}{
{"in range", 5, 1, 10, false},
{"at min", 1, 1, 10, false},
{"at max", 10, 1, 10, false},
{"below range", 0, 1, 10, true},
{"above range", 11, 1, 10, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Range(tt.min, tt.max)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Range() error = %v, wantError %v", err, tt.wantError)
}
})
}
}

View File

@@ -0,0 +1,173 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"regexp"
"strings"
)
var (
htmlTagRegex = regexp.MustCompile(`<[^>]*>`)
)
// NoHTML validates that a string does not contain HTML tags or angle brackets.
// It rejects:
// - HTML tags (e.g., <script>, <b>, <div>, etc.)
// - Angle brackets (< and >) even when not part of complete tags
//
// This helps prevent XSS attacks and ensures user input doesn't contain HTML markup.
// Combine with PrintableText() for comprehensive text field validation.
func NoHTML() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
// Check for HTML tags first (more specific error message)
if htmlTagRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "must not contain HTML tags")
}
// Check for angle brackets (even without complete tags)
if strings.ContainsAny(str, "<>") {
return newValidationError(ErrorCodeInvalidFormat, "must not contain angle brackets")
}
return nil
}
}
// PrintableText validates that a string contains only printable UTF-8 characters.
// It rejects:
// - Control characters (including null bytes, tabs, line breaks except space)
// - Unicode direction override characters (RLO, LRO, PDF, etc.)
// - Zero-width characters (ZWSP, ZWNJ, ZWJ, etc.)
// - Other invisible or formatting characters
// - Private use area characters
// - Replacement characters
//
// This validator does NOT check for HTML tags - use NoHTML() for that.
// This is ideal for validating titles, full names, display names, and similar text fields
// where only printable characters should be allowed.
func PrintableText() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
// Check each rune for invisible or problematic characters
for i, r := range str {
// Allow normal space
if r == ' ' {
continue
}
// Reject control characters (0x00-0x1F and 0x7F-0x9F)
if r < 0x20 || (r >= 0x7F && r < 0xA0) {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains invalid control character at position %d", i))
}
// Reject Unicode direction override and formatting characters
// U+200E LEFT-TO-RIGHT MARK (LRM)
// U+200F RIGHT-TO-LEFT MARK (RLM)
// U+202A LEFT-TO-RIGHT EMBEDDING (LRE)
// U+202B RIGHT-TO-LEFT EMBEDDING (RLE)
// U+202C POP DIRECTIONAL FORMATTING (PDF)
// U+202D LEFT-TO-RIGHT OVERRIDE (LRO)
// U+202E RIGHT-TO-LEFT OVERRIDE (RLO)
// U+2066 LEFT-TO-RIGHT ISOLATE (LRI)
// U+2067 RIGHT-TO-LEFT ISOLATE (RLI)
// U+2068 FIRST STRONG ISOLATE (FSI)
// U+2069 POP DIRECTIONAL ISOLATE (PDI)
if r >= 0x200E && r <= 0x200F || r >= 0x202A && r <= 0x202E || r >= 0x2066 && r <= 0x2069 {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains bidirectional override character at position %d", i))
}
// Reject zero-width characters
// U+200B ZERO WIDTH SPACE (ZWSP)
// U+200C ZERO WIDTH NON-JOINER (ZWNJ)
// U+200D ZERO WIDTH JOINER (ZWJ)
// U+FEFF ZERO WIDTH NO-BREAK SPACE (BOM)
if r == 0x200B || r == 0x200C || r == 0x200D || r == 0xFEFF {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains zero-width character at position %d", i))
}
// Reject other format characters (Cf category)
// U+00AD SOFT HYPHEN
// U+2060 WORD JOINER
// U+180E MONGOLIAN VOWEL SEPARATOR (deprecated but still problematic)
if r == 0x00AD || r == 0x2060 || r == 0x180E {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains invisible formatting character at position %d", i))
}
// Reject private use area characters (often used for exploits)
// U+E000-U+F8FF Private Use Area
// U+F0000-U+FFFFD Supplementary Private Use Area-A
// U+100000-U+10FFFD Supplementary Private Use Area-B
if (r >= 0xE000 && r <= 0xF8FF) || (r >= 0xF0000 && r <= 0xFFFFD) || (r >= 0x100000 && r <= 0x10FFFD) {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains private use character at position %d", i))
}
// Reject replacement character (often indicates encoding issues)
if r == 0xFFFD {
return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains replacement character at position %d", i))
}
}
return nil
}
}
// SafeText validates that a string is non-empty, bounded, and contains only safe content.
// It combines NotEmpty, MaxLen, NoHTML, and PrintableText validators.
func SafeText(maxLen int) ValidatorFunc {
validators := []ValidatorFunc{
NotEmpty(),
MaxLen(maxLen),
NoHTML(),
PrintableText(),
}
return func(value any) *ValidationError {
for _, validator := range validators {
if err := validator(value); err != nil {
return err
}
}
return nil
}
}

View File

@@ -0,0 +1,755 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"strings"
"testing"
)
func TestNoHTML(t *testing.T) {
t.Run("valid text without HTML", func(t *testing.T) {
str := "This is a normal text"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid text with special characters", func(t *testing.T) {
str := "Price: $10.99 - 20% off!"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid UTF-8 text", func(t *testing.T) {
str := "José García 张伟"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid text with emojis", func(t *testing.T) {
str := "Hello World 🌍"
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - script tag XSS", func(t *testing.T) {
str := "<script>alert('xss')</script>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for script tag")
}
if !strings.Contains(err.Message, "HTML tags") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - simple bold tag", func(t *testing.T) {
str := "Hello <b>World</b>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for bold tag")
}
if !strings.Contains(err.Message, "HTML tags") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - div tag", func(t *testing.T) {
str := "<div>Content</div>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for div tag")
}
})
t.Run("invalid - self-closing tag", func(t *testing.T) {
str := "Line break<br/>here"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for self-closing tag")
}
})
t.Run("invalid - img tag", func(t *testing.T) {
str := `<img src="x" onerror="alert(1)">`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for img tag")
}
})
t.Run("invalid - anchor tag", func(t *testing.T) {
str := `<a href="javascript:alert(1)">Click</a>`
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for anchor tag")
}
})
t.Run("invalid - less than symbol", func(t *testing.T) {
str := "5 < 10"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for angle bracket")
}
if !strings.Contains(err.Message, "angle brackets") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - greater than symbol", func(t *testing.T) {
str := "10 > 5"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for angle bracket")
}
})
t.Run("invalid - both angle brackets", func(t *testing.T) {
str := "5 < x > 10"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for angle brackets")
}
})
t.Run("invalid - malformed tag", func(t *testing.T) {
str := "text <incomplete"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for incomplete tag")
}
})
t.Run("invalid - encoded attempt", func(t *testing.T) {
str := "<ScRiPt>alert(1)</ScRiPt>"
err := NoHTML()(&str)
if err == nil {
t.Error("expected validation error for mixed case script tag")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := NoHTML()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := NoHTML()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
t.Run("not a string", func(t *testing.T) {
num := 123
err := NoHTML()(&num)
if err == nil {
t.Error("expected validation error for non-string")
}
if !strings.Contains(err.Message, "must be a string") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("combined with other validators", func(t *testing.T) {
v := New()
title := "Product Title 2024"
v.Check(&title, "title", Required(), NoHTML(), MinLen(3), MaxLen(100))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("combined with PrintableText", func(t *testing.T) {
v := New()
title := "José García-O'Brien"
v.Check(&title, "title", Required(), NoHTML(), PrintableText(), MinLen(3), MaxLen(100))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("combined validators catch XSS", func(t *testing.T) {
v := New()
malicious := "<script>alert('xss')</script>"
v.Check(&malicious, "content", Required(), NoHTML(), PrintableText())
if !v.HasErrors() {
t.Error("expected validation errors")
}
// Should have error from NoHTML
errors := v.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, "HTML tags") || strings.Contains(err.Message, "angle brackets") {
found = true
break
}
}
if !found {
t.Error("expected error about HTML tags or angle brackets")
}
})
t.Run("combined validators catch invisible chars and HTML", func(t *testing.T) {
v := New()
malicious := "<b>test\x00text</b>"
v.Check(&malicious, "content", NoHTML(), PrintableText())
if !v.HasErrors() {
t.Error("expected validation errors")
}
// Should have at least one error (NoHTML will catch it first)
if len(v.Errors()) < 1 {
t.Error("expected at least one validation error")
}
})
}
func TestPrintableText(t *testing.T) {
t.Run("valid UTF-8 text with accents", func(t *testing.T) {
str := "José García"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for valid UTF-8 name, got: %v", err)
}
})
t.Run("valid text with emojis", func(t *testing.T) {
str := "Hello World 🌍"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for emojis, got: %v", err)
}
})
t.Run("valid Chinese characters", func(t *testing.T) {
str := "张伟"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for Chinese characters, got: %v", err)
}
})
t.Run("valid Arabic text", func(t *testing.T) {
str := "محمد"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for Arabic text, got: %v", err)
}
})
t.Run("valid Cyrillic text", func(t *testing.T) {
str := "Александр"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for Cyrillic text, got: %v", err)
}
})
t.Run("valid text with apostrophe and hyphen", func(t *testing.T) {
str := "O'Brien-Smith"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for apostrophe and hyphen, got: %v", err)
}
})
t.Run("valid text with numbers", func(t *testing.T) {
str := "Product 2024"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for text with numbers, got: %v", err)
}
})
t.Run("valid text with punctuation", func(t *testing.T) {
str := "Hello, World! How are you?"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for punctuation, got: %v", err)
}
})
t.Run("valid text with angle brackets", func(t *testing.T) {
str := "5 < 10 > 3"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for angle brackets (HTML checking is separate), got: %v", err)
}
})
t.Run("invalid - RLO character", func(t *testing.T) {
str := "test\u202Eexe.txt"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for RLO character")
}
if !strings.Contains(err.Message, "bidirectional override") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - LRO character", func(t *testing.T) {
str := "test\u202Dtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for LRO character")
}
})
t.Run("invalid - zero-width space", func(t *testing.T) {
str := "test\u200Btext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for zero-width space")
}
if !strings.Contains(err.Message, "zero-width") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - zero-width non-joiner", func(t *testing.T) {
str := "test\u200Ctext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for zero-width non-joiner")
}
})
t.Run("invalid - zero-width joiner", func(t *testing.T) {
str := "test\u200Dtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for zero-width joiner")
}
})
t.Run("invalid - BOM character", func(t *testing.T) {
str := "\uFEFFtest"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for BOM character")
}
})
t.Run("invalid - null byte", func(t *testing.T) {
str := "test\x00text"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for null byte")
}
if !strings.Contains(err.Message, "control character") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - tab character", func(t *testing.T) {
str := "test\ttext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for tab character")
}
})
t.Run("invalid - newline character", func(t *testing.T) {
str := "test\ntext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for newline character")
}
})
t.Run("invalid - carriage return", func(t *testing.T) {
str := "test\rtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for carriage return")
}
})
t.Run("invalid - soft hyphen", func(t *testing.T) {
str := "test\u00ADtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for soft hyphen")
}
if !strings.Contains(err.Message, "invisible formatting") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - word joiner", func(t *testing.T) {
str := "test\u2060text"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for word joiner")
}
})
t.Run("invalid - private use area character", func(t *testing.T) {
str := "test\uE000text"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for private use area")
}
if !strings.Contains(err.Message, "private use") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - replacement character", func(t *testing.T) {
str := "test\uFFFDtext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for replacement character")
}
if !strings.Contains(err.Message, "replacement character") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - DEL control character", func(t *testing.T) {
str := "test\x7Ftext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for DEL control character")
}
})
t.Run("invalid - C1 control character", func(t *testing.T) {
str := "test\u0080text"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for C1 control character")
}
})
t.Run("invalid - LTR mark", func(t *testing.T) {
str := "test\u200Etext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for LTR mark")
}
})
t.Run("invalid - RTL mark", func(t *testing.T) {
str := "test\u200Ftext"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error for RTL mark")
}
})
t.Run("valid with pointer", func(t *testing.T) {
str := "Valid Name"
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := PrintableText()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := PrintableText()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
t.Run("not a string", func(t *testing.T) {
num := 123
err := PrintableText()(&num)
if err == nil {
t.Error("expected validation error for non-string")
}
if !strings.Contains(err.Message, "must be a string") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("combined with other validators", func(t *testing.T) {
v := New()
title := "Product Title 2024"
v.Check(&title, "title", Required(), PrintableText(), MinLen(3), MaxLen(100))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("position reported correctly", func(t *testing.T) {
str := "abc\x00def"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error")
}
if !strings.Contains(err.Message, "position 3") {
t.Errorf("expected position 3 in error message, got: %s", err.Message)
}
})
t.Run("UTF-8 position counting", func(t *testing.T) {
// Test that position is counted correctly with UTF-8 characters
// The range loop in Go iterates by runes, so position will be rune index
str := "abc\x00"
err := PrintableText()(&str)
if err == nil {
t.Error("expected validation error")
}
// The null byte is at rune position 3 (after 'a', 'b', 'c')
if !strings.Contains(err.Message, "position 3") {
t.Errorf("expected position 3 in error message, got: %s", err.Message)
}
})
}
func TestSafeText(t *testing.T) {
t.Run("valid text", func(t *testing.T) {
str := "Product Name 2024"
err := SafeText(100)(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid UTF-8 text", func(t *testing.T) {
str := "José García"
err := SafeText(50)(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid text with emoji", func(t *testing.T) {
str := "Hello World 🌍"
err := SafeText(50)(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid text with apostrophe and hyphen", func(t *testing.T) {
str := "O'Brien-Smith"
err := SafeText(50)(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("invalid - empty string", func(t *testing.T) {
str := ""
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for empty string")
}
if !strings.Contains(err.Message, "empty") && !strings.Contains(err.Message, "required") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - exceeds max length", func(t *testing.T) {
str := "This is a very long string that exceeds the maximum length"
err := SafeText(10)(&str)
if err == nil {
t.Error("expected validation error for exceeding max length")
}
if !strings.Contains(err.Message, "at most") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains HTML tags", func(t *testing.T) {
str := "Hello <b>World</b>"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for HTML tags")
}
if !strings.Contains(err.Message, "HTML tags") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains script tag", func(t *testing.T) {
str := "<script>alert('xss')</script>"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for script tag")
}
})
t.Run("invalid - contains angle brackets", func(t *testing.T) {
str := "5 < 10"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for angle brackets")
}
if !strings.Contains(err.Message, "angle brackets") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains null byte", func(t *testing.T) {
str := "test\x00text"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for null byte")
}
if !strings.Contains(err.Message, "control character") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains tab character", func(t *testing.T) {
str := "test\ttext"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for tab character")
}
})
t.Run("invalid - contains newline", func(t *testing.T) {
str := "test\ntext"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for newline")
}
})
t.Run("invalid - contains zero-width space", func(t *testing.T) {
str := "test\u200Btext"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for zero-width space")
}
if !strings.Contains(err.Message, "zero-width") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains RLO character", func(t *testing.T) {
str := "test\u202Eexe.txt"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for RLO character")
}
if !strings.Contains(err.Message, "bidirectional override") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("invalid - contains private use area character", func(t *testing.T) {
str := "test\uE000text"
err := SafeText(100)(&str)
if err == nil {
t.Error("expected validation error for private use area")
}
if !strings.Contains(err.Message, "private use") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := SafeText(100)(str)
if err != nil {
t.Errorf("expected no error for nil pointer, got: %v", err)
}
})
t.Run("not a string", func(t *testing.T) {
num := 123
err := SafeText(100)(&num)
if err == nil {
t.Error("expected validation error for non-string")
}
if !strings.Contains(err.Message, "must be a string") {
t.Errorf("unexpected error message: %s", err.Message)
}
})
t.Run("combined with validator struct", func(t *testing.T) {
v := New()
title := "Product Title 2024"
v.Check(&title, "title", SafeText(100))
if v.HasErrors() {
t.Errorf("expected no errors, got: %v", v.Errors())
}
})
t.Run("combined with validator struct - invalid", func(t *testing.T) {
v := New()
malicious := "<script>alert('xss')</script>"
v.Check(&malicious, "content", SafeText(100))
if !v.HasErrors() {
t.Error("expected validation errors")
}
errors := v.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, "HTML tags") || strings.Contains(err.Message, "angle brackets") {
found = true
break
}
}
if !found {
t.Error("expected error about HTML tags or angle brackets")
}
})
t.Run("edge case - exactly at max length", func(t *testing.T) {
str := "12345"
err := SafeText(5)(&str)
if err != nil {
t.Errorf("expected no error for string at max length, got: %v", err)
}
})
t.Run("edge case - one character over max length", func(t *testing.T) {
str := "123456"
err := SafeText(5)(&str)
if err == nil {
t.Error("expected validation error for string over max length")
}
})
}

View File

@@ -0,0 +1,275 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"fmt"
"reflect"
"regexp"
"strings"
)
var (
alphaNumericRegex = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
slugRegex = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
)
// MinLen validates that a string has at least the specified minimum length.
func MinLen(minLength int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if len(str) < minLength {
return newValidationError(
ErrorCodeTooShort,
fmt.Sprintf("must be at least %d characters", minLength),
)
}
return nil
}
}
// MaxLen validates that a string does not exceed the specified maximum length.
func MaxLen(maxLength int) ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if len(str) > maxLength {
return newValidationError(
ErrorCodeTooLong,
fmt.Sprintf("must be at most %d characters", maxLength),
)
}
return nil
}
}
// Pattern validates that a string matches the specified regular expression pattern.
func Pattern(pattern string, message string) ValidatorFunc {
regex := regexp.MustCompile(pattern)
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if !regex.MatchString(str) {
if message == "" {
message = fmt.Sprintf("must match pattern: %s", pattern)
}
return newValidationError(ErrorCodeInvalidFormat, message)
}
return nil
}
}
// AlphaNumeric validates that a string contains only letters and numbers.
func AlphaNumeric() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if !alphaNumericRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "must contain only letters and numbers")
}
return nil
}
}
// NoSpaces validates that a string does not contain any spaces.
func NoSpaces() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if strings.Contains(str, " ") {
return newValidationError(ErrorCodeInvalidFormat, "must not contain spaces")
}
return nil
}
}
// Slug validates that a string is a valid URL slug (lowercase letters, numbers, and hyphens).
func Slug() ValidatorFunc {
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
str, ok := actualValue.(string)
if !ok {
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
}
if str == "" {
return nil
}
if !slugRegex.MatchString(str) {
return newValidationError(ErrorCodeInvalidFormat, "must be a valid slug (lowercase letters, numbers, and hyphens)")
}
return nil
}
}
// OneOfSlice validates that a value is one of the allowed values in the slice.
// Accepts a slice of any type. Compares by value first, then by string representation.
func OneOfSlice[T any](allowed []T) ValidatorFunc {
// Build allowed map with string keys for flexible comparison
allowedMap := make(map[string]bool)
allowedStrings := make([]string, 0, len(allowed))
for _, v := range allowed {
str := fmt.Sprint(v)
allowedMap[str] = true
allowedStrings = append(allowedStrings, str)
}
return func(value any) *ValidationError {
// Handle nil values first
if value == nil {
return nil
}
// Dereference all pointer levels
actualValue := value
val := reflect.ValueOf(value)
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil
}
val = val.Elem()
actualValue = val.Interface()
}
// First try exact match with DeepEqual
for _, allowedVal := range allowed {
if reflect.DeepEqual(actualValue, allowedVal) {
return nil
}
}
// Then try string comparison (for custom string types)
valueStr := fmt.Sprint(actualValue)
if allowedMap[valueStr] {
return nil
}
return newValidationError(
ErrorCodeInvalidEnum,
fmt.Sprintf("must be one of: %s", strings.Join(allowedStrings, ", ")),
)
}
}
// OneOf validates that a value is one of the allowed values.
// Accepts strings or types that implement fmt.Stringer as variadic arguments.
func OneOf(allowed ...any) ValidatorFunc {
allowedMap := make(map[string]bool)
allowedStrings := make([]string, 0, len(allowed))
for _, v := range allowed {
var str string
switch val := v.(type) {
case string:
str = val
case fmt.Stringer:
str = val.String()
default:
str = fmt.Sprint(val)
}
allowedMap[str] = true
allowedStrings = append(allowedStrings, str)
}
return func(value any) *ValidationError {
actualValue, isNil := dereferenceValue(value)
if isNil {
return nil
}
var str string
switch v := actualValue.(type) {
case string:
str = v
default:
if stringer, ok := actualValue.(fmt.Stringer); ok {
str = stringer.String()
} else {
return newValidationError(ErrorCodeInvalidEnum, "value must be a string or implement fmt.Stringer")
}
}
if !allowedMap[str] {
return newValidationError(
ErrorCodeInvalidEnum,
fmt.Sprintf("must be one of: %s", strings.Join(allowedStrings, ", ")),
)
}
return nil
}
}

View File

@@ -0,0 +1,303 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package validator
import (
"testing"
"go.gearno.de/x/ref"
)
func TestMinLen(t *testing.T) {
tests := []struct {
name string
value any
minLen int
wantError bool
}{
{"valid string", "hello", 3, false},
{"exact length", "hello", 5, false},
{"too short", "hi", 5, true},
{"nil pointer", (*string)(nil), 5, false}, // Skip validation
{"valid pointer", ref.Ref("hello"), 3, false},
{"non-string", 123, 5, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := MinLen(tt.minLen)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("MinLen() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestMaxLen(t *testing.T) {
tests := []struct {
name string
value any
maxLen int
wantError bool
}{
{"valid string", "hello", 10, false},
{"exact length", "hello", 5, false},
{"too long", "hello world", 5, true},
{"nil pointer", (*string)(nil), 5, false}, // Skip validation
{"valid pointer", ref.Ref("hi"), 5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := MaxLen(tt.maxLen)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("MaxLen() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestPattern(t *testing.T) {
tests := []struct {
name string
value any
pattern string
message string
wantError bool
}{
{"valid pattern", "abc123", `^[a-z0-9]+$`, "", false},
{"invalid pattern", "ABC123", `^[a-z0-9]+$`, "", true},
{"custom message", "invalid", `^valid$`, "must be 'valid'", true},
{"nil pointer", (*string)(nil), `^test$`, "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Pattern(tt.pattern, tt.message)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("Pattern() error = %v, wantError %v", err, tt.wantError)
}
if err != nil && tt.message != "" && err.Message != tt.message {
t.Errorf("Expected message '%s', got '%s'", tt.message, err.Message)
}
})
}
}
func TestAlphaNumeric(t *testing.T) {
t.Run("valid alphanumeric", func(t *testing.T) {
str := "abc123"
err := AlphaNumeric()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("only letters", func(t *testing.T) {
str := "abcDEF"
err := AlphaNumeric()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("only numbers", func(t *testing.T) {
str := "123456"
err := AlphaNumeric()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("contains spaces", func(t *testing.T) {
str := "abc 123"
err := AlphaNumeric()(&str)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("contains special characters", func(t *testing.T) {
str := "abc-123"
err := AlphaNumeric()(&str)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := AlphaNumeric()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := AlphaNumeric()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestNoSpaces(t *testing.T) {
t.Run("no spaces", func(t *testing.T) {
str := "hello-world"
err := NoSpaces()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("contains spaces", func(t *testing.T) {
str := "hello world"
err := NoSpaces()(&str)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("multiple spaces", func(t *testing.T) {
str := "hello world test"
err := NoSpaces()(&str)
if err == nil {
t.Error("expected validation error")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := NoSpaces()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := NoSpaces()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestSlug(t *testing.T) {
t.Run("valid slug", func(t *testing.T) {
str := "hello-world"
err := Slug()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("valid slug with numbers", func(t *testing.T) {
str := "hello-world-123"
err := Slug()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("single word", func(t *testing.T) {
str := "hello"
err := Slug()(&str)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
})
t.Run("contains uppercase", func(t *testing.T) {
str := "Hello-World"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for uppercase")
}
})
t.Run("contains spaces", func(t *testing.T) {
str := "hello world"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for spaces")
}
})
t.Run("contains underscores", func(t *testing.T) {
str := "hello_world"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for underscores")
}
})
t.Run("starts with hyphen", func(t *testing.T) {
str := "-hello"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for leading hyphen")
}
})
t.Run("ends with hyphen", func(t *testing.T) {
str := "hello-"
err := Slug()(&str)
if err == nil {
t.Error("expected validation error for trailing hyphen")
}
})
t.Run("empty string", func(t *testing.T) {
str := ""
err := Slug()(&str)
if err != nil {
t.Errorf("expected no error for empty string, got: %v", err)
}
})
t.Run("nil pointer", func(t *testing.T) {
var str *string
err := Slug()(str)
if err != nil {
t.Errorf("expected no error for nil, got: %v", err)
}
})
}
func TestOneOf(t *testing.T) {
tests := []struct {
name string
value any
allowed []string
wantError bool
}{
{"valid value", "apple", []string{"apple", "banana", "orange"}, false},
{"invalid value", "grape", []string{"apple", "banana", "orange"}, true},
{"nil pointer", (*string)(nil), []string{"apple"}, false},
{"empty string", "", []string{"apple", ""}, false},
{"non-string", 123, []string{"apple"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := OneOfSlice(tt.allowed)(tt.value)
if (err != nil) != tt.wantError {
t.Errorf("OneOfSlice() error = %v, wantError %v", err, tt.wantError)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More