feat: add API v2 support by adding APGroup and DNSRecord resource handling with generated code (#23)

* feat: add API v2 support by adding APGroup and DNSRecord resource handling with generated code

* fix tests
This commit is contained in:
Mateusz Filipowicz
2025-02-17 15:39:54 +01:00
committed by GitHub
parent dca894e8e7
commit aa188a6faa
14 changed files with 482 additions and 175 deletions

119
codegen/apiv2.go.tmpl Normal file
View File

@@ -0,0 +1,119 @@
{{- $structName := .StructName }}
{{ define "field" }}
{{ .FieldName }} {{ if .IsArray }}[]{{end}}{{ .FieldType }} `json:"{{ .JSONName }}{{ if .OmitEmpty }},omitempty{{ end }}"{{if .FieldValidation }} {{ .FieldValidation }}{{ end }}` {{ if .FieldValidationComment }}// {{ .FieldValidationComment }}{{ end }} {{- end }}
{{ define "field-customUnmarshalType" }}
{{- if eq .CustomUnmarshalType "" }}{{else}}
{{ .FieldName }} {{ if .IsArray }}[]{{end}}{{ .CustomUnmarshalType }} `json:"{{ .JSONName }}"`{{ end }} {{- end }}
{{ define "typecast" }}
{{- if ne .CustomUnmarshalFunc "" }}
dst.{{ .FieldName }}= {{ .CustomUnmarshalFunc }}(aux.{{ .FieldName }})
{{- else if eq .CustomUnmarshalType "" }}{{else}}
{{- if .IsArray }}
dst.{{ .FieldName }}= make([]{{ .FieldType }}, len(aux.{{ .FieldName }}))
for i, v := range aux.{{ .FieldName }} {
dst.{{ .FieldName }}[i] = {{ .FieldType }}(v)
}
{{- else }}
dst.{{ .FieldName }} = {{ .FieldType }}(aux.{{ .FieldName }})
{{- end }}{{- end }}{{- end }}
// Code generated from ace.jar fields *.json files
// DO NOT EDIT.
package unifi
import (
"context"
"encoding/json"
"fmt"
)
// just to fix compile issues with the import
var (
_ context.Context
_ fmt.Formatter
_ json.Marshaler
)
{{ range $k, $v := .Types }}
type {{ $k }} struct {
{{ range $fk, $fv := $v.Fields }}{{ if not $fv }}
{{ else }}{{- template "field" $fv }}{{ end }}{{ end }}
}
func (dst *{{ $k }}) UnmarshalJSON(b []byte) error {
type Alias {{ $k }}
aux := &struct {
{{- range $fk, $fv := $v.Fields }}{{ if not $fv }}
{{- else }}{{- template "field-customUnmarshalType" $fv }}{{ end }}{{- end }}
*Alias
}{
Alias: (*Alias)(dst),
}
err := json.Unmarshal(b, &aux)
if err != nil {
return fmt.Errorf("unable to unmarshal alias: %w", err)
}
{{- range $fk, $fv := $v.Fields }}{{ if not $fv }}
{{- else }}{{- template "typecast" $fv }}{{ end }}{{ end }}
return nil
}
{{ end }}
func (c *client) list{{ .StructName }}(ctx context.Context, site string) ([]{{ .StructName }}, error) {
var respBody []{{ .StructName }}
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/{{ .ResourcePath }}", c.apiPaths.ApiV2Path, site), nil, &respBody)
if err != nil {
return nil, err
}
return respBody, nil
}
func (c *client) get{{ .StructName }}(ctx context.Context, site, id string) (*{{ .StructName }}, error) {
var respBody {{ .StructName }}
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/{{ .ResourcePath }}/%s", c.apiPaths.ApiV2Path, site, id), nil, &respBody)
if err != nil {
return nil, err
}
if respBody.ID == "" {
return nil, ErrNotFound
}
return &respBody, nil
}
func (c *client) delete{{ .StructName }}(ctx context.Context, site, id string) error {
err := c.Delete(ctx, fmt.Sprintf("%s/site/%s/{{ .ResourcePath }}/%s", c.apiPaths.ApiV2Path, site, id), struct{}{}, nil)
if err != nil {
return err
}
return nil
}
func (c *client) create{{ .StructName }}(ctx context.Context, site string, d *{{ .StructName }}) (*{{ .StructName }}, error) {
var respBody {{ .StructName }}
err := c.Post(ctx, fmt.Sprintf("%s/site/%s/{{ .ResourcePath }}", c.apiPaths.ApiV2Path, site), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
}
func (c *client) update{{ .StructName }}(ctx context.Context, site string, d *{{ .StructName }}) (*{{ .StructName }}, error) {
var respBody {{ .StructName }}
err := c.Put(ctx, fmt.Sprintf("%s/site/%s/{{ .ResourcePath }}/%s", c.apiPaths.ApiV2Path, site, d.ID), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
}

View File

@@ -301,6 +301,8 @@ customizations:
omitEmpty: true
NetworkID:
omitEmpty: true
APGroup:
resourcePath: "apgroups"
ChannelPlan:
fields:
Channel:
@@ -312,6 +314,8 @@ customizations:
TxPower:
ifFieldType: "string"
customUnmarshalType: "numberOrString"
DNSRecord:
resourcePath: "static-dns"
Device:
fields:
_all:

View File

@@ -26,6 +26,7 @@ type Generate struct {
type ResourceCustomization struct {
ResourceName string `yaml:"-"`
Fields map[string]*FieldCustomization `yaml:"fields"`
ResourcePath string `yaml:"resourcePath"`
}
type ClientCustomization struct {
@@ -75,6 +76,9 @@ func (r *ResourceCustomization) ApplyTo(resource *Resource) {
}
return currentProcessor(name, f)
}
if r.ResourcePath != "" {
resource.ResourcePath = r.ResourcePath
}
} else {
resource.FieldProcessor = compositeCustomizationsProcessor(customizationsProcessor)
}
@@ -155,6 +159,9 @@ func NewCodeCustomizer(customizationsPath string) (*CodeCustomizer, error) {
}
func (r *CodeCustomizer) IsExcludedFromClient(resourceName string) bool {
if r.Customizations.Client == nil || r.Customizations.Client.ExcludeResources == nil {
return false
}
for _, excludedResource := range r.Customizations.Client.ExcludeResources {
prefixedAll := strings.HasPrefix(excludedResource, "*")
suffixedAll := strings.HasSuffix(excludedResource, "*")

View File

@@ -41,23 +41,32 @@ func generateCodeFromTemplate(templateName, templateContent string, toWrite any)
}
// generateCode generates code for each generation source and writes it to file.
func generateCode(fieldsDir string, outDir string, customizer CodeCustomizer) error {
func generateCode(fieldsDir, outDir string, customizer CodeCustomizer) error {
if _, err := ensurePath(outDir); err != nil {
return fmt.Errorf("unable to create output directory %s: %w", outDir, err)
}
generators := make([]Generatable, 0)
resources, err := buildResourcesFromDownloadedFields(fieldsDir, customizer)
resources, err := buildResourcesFromDownloadedFields(fieldsDir, customizer, false)
if err != nil {
return fmt.Errorf("failed to build resources from downloaded fields: %w", err)
}
codegenPath, err := findCodegenDir()
if err != nil {
return fmt.Errorf("failed to find codegen directory: %w", err)
}
resourcesCustomV2, err := buildCustomResources(filepath.Join(codegenPath, "v2"), customizer, true)
if err != nil {
return fmt.Errorf("failed to build resources from downloaded fields: %w", err)
}
resources = append(resources, resourcesCustomV2...)
cb := NewClientInfoBuilder()
customizer.ApplyToClient(cb)
for _, resource := range resources {
if customizer.IsExcludedFromClient(resource.Name()) {
continue
}
if !customizer.IsExcludedFromClient(resource.Name()) {
cb.AddResource(resource)
}
customizer.ApplyToResource(resource)
generators = append(generators, resource)
}

View File

@@ -89,6 +89,11 @@ type Resource struct {
ResourcePath string
Types map[string]*FieldInfo
FieldProcessor FieldProcessor
V2 bool
}
func (r *Resource) IsV2() bool {
return r.V2
}
func (r *Resource) BaseType() *FieldInfo {
@@ -294,7 +299,13 @@ func (r *Resource) processJSON(b []byte) error {
//go:embed api.go.tmpl
var apiGoTemplate string
//go:embed apiv2.go.tmpl
var apiGoV2Template string
func (r *Resource) GenerateCode() (string, error) {
if r.IsV2() {
return generateCodeFromTemplate("apiv2.go.tmpl", apiGoV2Template, r)
}
return generateCodeFromTemplate("api.go.tmpl", apiGoTemplate, r)
}
@@ -320,7 +331,7 @@ func normalizeValidation(re string) string {
var skippable = []string{"AuthenticationRequest.json", "Setting.json", "Wall.json"}
func buildResourcesFromDownloadedFields(fieldsDir string, customizer CodeCustomizer) ([]*Resource, error) {
func buildResourcesFromDownloadedFields(fieldsDir string, customizer CodeCustomizer, v2 bool) ([]*Resource, error) {
fieldsFiles, err := os.ReadDir(fieldsDir)
if err != nil {
return nil, fmt.Errorf("unable to read fields directory %s: %w", fieldsDir, err)
@@ -349,7 +360,7 @@ func buildResourcesFromDownloadedFields(fieldsDir string, customizer CodeCustomi
}
resource := NewResource(structName, urlPath)
customizeResource(resource)
customizeResource(resource, v2)
customizer.ApplyToResource(resource)
err = resource.processJSON(b)
@@ -362,6 +373,10 @@ func buildResourcesFromDownloadedFields(fieldsDir string, customizer CodeCustomi
return resources, nil
}
func buildCustomResources(dir string, customizer CodeCustomizer, v2 bool) ([]*Resource, error) {
return buildResourcesFromDownloadedFields(dir, customizer, v2)
}
func customizeBaseType(resource *Resource) {
baseType := resource.BaseType()
if resource.IsSetting() {
@@ -395,8 +410,11 @@ func customizeBaseType(resource *Resource) {
}
}
func customizeResource(resource *Resource) {
func customizeResource(resource *Resource, v2 bool) {
customizeBaseType(resource)
if v2 {
resource.V2 = true
}
switch resource.StructName {
case "SettingGlobalAp":

View File

@@ -354,7 +354,7 @@ func TestBuildResourcesFromDownloadedFields(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
a := assert.New(t)
resources, err := buildResourcesFromDownloadedFields(tc.dir, CodeCustomizer{})
resources, err := buildResourcesFromDownloadedFields(tc.dir, CodeCustomizer{}, false)
if tc.errorContains != "" {
require.ErrorContains(t, err, tc.errorContains)
a.Nil(resources)

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
)
// ensurePath checks if a path exists and is a directory, if not it creates the directory. Returns true if the directories were created.
@@ -24,3 +25,29 @@ func ensurePath(path string) (bool, error) {
}
return false, nil
}
func findProjectRoot() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
// Walk up the directory tree until we find a go.mod file
for {
if _, err := os.Stat(filepath.Join(wd, "go.mod")); err == nil {
return wd, nil
}
if wd == "/" {
break
}
wd = filepath.Dir(wd)
}
return "", errors.New("unable to find project root")
}
func findCodegenDir() (string, error) {
root, err := findProjectRoot()
if err != nil {
return "", err
}
return filepath.Join(root, "codegen"), nil
}

4
codegen/v2/APGroup.json Normal file
View File

@@ -0,0 +1,4 @@
{
"name": "",
"device_macs": [""]
}

10
codegen/v2/DNSRecord.json Normal file
View File

@@ -0,0 +1,10 @@
{
"enabled": "true|false",
"key": ".{1,256}",
"port": "^[0-9][0-9]?$|^",
"priority": "^[0-9][0-9]?$|^",
"record_type": "A|AAAA|CNAME|MX|NS|PTR|SOA|SRV|TXT",
"ttl": "^[0-9][0-9]?$|^",
"value": ".{1,256}",
"weight": "^[0-9][0-9]?$|^"
}

100
unifi/ap_group.generated.go generated Normal file
View File

@@ -0,0 +1,100 @@
// Code generated from ace.jar fields *.json files
// DO NOT EDIT.
package unifi
import (
"context"
"encoding/json"
"fmt"
)
// just to fix compile issues with the import
var (
_ context.Context
_ fmt.Formatter
_ json.Marshaler
)
type APGroup struct {
ID string `json:"_id,omitempty"`
SiteID string `json:"site_id,omitempty"`
Hidden bool `json:"attr_hidden,omitempty"`
HiddenID string `json:"attr_hidden_id,omitempty"`
NoDelete bool `json:"attr_no_delete,omitempty"`
NoEdit bool `json:"attr_no_edit,omitempty"`
DeviceMACs []string `json:"device_macs,omitempty"`
Name string `json:"name,omitempty"`
}
func (dst *APGroup) UnmarshalJSON(b []byte) error {
type Alias APGroup
aux := &struct {
*Alias
}{
Alias: (*Alias)(dst),
}
err := json.Unmarshal(b, &aux)
if err != nil {
return fmt.Errorf("unable to unmarshal alias: %w", err)
}
return nil
}
func (c *client) listAPGroup(ctx context.Context, site string) ([]APGroup, error) {
var respBody []APGroup
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/apgroups", c.apiPaths.ApiV2Path, site), nil, &respBody)
if err != nil {
return nil, err
}
return respBody, nil
}
func (c *client) getAPGroup(ctx context.Context, site, id string) (*APGroup, error) {
var respBody APGroup
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/apgroups/%s", c.apiPaths.ApiV2Path, site, id), nil, &respBody)
if err != nil {
return nil, err
}
if respBody.ID == "" {
return nil, ErrNotFound
}
return &respBody, nil
}
func (c *client) deleteAPGroup(ctx context.Context, site, id string) error {
err := c.Delete(ctx, fmt.Sprintf("%s/site/%s/apgroups/%s", c.apiPaths.ApiV2Path, site, id), struct{}{}, nil)
if err != nil {
return err
}
return nil
}
func (c *client) createAPGroup(ctx context.Context, site string, d *APGroup) (*APGroup, error) {
var respBody APGroup
err := c.Post(ctx, fmt.Sprintf("%s/site/%s/apgroups", c.apiPaths.ApiV2Path, site), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
}
func (c *client) updateAPGroup(ctx context.Context, site string, d *APGroup) (*APGroup, error) {
var respBody APGroup
err := c.Put(ctx, fmt.Sprintf("%s/site/%s/apgroups/%s", c.apiPaths.ApiV2Path, site, d.ID), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
}

View File

@@ -2,94 +2,24 @@ package unifi
import (
"context"
"fmt"
)
// just to fix compile issues with the import.
var (
_ fmt.Formatter
_ context.Context
)
// This is a v2 API object, so manually coded for now, need to figure out generation...
type APGroup struct {
ID string `json:"_id,omitempty"`
Hidden bool `json:"attr_hidden,omitempty"`
HiddenID string `json:"attr_hidden_id,omitempty"`
NoDelete bool `json:"attr_no_delete,omitempty"`
NoEdit bool `json:"attr_no_edit,omitempty"`
Name string `json:"name"`
DeviceMACs []string `json:"device_macs"`
}
func (c *client) ListAPGroup(ctx context.Context, site string) ([]APGroup, error) {
var respBody []APGroup
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/apgroups", c.apiPaths.ApiV2Path, site), nil, &respBody)
if err != nil {
return nil, err
}
return respBody, nil
return c.listAPGroup(ctx, site)
}
// func (c *client) getWLANGroup(ctx context.Context, site, id string) (*WLANGroup, error) {
// var respBody struct {
// Meta `json:"Meta"`
// Data []WLANGroup `json:"data"`
// }
// err := c.Get(ctx, fmt.Sprintf("s/%s/rest/wlangroup/%s", site, id), nil, &respBody)
// if err != nil {
// return nil, err
// }
// if len(respBody.Data) != 1 {
// return nil, ErrNotFound
// }
// d := respBody.Data[0]
// return &d, nil
// }
// func (c *client) deleteWLANGroup(ctx context.Context, site, id string) error {
// err := c.Delete(ctx, fmt.Sprintf("s/%s/rest/wlangroup/%s", site, id), struct{}{}, nil)
// if err != nil {
// return err
// }
// return nil
// }
func (c *client) CreateAPGroup(ctx context.Context, site string, d *APGroup) (*APGroup, error) {
var respBody APGroup
err := c.Post(ctx, fmt.Sprintf("%s/site/%s/apgroups", c.apiPaths.ApiV2Path, site), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
return c.createAPGroup(ctx, site, d)
}
// func (c *client) updateWLANGroup(ctx context.Context, site string, d *WLANGroup) (*WLANGroup, error) {
// var respBody struct {
// Meta `json:"Meta"`
// Data []WLANGroup `json:"data"`
// }
func (c *client) GetAPGroup(ctx context.Context, site, id string) (*APGroup, error) {
return c.getAPGroup(ctx, site, id)
}
// err := c.Put(ctx, fmt.Sprintf("s/%s/rest/wlangroup/%s", site, d.ID), d, &respBody)
// if err != nil {
// return nil, err
// }
func (c *client) DeleteAPGroup(ctx context.Context, site, id string) error {
return c.deleteAPGroup(ctx, site, id)
}
// if len(respBody.Data) != 1 {
// return nil, ErrNotFound
// }
// new := respBody.Data[0]
// return &new, nil
// }
func (c *client) UpdateAPGroup(ctx context.Context, site string, d *APGroup) (*APGroup, error) {
return c.updateAPGroup(ctx, site, d)
}

View File

@@ -33,6 +33,25 @@ type Client interface {
// Put sends a PUT request to the controller.
Put(ctx context.Context, apiPath string, reqBody interface{}, respBody interface{}) error
// ==== client methods for APGroup resource ====
// CreateAPGroup creates a resource
CreateAPGroup(ctx context.Context, site string, a *APGroup) (*APGroup, error)
// DeleteAPGroup deletes a resource
DeleteAPGroup(ctx context.Context, site string, id string) error
// GetAPGroup retrieves a resource
GetAPGroup(ctx context.Context, site string, id string) (*APGroup, error)
// ListAPGroup lists the resources
ListAPGroup(ctx context.Context, site string) ([]APGroup, error)
// UpdateAPGroup updates a resource
UpdateAPGroup(ctx context.Context, site string, a *APGroup) (*APGroup, error)
// ==== end of client methods for APGroup resource ====
// ==== client methods for Account resource ====
// CreateAccount creates a resource
@@ -52,6 +71,25 @@ type Client interface {
// ==== end of client methods for Account resource ====
// ==== client methods for DNSRecord resource ====
// CreateDNSRecord creates a resource
CreateDNSRecord(ctx context.Context, site string, d *DNSRecord) (*DNSRecord, error)
// DeleteDNSRecord deletes a resource
DeleteDNSRecord(ctx context.Context, site string, id string) error
// GetDNSRecord retrieves a resource
GetDNSRecord(ctx context.Context, site string, id string) (*DNSRecord, error)
// ListDNSRecord lists the resources
ListDNSRecord(ctx context.Context, site string) ([]DNSRecord, error)
// UpdateDNSRecord updates a resource
UpdateDNSRecord(ctx context.Context, site string, d *DNSRecord) (*DNSRecord, error)
// ==== end of client methods for DNSRecord resource ====
// ==== client methods for Device resource ====
// AdoptDevice adopts a device by MAC address.

115
unifi/dns_record.generated.go generated Normal file
View File

@@ -0,0 +1,115 @@
// Code generated from ace.jar fields *.json files
// DO NOT EDIT.
package unifi
import (
"context"
"encoding/json"
"fmt"
)
// just to fix compile issues with the import
var (
_ context.Context
_ fmt.Formatter
_ json.Marshaler
)
type DNSRecord struct {
ID string `json:"_id,omitempty"`
SiteID string `json:"site_id,omitempty"`
Hidden bool `json:"attr_hidden,omitempty"`
HiddenID string `json:"attr_hidden_id,omitempty"`
NoDelete bool `json:"attr_no_delete,omitempty"`
NoEdit bool `json:"attr_no_edit,omitempty"`
Enabled bool `json:"enabled"`
Key string `json:"key,omitempty" validate:"omitempty,gte=1,lte=256"` // .{1,256}
Port int `json:"port,omitempty"` // ^[0-9][0-9]?$|^
Priority int `json:"priority,omitempty"` // ^[0-9][0-9]?$|^
RecordType string `json:"record_type,omitempty" validate:"omitempty,oneof=A AAAA CNAME MX NS PTR SOA SRV TXT"` // A|AAAA|CNAME|MX|NS|PTR|SOA|SRV|TXT
Ttl int `json:"ttl,omitempty"` // ^[0-9][0-9]?$|^
Value string `json:"value,omitempty" validate:"omitempty,gte=1,lte=256"` // .{1,256}
Weight int `json:"weight,omitempty"` // ^[0-9][0-9]?$|^
}
func (dst *DNSRecord) UnmarshalJSON(b []byte) error {
type Alias DNSRecord
aux := &struct {
Port emptyStringInt `json:"port"`
Priority emptyStringInt `json:"priority"`
Ttl emptyStringInt `json:"ttl"`
Weight emptyStringInt `json:"weight"`
*Alias
}{
Alias: (*Alias)(dst),
}
err := json.Unmarshal(b, &aux)
if err != nil {
return fmt.Errorf("unable to unmarshal alias: %w", err)
}
dst.Port = int(aux.Port)
dst.Priority = int(aux.Priority)
dst.Ttl = int(aux.Ttl)
dst.Weight = int(aux.Weight)
return nil
}
func (c *client) listDNSRecord(ctx context.Context, site string) ([]DNSRecord, error) {
var respBody []DNSRecord
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/static-dns", c.apiPaths.ApiV2Path, site), nil, &respBody)
if err != nil {
return nil, err
}
return respBody, nil
}
func (c *client) getDNSRecord(ctx context.Context, site, id string) (*DNSRecord, error) {
var respBody DNSRecord
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/static-dns/%s", c.apiPaths.ApiV2Path, site, id), nil, &respBody)
if err != nil {
return nil, err
}
if respBody.ID == "" {
return nil, ErrNotFound
}
return &respBody, nil
}
func (c *client) deleteDNSRecord(ctx context.Context, site, id string) error {
err := c.Delete(ctx, fmt.Sprintf("%s/site/%s/static-dns/%s", c.apiPaths.ApiV2Path, site, id), struct{}{}, nil)
if err != nil {
return err
}
return nil
}
func (c *client) createDNSRecord(ctx context.Context, site string, d *DNSRecord) (*DNSRecord, error) {
var respBody DNSRecord
err := c.Post(ctx, fmt.Sprintf("%s/site/%s/static-dns", c.apiPaths.ApiV2Path, site), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
}
func (c *client) updateDNSRecord(ctx context.Context, site string, d *DNSRecord) (*DNSRecord, error) {
var respBody DNSRecord
err := c.Put(ctx, fmt.Sprintf("%s/site/%s/static-dns/%s", c.apiPaths.ApiV2Path, site, d.ID), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
}

View File

@@ -1,110 +1,36 @@
// Custom package for handling DNS records in client Controller
package unifi
import (
"context"
"encoding/json"
"fmt"
)
type DNSRecord struct {
ID string `json:"_id,omitempty"`
SiteID string `json:"site_id,omitempty"`
Hidden bool `json:"attr_hidden,omitempty"`
HiddenID string `json:"attr_hidden_id,omitempty"`
NoDelete bool `json:"attr_no_delete,omitempty"`
NoEdit bool `json:"attr_no_edit,omitempty"`
Enabled bool `json:"enabled"`
Key string `json:"key,omitempty" validate:"required,gte=1,lte=128"` // .{1,128}
Port int `json:"port,omitempty"`
Priority int `json:"priority,omitempty" validate:"omitempty,gte=1,lte=128"` // .{1,128}
RecordType string `json:"record_type,omitempty" validate:"required,oneof=A AAAA MX NS PTR SOA SRV TXT"` // A|AAAA|CNAME|MX|NS|PTR|SOA|SRV|TXT
Ttl int `json:"ttl,omitempty"`
Value string `json:"value,omitempty" validate:"required,gte=1,lte=256"` // .{1,256}
Weight int `json:"weight,omitempty"`
}
func (dst *DNSRecord) UnmarshalJSON(b []byte) error {
type Alias DNSRecord
aux := &struct {
Port emptyStringInt `json:"port"`
Priority emptyStringInt `json:"priority"`
Ttl emptyStringInt `json:"ttl"`
Weight emptyStringInt `json:"weight"`
*Alias
}{
Alias: (*Alias)(dst),
}
err := json.Unmarshal(b, &aux)
if err != nil {
return fmt.Errorf("unable to unmarshal alias: %w", err)
}
dst.Port = int(aux.Port)
dst.Ttl = int(aux.Ttl)
dst.Weight = int(aux.Weight)
return nil
}
func (c *client) ListDNSRecord(ctx context.Context, site string) ([]DNSRecord, error) {
var respBody []DNSRecord
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/static-dns", c.apiPaths.ApiV2Path, site), nil, &respBody)
if err != nil {
return nil, err
}
return respBody, nil
return c.listDNSRecord(ctx, site)
}
func (c *client) GetDNSRecord(ctx context.Context, site, id string) (*DNSRecord, error) {
var respBody DNSRecord
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/static-dns/%s", c.apiPaths.ApiV2Path, site, id), nil, &respBody)
// client-side filtering is needed, because of lack of endpoint
records, err := c.listDNSRecord(ctx, site)
if err != nil {
return nil, err
}
if respBody.ID == "" {
return nil, ErrNotFound
for _, record := range records {
if record.ID == id {
return &record, nil
}
return &respBody, nil
}
return nil, ErrNotFound
}
func (c *client) DeleteDNSRecord(ctx context.Context, site, id string) error {
err := c.Delete(ctx, fmt.Sprintf("%s/site/%s/static-dns/%s", c.apiPaths.ApiV2Path, site, id), struct{}{}, nil)
if err != nil {
return err
}
return nil
return c.deleteDNSRecord(ctx, site, id)
}
func (c *client) CreateDNSRecord(ctx context.Context, site string, d *DNSRecord) (*DNSRecord, error) {
var respBody DNSRecord
err := c.Post(ctx, fmt.Sprintf("%s/site/%s/static-dns", c.apiPaths.ApiV2Path, site), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
return c.createDNSRecord(ctx, site, d)
}
func (c *client) UpdateDNSRecord(ctx context.Context, site string, d *DNSRecord) (*DNSRecord, error) {
var respBody DNSRecord
err := c.Put(ctx, fmt.Sprintf("%s/site/%s/static-dns/%s", c.apiPaths.ApiV2Path, site, d.ID), d, &respBody)
if err != nil {
return nil, err
}
// if len(respBody) != nil {
// return nil, ErrNotFound
// }
return &respBody, nil
return c.updateDNSRecord(ctx, site, d)
}