phorm

package module
v0.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

phorm

A Go HTTP client for phorm — a standalone business-document validation service built on phive and phive-rules by @phax.

This is the successor to the invopop/phive gRPC client, which is now archived. Instead of running our own Java gRPC wrapper, we deploy phorm and talk to it over its HTTP/JSON API. The package keeps the same request/response field names as the old generated gRPC client, so callers migrate by swapping only the constructor.

Install

go get github.com/invopop/phorm

Usage

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/invopop/phorm"
)

func main() {
	// baseURL of the phorm service and its X-Token auth value. An empty token
	// falls back to phorm.DefaultToken, so callers using phorm's stock token can
	// pass "".
	client := phorm.New("http://phorm:8080", os.Getenv("PHORM_TOKEN"))
	ctx := context.Background()

	// List validation rule sets (client-side filter).
	list, err := client.ListVesIds(ctx, &phorm.ListVesIdsRequest{Filter: "peppol"})
	if err != nil {
		log.Fatal(err)
	}
	for _, v := range list.Vesids {
		if v.Status == "VALID" {
			fmt.Printf("%s: %s\n", v.Vesid, v.Name)
		}
	}

	// Validate an XML document.
	xml, err := os.ReadFile("invoice.xml")
	if err != nil {
		log.Fatal(err)
	}
	result, err := client.ValidateXml(ctx, &phorm.ValidateXmlRequest{
		Vesid:      "eu.peppol.bis3:invoice:2024.5",
		XmlContent: xml,
	})
	if err != nil {
		log.Fatal(err) // the validation never ran; see "Errors vs. findings"
	}
	fmt.Printf("Valid: %v\n", result.Success)
	for _, r := range result.Results {
		for _, e := range r.Errors {
			fmt.Printf("  %s: %s\n", e.ErrorID, e.Message)
		}
	}
}
Errors vs. findings

Validation findings live in result.Results[].Errors / .Warnings, and result.Success reports the outcome.

A non-nil error means no validation happened at all: the service was unreachable, the X-Token was rejected (403), the VESID could not be resolved, or the body was not readable as XML.

A document that simply breaks a rule is not an error. Note that phorm answers one with HTTP 400 and the report as the body, which it also uses for a rejected request, so the status alone cannot tell the two apart — the client separates them by whether the body is a validation report, and returns the report either way.

Running phorm

phorm is a Java service. Run the upstream image directly:

docker run -d --name phorm -p 8080:8080 phelger/phorm

Use phelger/phorm-arm64 on arm64 hosts such as Apple Silicon. The image is published as phelger/phorm; there is no phax/phorm.

It takes a few seconds to start. It is ready once this returns HTTP 200:

curl -s -o /dev/null -w '%{http_code}\n' \
  -H 'X-Token: phorm-dev-token' \
  'http://localhost:8080/api/get/vesids?include-deprecated=true'

Key phorm settings:

  • phorm.api.requiredtoken — the X-Token the client must send. Defaults to phorm-dev-token, which is also this package's DefaultToken.
  • webapp.datapath — configuration and data location, /config/phorm in the image. Mount it to keep settings across restarts.

Migration from the phive gRPC client

gRPC (old) phorm HTTP (new)
phive.NewValidationServiceClient(conn) phorm.New(baseURL, token)
ValidateXml(ctx, req) POST /api/validate/{vesid}, raw XML body, X-Token header
ListVesIds(ctx, req) GET /api/get/vesids?include-deprecated=true (filter applied client-side)
PHIVE_ADDRESS=phive:50051 PHORM_URL=http://phorm:8080 + PHORM_TOKEN=…

Response field names (Success, Results, ValidationType, ArtifactId, Errors, Warnings, Level, Message, Location, TestId, ResolvedVesid) are unchanged, so ProcessValidationResponse-style code keeps working.

One behavioural difference is worth checking at each call site: the gRPC client reported an unreachable service and a failed validation the same way, through the response. Here an unreachable service is an error and a failed validation is not, so a caller that skips validation when the request errors no longer skips it for documents that are merely invalid.

Note: many rule sets are deprecated; filter by Status == "VALID" for current ones.

License

Apache License 2.0

  • phorm — validation service
  • phive — core validation engine
  • phive-rules — pre-built validation rules

Documentation

Overview

