docfast/sdk/go/docfast.go
DocFast Bot bc67c52d3a
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
feat: add Go, PHP, and Laravel SDKs
- Go SDK: zero deps, functional options pattern, full endpoint coverage
- PHP SDK: PHP 8.1+, curl-based, PdfOptions class, PSR-4 autoloading
- Laravel package: ServiceProvider, Facade, config publishing
- All SDKs document complete PDF options including new v0.4.5 params
2026-02-21 13:29:48 +00:00

293 lines
8.2 KiB
Go

// Package docfast provides a Go client for the DocFast HTML/Markdown to PDF API.
//
// Usage:
//
// client := docfast.New("df_pro_your_key")
// pdf, err := client.HTML("<h1>Hello</h1>", nil)
package docfast
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const defaultBaseURL = "https://docfast.dev"
// Margin defines PDF page margins using CSS units (e.g. "20mm", "1in").
type Margin struct {
Top string `json:"top,omitempty"`
Bottom string `json:"bottom,omitempty"`
Left string `json:"left,omitempty"`
Right string `json:"right,omitempty"`
}
// PDFOptions configures PDF generation. All fields are optional.
type PDFOptions struct {
// Page size: A4, Letter, Legal, A3, A5, Tabloid. Ignored if Width/Height set.
Format string `json:"format,omitempty"`
// Landscape orientation.
Landscape bool `json:"landscape,omitempty"`
// Page margins.
Margin *Margin `json:"margin,omitempty"`
// Print background graphics and colors. Default: true.
PrintBackground *bool `json:"printBackground,omitempty"`
// Suggested filename for the PDF download.
Filename string `json:"filename,omitempty"`
// HTML template for page header. Requires DisplayHeaderFooter: true.
// Supports CSS classes: date, title, url, pageNumber, totalPages.
HeaderTemplate string `json:"headerTemplate,omitempty"`
// HTML template for page footer. Same classes as HeaderTemplate.
FooterTemplate string `json:"footerTemplate,omitempty"`
// Show header and footer templates.
DisplayHeaderFooter bool `json:"displayHeaderFooter,omitempty"`
// Scale of webpage rendering (0.1 to 2.0). Default: 1.
Scale float64 `json:"scale,omitempty"`
// Paper ranges to print, e.g. "1-3,5".
PageRanges string `json:"pageRanges,omitempty"`
// Give CSS @page size priority over Format.
PreferCSSPageSize bool `json:"preferCSSPageSize,omitempty"`
// Paper width with units (e.g. "8.5in"). Overrides Format.
Width string `json:"width,omitempty"`
// Paper height with units (e.g. "11in"). Overrides Format.
Height string `json:"height,omitempty"`
}
// Template describes an available PDF template.
type Template struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
}
// Error is returned when the API responds with an error.
type Error struct {
StatusCode int
Message string
Code string
}
func (e *Error) Error() string {
if e.Code != "" {
return fmt.Sprintf("docfast: %s (code=%s, status=%d)", e.Message, e.Code, e.StatusCode)
}
return fmt.Sprintf("docfast: %s (status=%d)", e.Message, e.StatusCode)
}
// ClientOption configures the Client.
type ClientOption func(*Client)
// WithBaseURL sets a custom API base URL.
func WithBaseURL(url string) ClientOption {
return func(c *Client) { c.baseURL = url }
}
// WithHTTPClient sets a custom http.Client.
func WithHTTPClient(hc *http.Client) ClientOption {
return func(c *Client) { c.httpClient = hc }
}
// Client is the DocFast API client.
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
}
// New creates a new DocFast client.
func New(apiKey string, opts ...ClientOption) *Client {
c := &Client{
apiKey: apiKey,
baseURL: defaultBaseURL,
httpClient: &http.Client{
Timeout: 60 * time.Second,
},
}
for _, o := range opts {
o(c)
}
return c
}
func (c *Client) post(path string, body map[string]interface{}) ([]byte, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("docfast: marshal error: %w", err)
}
req, err := http.NewRequest("POST", c.baseURL+path, bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("docfast: request error: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("docfast: request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("docfast: read error: %w", err)
}
if resp.StatusCode >= 400 {
apiErr := &Error{StatusCode: resp.StatusCode, Message: fmt.Sprintf("HTTP %d", resp.StatusCode)}
var errResp struct {
Error string `json:"error"`
Code string `json:"code"`
}
if json.Unmarshal(respBody, &errResp) == nil {
if errResp.Error != "" {
apiErr.Message = errResp.Error
}
apiErr.Code = errResp.Code
}
return nil, apiErr
}
return respBody, nil
}
func (c *Client) get(path string) ([]byte, error) {
req, err := http.NewRequest("GET", c.baseURL+path, nil)
if err != nil {
return nil, fmt.Errorf("docfast: request error: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("docfast: request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("docfast: read error: %w", err)
}
if resp.StatusCode >= 400 {
apiErr := &Error{StatusCode: resp.StatusCode, Message: fmt.Sprintf("HTTP %d", resp.StatusCode)}
var errResp struct {
Error string `json:"error"`
Code string `json:"code"`
}
if json.Unmarshal(respBody, &errResp) == nil && errResp.Error != "" {
apiErr.Message = errResp.Error
apiErr.Code = errResp.Code
}
return nil, apiErr
}
return respBody, nil
}
func mergeOptions(body map[string]interface{}, opts *PDFOptions) {
if opts == nil {
return
}
if opts.Format != "" {
body["format"] = opts.Format
}
if opts.Landscape {
body["landscape"] = true
}
if opts.Margin != nil {
body["margin"] = opts.Margin
}
if opts.PrintBackground != nil {
body["printBackground"] = *opts.PrintBackground
}
if opts.Filename != "" {
body["filename"] = opts.Filename
}
if opts.HeaderTemplate != "" {
body["headerTemplate"] = opts.HeaderTemplate
}
if opts.FooterTemplate != "" {
body["footerTemplate"] = opts.FooterTemplate
}
if opts.DisplayHeaderFooter {
body["displayHeaderFooter"] = true
}
if opts.Scale != 0 {
body["scale"] = opts.Scale
}
if opts.PageRanges != "" {
body["pageRanges"] = opts.PageRanges
}
if opts.PreferCSSPageSize {
body["preferCSSPageSize"] = true
}
if opts.Width != "" {
body["width"] = opts.Width
}
if opts.Height != "" {
body["height"] = opts.Height
}
}
// HTML converts HTML content to PDF. Returns the raw PDF bytes.
func (c *Client) HTML(html string, opts *PDFOptions) ([]byte, error) {
body := map[string]interface{}{"html": html}
mergeOptions(body, opts)
return c.post("/v1/convert/html", body)
}
// HTMLWithCSS converts an HTML fragment with optional CSS to PDF.
func (c *Client) HTMLWithCSS(html, css string, opts *PDFOptions) ([]byte, error) {
body := map[string]interface{}{"html": html, "css": css}
mergeOptions(body, opts)
return c.post("/v1/convert/html", body)
}
// Markdown converts Markdown content to PDF.
func (c *Client) Markdown(markdown string, opts *PDFOptions) ([]byte, error) {
body := map[string]interface{}{"markdown": markdown}
mergeOptions(body, opts)
return c.post("/v1/convert/markdown", body)
}
// MarkdownWithCSS converts Markdown with optional CSS to PDF.
func (c *Client) MarkdownWithCSS(markdown, css string, opts *PDFOptions) ([]byte, error) {
body := map[string]interface{}{"markdown": markdown, "css": css}
mergeOptions(body, opts)
return c.post("/v1/convert/markdown", body)
}
// URL converts a web page at the given URL to PDF.
func (c *Client) URL(url string, opts *PDFOptions) ([]byte, error) {
body := map[string]interface{}{"url": url}
mergeOptions(body, opts)
return c.post("/v1/convert/url", body)
}
// Templates returns the list of available PDF templates.
func (c *Client) Templates() ([]Template, error) {
data, err := c.get("/v1/templates")
if err != nil {
return nil, err
}
var result struct {
Templates []Template `json:"templates"`
}
if err := json.Unmarshal(data, &result); err != nil {
return nil, fmt.Errorf("docfast: decode error: %w", err)
}
return result.Templates, nil
}
// RenderTemplate renders a template with the given data and returns PDF bytes.
func (c *Client) RenderTemplate(templateID string, data map[string]interface{}) ([]byte, error) {
body := map[string]interface{}{"data": data}
return c.post("/v1/templates/"+templateID, body)
}
// Bool is a helper to create a *bool for PDFOptions.PrintBackground.
func Bool(v bool) *bool { return &v }