From aa188a6faa148017b711dfc4455e4355a9777849 Mon Sep 17 00:00:00 2001 From: Mateusz Filipowicz Date: Mon, 17 Feb 2025 15:39:54 +0100 Subject: [PATCH] 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 --- codegen/apiv2.go.tmpl | 119 ++++++++++++++++++++++++++++++++++ codegen/customizations.yml | 4 ++ codegen/customize.go | 7 ++ codegen/generator.go | 19 ++++-- codegen/resources.go | 24 ++++++- codegen/resources_test.go | 2 +- codegen/utils.go | 27 ++++++++ codegen/v2/APGroup.json | 4 ++ codegen/v2/DNSRecord.json | 10 +++ unifi/ap_group.generated.go | 100 ++++++++++++++++++++++++++++ unifi/ap_group.go | 92 ++++---------------------- unifi/client.generated.go | 38 +++++++++++ unifi/dns_record.generated.go | 115 ++++++++++++++++++++++++++++++++ unifi/dns_record.go | 96 ++++----------------------- 14 files changed, 482 insertions(+), 175 deletions(-) create mode 100644 codegen/apiv2.go.tmpl create mode 100644 codegen/v2/APGroup.json create mode 100644 codegen/v2/DNSRecord.json create mode 100644 unifi/ap_group.generated.go create mode 100644 unifi/dns_record.generated.go diff --git a/codegen/apiv2.go.tmpl b/codegen/apiv2.go.tmpl new file mode 100644 index 0000000..e8ec61e --- /dev/null +++ b/codegen/apiv2.go.tmpl @@ -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 +} diff --git a/codegen/customizations.yml b/codegen/customizations.yml index d2ca473..44468eb 100644 --- a/codegen/customizations.yml +++ b/codegen/customizations.yml @@ -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: diff --git a/codegen/customize.go b/codegen/customize.go index 552c370..3e32f43 100644 --- a/codegen/customize.go +++ b/codegen/customize.go @@ -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, "*") diff --git a/codegen/generator.go b/codegen/generator.go index 41a24b2..4423c9b 100644 --- a/codegen/generator.go +++ b/codegen/generator.go @@ -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) } - cb.AddResource(resource) customizer.ApplyToResource(resource) generators = append(generators, resource) } diff --git a/codegen/resources.go b/codegen/resources.go index d33f4cf..0c9a1d4 100644 --- a/codegen/resources.go +++ b/codegen/resources.go @@ -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": diff --git a/codegen/resources_test.go b/codegen/resources_test.go index fe34c25..e9fedb2 100644 --- a/codegen/resources_test.go +++ b/codegen/resources_test.go @@ -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) diff --git a/codegen/utils.go b/codegen/utils.go index cb85e3c..c6361db 100644 --- a/codegen/utils.go +++ b/codegen/utils.go @@ -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 +} diff --git a/codegen/v2/APGroup.json b/codegen/v2/APGroup.json new file mode 100644 index 0000000..0aaaa94 --- /dev/null +++ b/codegen/v2/APGroup.json @@ -0,0 +1,4 @@ +{ + "name": "", + "device_macs": [""] +} \ No newline at end of file diff --git a/codegen/v2/DNSRecord.json b/codegen/v2/DNSRecord.json new file mode 100644 index 0000000..2da9fa2 --- /dev/null +++ b/codegen/v2/DNSRecord.json @@ -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]?$|^" +} \ No newline at end of file diff --git a/unifi/ap_group.generated.go b/unifi/ap_group.generated.go new file mode 100644 index 0000000..0e4dfd9 --- /dev/null +++ b/unifi/ap_group.generated.go @@ -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 +} diff --git a/unifi/ap_group.go b/unifi/ap_group.go index 3931d04..084efc6 100644 --- a/unifi/ap_group.go +++ b/unifi/ap_group.go @@ -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) +} diff --git a/unifi/client.generated.go b/unifi/client.generated.go index 2bc4865..732b730 100644 --- a/unifi/client.generated.go +++ b/unifi/client.generated.go @@ -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. diff --git a/unifi/dns_record.generated.go b/unifi/dns_record.generated.go new file mode 100644 index 0000000..d8f7dcb --- /dev/null +++ b/unifi/dns_record.generated.go @@ -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 +} diff --git a/unifi/dns_record.go b/unifi/dns_record.go index 7c4c2e0..d89e9e3 100644 --- a/unifi/dns_record.go +++ b/unifi/dns_record.go @@ -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) }