Package phorm is a Go HTTP client for a phorm (https://github.com/phax/phorm) business-document validation service. It replaces the gRPC client of the invopop/phive service; the request/response types keep the same exported field names so callers migrate by swapping only the constructor.

Index

Constants

View Source
const (

	// DefaultToken is phorm's built-in default X-Token. phorm always requires a
	// matching non-empty token and cannot disable auth, but when it is only
	// reachable inside a trusted network the token is not a security boundary, so
	// New falls back to this when no token is supplied.
	DefaultToken = "phorm-dev-token"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to a phorm validation service over HTTP.

func New

func New(baseURL, token string, opts ...Option) *Client

New creates a phorm client for the service at baseURL (e.g. "http://phorm:8080") authenticating with the given X-Token value. An empty token falls back to DefaultToken, so callers pointing at a phorm that uses the stock token need not configure one.

func (*Client) ListVesIds

func (c *Client) ListVesIds(ctx context.Context, req *ListVesIdsRequest) (*ListVesIdsResponse, error)

ListVesIds lists the available VESIDs via GET /api/get/vesids. req.Filter, if set, is applied client-side (phorm has no server-side filter).

func (*Client) ValidateXml

func (c *Client) ValidateXml(ctx context.Context, req *ValidateXmlRequest) (*ValidateXmlResponse, error)

ValidateXml validates req.XmlContent against req.Vesid via POST /api/validate/{vesid}.

phorm answers a document that breaks a rule with HTTP 400 and the validation report as the body, so the status code alone cannot tell a failed validation from a rejected request: an unresolvable VESID and a body that is not XML are also 400, and a bad token is 403. What separates them is the body, which is the JSON report only when the validation actually ran.

A report is therefore returned as a response whatever the status, with the findings in resp.Results and resp.Success reporting the outcome. A non-nil error means the request never produced a report — the service is unreachable, the token was rejected, the VESID could not be resolved, or the document was not readable as XML.

type ListVesIdsRequest

type ListVesIdsRequest struct {
	// Filter, when non-empty, keeps only VESIDs whose id or name contains it
	// (case-insensitive). phorm has no server-side filter, so it is applied
	// client-side.
	Filter string
}

ListVesIdsRequest is a request to list the available VESIDs.

type ListVesIdsResponse

type ListVesIdsResponse struct {
	Vesids       []*VesIdInfo
	ErrorMessage string
}

ListVesIdsResponse contains the list of available VESIDs.

type Option

type Option func(*Client)

Option configures a Client.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient overrides the underlying *http.Client (e.g. to set a custom timeout or transport).

type ValidateXmlRequest

type ValidateXmlRequest struct {
	// Vesid is sent as the {vesid} path segment of POST /api/validate/{vesid}.
	Vesid string
	// XmlContent is the raw XML request body.
	XmlContent []byte
	// SourceIdentifier is retained for caller-side logging only; phorm's
	// validate endpoint does not accept it.
	SourceIdentifier string
}

ValidateXmlRequest is a request to validate an XML document.

type ValidateXmlResponse

type ValidateXmlResponse struct {
	Success bool
	// ResolvedVesid is populated from phorm's `ves.vesid` when present.
	ResolvedVesid string
	Results       []*ValidationLayerResult
	// ErrorMessage carries an execution-level error. With phorm these arrive as
	// non-2xx HTTP responses and are returned as a Go error instead, so this is
	// normally empty.
	ErrorMessage string
	Timestamp    string
}

ValidateXmlResponse is the result of a validation.

type ValidationError

type ValidationError struct {
	// Level is "ERROR", "WARN", etc. (phorm's `errorLevel`).
	Level string
	// ErrorID is the rule identifier (phorm's `errorID`), e.g. "UBL-CR-397".
	ErrorID string
	// Message is phorm's `errorText`.
	Message string
	// Location is a human-readable "line N, col M" from phorm's `errorLocationObj`.
	Location string
	// Xpath is phorm's `errorFieldName`.
	Xpath string
	// TestId is the schematron rule id (phorm's `test`).
	TestId  string
	Details map[string]string
}

ValidationError is a single error or warning item.

type ValidationLayerResult

type ValidationLayerResult struct {
	// ValidationType is mapped from phorm's `artifactType`.
	ValidationType string
	// ArtifactId is mapped from phorm's `artifactPath`.
	ArtifactId string
	Success    bool
	Errors     []*ValidationError
	Warnings   []*ValidationError
}

ValidationLayerResult holds the outcome of a single validation layer/artifact.

type VesIdInfo

type VesIdInfo struct {
	Vesid   string
	Name    string
	Version string
	// Status is "VALID" or "DEPRECATED" (derived from phorm's `deprecated` flag).
	Status string
}

VesIdInfo describes a single VESID.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL