committed by
Sacha Al Himdani
parent
288c59a5f2
commit
f9216d30b2
@@ -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 (
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ const (
|
||||
ConnectorProtocolOAuth2 ConnectorProtocol = "OAUTH2"
|
||||
)
|
||||
|
||||
func ConnectorProtocols() []ConnectorProtocol {
|
||||
return []ConnectorProtocol{
|
||||
ConnectorProtocolOAuth2,
|
||||
}
|
||||
}
|
||||
|
||||
func (cp ConnectorProtocol) String() string {
|
||||
return string(cp)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ const (
|
||||
ConnectorProviderSlack ConnectorProvider = "SLACK"
|
||||
)
|
||||
|
||||
func ConnectorProviders() []ConnectorProvider {
|
||||
return []ConnectorProvider{
|
||||
ConnectorProviderSlack,
|
||||
}
|
||||
}
|
||||
|
||||
func (cp ConnectorProvider) String() string {
|
||||
return string(cp)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ const (
|
||||
ContinualImprovementPriorityHigh ContinualImprovementPriority = "HIGH"
|
||||
)
|
||||
|
||||
func ContinualImprovementPriorities() []ContinualImprovementPriority {
|
||||
return []ContinualImprovementPriority{
|
||||
ContinualImprovementPriorityLow,
|
||||
ContinualImprovementPriorityMedium,
|
||||
ContinualImprovementPriorityHigh,
|
||||
}
|
||||
}
|
||||
|
||||
func (cip ContinualImprovementPriority) String() string {
|
||||
return string(cip)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ const (
|
||||
ContinualImprovementStatusClosed ContinualImprovementStatus = "CLOSED"
|
||||
)
|
||||
|
||||
func ContinualImprovementStatuses() []ContinualImprovementStatus {
|
||||
return []ContinualImprovementStatus{
|
||||
ContinualImprovementStatusOpen,
|
||||
ContinualImprovementStatusInProgress,
|
||||
ContinualImprovementStatusClosed,
|
||||
}
|
||||
}
|
||||
|
||||
func (cis ContinualImprovementStatus) String() string {
|
||||
return string(cis)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,13 @@ const (
|
||||
ControlStatusExcluded ControlStatus = "EXCLUDED"
|
||||
)
|
||||
|
||||
func ControlStatuses() []ControlStatus {
|
||||
return []ControlStatus{
|
||||
ControlStatusIncluded,
|
||||
ControlStatusExcluded,
|
||||
}
|
||||
}
|
||||
|
||||
func (cs ControlStatus) String() string {
|
||||
return string(cs)
|
||||
}
|
||||
|
||||
@@ -22,3 +22,12 @@ const (
|
||||
DataClassificationConfidential DataClassification = "CONFIDENTIAL"
|
||||
DataClassificationSecret DataClassification = "SECRET"
|
||||
)
|
||||
|
||||
func DataClassifications() []DataClassification {
|
||||
return []DataClassification{
|
||||
DataClassificationPublic,
|
||||
DataClassificationInternal,
|
||||
DataClassificationConfidential,
|
||||
DataClassificationSecret,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ const (
|
||||
NonconformityStatusClosed NonconformityStatus = "CLOSED"
|
||||
)
|
||||
|
||||
func NonconformityStatuses() []NonconformityStatus {
|
||||
return []NonconformityStatus{
|
||||
NonconformityStatusOpen,
|
||||
NonconformityStatusInProgress,
|
||||
NonconformityStatusClosed,
|
||||
}
|
||||
}
|
||||
|
||||
func (ncs NonconformityStatus) String() string {
|
||||
return string(ncs)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ const (
|
||||
ObligationStatusCompliant ObligationStatus = "COMPLIANT"
|
||||
)
|
||||
|
||||
func ObligationStatuses() []ObligationStatus {
|
||||
return []ObligationStatus{
|
||||
ObligationStatusNonCompliant,
|
||||
ObligationStatusPartiallyCompliant,
|
||||
ObligationStatusCompliant,
|
||||
}
|
||||
}
|
||||
|
||||
func (os ObligationStatus) String() string {
|
||||
return string(os)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -26,6 +26,13 @@ const (
|
||||
ProcessingActivityDataProtectionImpactAssessmentNotNeeded ProcessingActivityDataProtectionImpactAssessment = "NOT_NEEDED"
|
||||
)
|
||||
|
||||
func ProcessingActivityDataProtectionImpactAssessments() []ProcessingActivityDataProtectionImpactAssessment {
|
||||
return []ProcessingActivityDataProtectionImpactAssessment{
|
||||
ProcessingActivityDataProtectionImpactAssessmentNeeded,
|
||||
ProcessingActivityDataProtectionImpactAssessmentNotNeeded,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ProcessingActivityDataProtectionImpactAssessment) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -26,6 +26,13 @@ const (
|
||||
ProcessingActivityTransferImpactAssessmentNotNeeded ProcessingActivityTransferImpactAssessment = "NOT_NEEDED"
|
||||
)
|
||||
|
||||
func ProcessingActivityTransferImpactAssessments() []ProcessingActivityTransferImpactAssessment {
|
||||
return []ProcessingActivityTransferImpactAssessment{
|
||||
ProcessingActivityTransferImpactAssessmentNeeded,
|
||||
ProcessingActivityTransferImpactAssessmentNotNeeded,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ProcessingActivityTransferImpactAssessment) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ const (
|
||||
TaskStateDone
|
||||
)
|
||||
|
||||
func TaskStates() []TaskState {
|
||||
return []TaskState{
|
||||
TaskStateTodo,
|
||||
TaskStateDone,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts TaskState) MarshalText() ([]byte, error) {
|
||||
return []byte(ts.String()), nil
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ const (
|
||||
TrustCenterVisibilityPublic TrustCenterVisibility = "PUBLIC"
|
||||
)
|
||||
|
||||
func TrustCenterVisibilities() []TrustCenterVisibility {
|
||||
return []TrustCenterVisibility{
|
||||
TrustCenterVisibilityNone,
|
||||
TrustCenterVisibilityPrivate,
|
||||
TrustCenterVisibilityPublic,
|
||||
}
|
||||
}
|
||||
|
||||
func (tcv TrustCenterVisibility) String() string {
|
||||
return string(tcv)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
201
pkg/validator/checkeach_slice_test.go
Normal file
201
pkg/validator/checkeach_slice_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
98
pkg/validator/double_pointer_test.go
Normal file
98
pkg/validator/double_pointer_test.go
Normal 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
107
pkg/validator/errors.go
Normal 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,
|
||||
}
|
||||
}
|
||||
82
pkg/validator/oneof_custom_type_test.go
Normal file
82
pkg/validator/oneof_custom_type_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
116
pkg/validator/optional_pointer_test.go
Normal file
116
pkg/validator/optional_pointer_test.go
Normal 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
149
pkg/validator/validation.go
Normal 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
|
||||
}
|
||||
425
pkg/validator/validation_bench_test.go
Normal file
425
pkg/validator/validation_bench_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
451
pkg/validator/validation_test.go
Normal file
451
pkg/validator/validation_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
109
pkg/validator/validator_collection.go
Normal file
109
pkg/validator/validator_collection.go
Normal 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
|
||||
}
|
||||
}
|
||||
220
pkg/validator/validator_collection_test.go
Normal file
220
pkg/validator/validator_collection_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
71
pkg/validator/validator_common.go
Normal file
71
pkg/validator/validator_common.go
Normal 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
|
||||
}
|
||||
}
|
||||
270
pkg/validator/validator_common_test.go
Normal file
270
pkg/validator/validator_common_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
71
pkg/validator/validator_conditional.go
Normal file
71
pkg/validator/validator_conditional.go
Normal 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)
|
||||
}
|
||||
182
pkg/validator/validator_conditional_test.go
Normal file
182
pkg/validator/validator_conditional_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
26
pkg/validator/validator_custom.go
Normal file
26
pkg/validator/validator_custom.go
Normal 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
|
||||
}
|
||||
}
|
||||
47
pkg/validator/validator_custom_test.go
Normal file
47
pkg/validator/validator_custom_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
255
pkg/validator/validator_format.go
Normal file
255
pkg/validator/validator_format.go
Normal 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
|
||||
}
|
||||
}
|
||||
432
pkg/validator/validator_format_test.go
Normal file
432
pkg/validator/validator_format_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
203
pkg/validator/validator_numeric.go
Normal file
203
pkg/validator/validator_numeric.go
Normal 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
|
||||
}
|
||||
}
|
||||
95
pkg/validator/validator_numeric_test.go
Normal file
95
pkg/validator/validator_numeric_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
173
pkg/validator/validator_security.go
Normal file
173
pkg/validator/validator_security.go
Normal 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
|
||||
}
|
||||
}
|
||||
755
pkg/validator/validator_security_test.go
Normal file
755
pkg/validator/validator_security_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
275
pkg/validator/validator_string.go
Normal file
275
pkg/validator/validator_string.go
Normal 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
|
||||
}
|
||||
}
|
||||
303
pkg/validator/validator_string_test.go
Normal file
303
pkg/validator/validator_string_test.go
Normal 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
Reference in New Issue
Block a user