From 35e7b2c3cc6a49acabb766455e432165b7ff5a70 Mon Sep 17 00:00:00 2001 From: Mateusz Filipowicz Date: Tue, 18 Feb 2025 22:36:08 +0100 Subject: [PATCH] docs: add detailed usage documentation together with examples (#29) * feat: rename Pass to Password in ClientConfig * docs: add more detailed usage documentation together with examples * chore: update issue templates * docs: add codeowners, code of conduct and contributing docs * chore: add editorconfig * chore: apply linter --- .editorconfig | 67 +++++++ .github/CODEOWNERS | 1 + .github/CODE_OF_CONDUCT.md | 132 ++++++++++++++ .github/CONTRIBUTING.md | 199 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 89 ++++++++-- .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 52 ++++-- README.md | 15 +- docs/advanced_topics.md | 169 ++++++++++++++++++ docs/codegen.md | 78 ++++++++ docs/configuration.md | 205 ++++++++++++++++++++++ docs/getting_started.md | 116 ++++++++++++ docs/readme.md | 15 ++ docs/usage_examples.md | 55 ++++++ unifi/client.go | 18 +- unifi/unifi_test.go | 14 +- 16 files changed, 1177 insertions(+), 49 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 docs/advanced_topics.md create mode 100644 docs/codegen.md create mode 100644 docs/configuration.md create mode 100644 docs/getting_started.md create mode 100644 docs/readme.md create mode 100644 docs/usage_examples.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..156bec3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,67 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 200 +tab_width = 4 +# this affects things like 'throws' in method declaration in the new line etc +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_wrap_on_typing = false +ij_any_block_comment_add_space = true + +[*.conf] +ij_continuation_indent_size = 2 + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[.editorconfig] +max_line_length = 300 +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[BUCK] +indent_size = 4 +tab_width = 4 +ij_buck_keep_indents_on_empty_lines = false + +[{*.bash,*.sh,*.zsh}] +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false + +[{*.har,*.json}] +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +tab_width = 2 +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..16feb80 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @filipowm \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..073dd58 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..a46a91c --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,199 @@ + + +# Contributing to go-unifi + +First off, thanks for taking the time to contribute! ❤️ + +All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to +read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your +contributions. 🎉 + +> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy +> about: +> - Star the project +> - Tweet about it and share on different social media +> - Refer this project in your project's readme +> - Mention the project at local meetups and tell your friends/colleagues + + + +## Table of Contents + +- [I Have a Question](#i-have-a-question) +- [I Want To Contribute](#i-want-to-contribute) +- [Reporting Bugs](#reporting-bugs) +- [Suggesting Enhancements](#suggesting-enhancements) +- [Your First Code Contribution](#your-first-code-contribution) +- [Improving The Documentation](#improving-the-documentation) +- [Styleguides](#styleguides) +- [Commit Messages](#commit-messages) +- [Join The Project Team](#join-the-project-team) + +## I Have a Question + +> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/filipowm/go-unifi/blob/main/docs/readme.go). + +Before you ask a question, it is best to search for existing [Discussions](https://github.com/filipowm/go-unifi/discussions) or [issues](https://github.com/filipowm/go-unifi/issues) +that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. + +If you then still feel the need to ask a question and need clarification, we recommend the following: + +- Open a [Discussion](https://github.com/filipowm/go-unifi/discussions/new/choose). +- Provide as much context as you can about what you're running into. +- Provide project and platform versions depending on what seems relevant. + +We will then take care of the question as soon as possible. + +## I Want To Contribute + +### Reporting Bugs + + + +#### Before Submitting a Bug Report + +A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your +report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. + +- Make sure that you are using the latest version, especially importat in case of minor/patch versions that may contain fixes of your issue. +- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read + the [documentation](https://github.com/filipowm/go-unifi/blob/main/docs/readme.go). If you are looking for support, you might want to check [this section](#i-have-a-question)). +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in + the [bug tracker](https://github.com/filipowm/go-unifi/issues?q=label%3Abug). +- Also make sure to search the internet (including Stack Overflow, Reddit) to see if users outside of the GitHub community have discussed the issue. +- Collect information about the bug: + - Stack trace (Traceback), logs + - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) + - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. + - Possibly your input and the output + - Can you reliably reproduce the issue? And can you also reproduce it with older versions? + + + +#### How Do I Submit a Good Bug Report? + +> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email +> to <>. + + +We use GitHub issues to track bugs and errors. If you run into an issue with the project: + +- Open an [Issue](https://github.com/filipowm/go-unifi/issues/new?template=bug_report.md) +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug + reports you should isolate the problem and create a reduced test case. +- Provide the information you collected in the previous section. + +Once it's filed: + +- The project team will label the issue accordingly. +- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark + the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. +- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to + be [implemented by someone](#your-first-code-contribution). + + + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for go-unifi, **including completely new features and minor improvements to existing functionality**. Following these guidelines +will help maintainers and the community to understand your suggestion and find related suggestions. + + + +#### Before Submitting an Enhancement + +- Make sure that you are using the latest version. +- Read the [documentation](https://github.com/filipowm/go-unifi/blob/main/docs/readme.go) carefully and find out if the functionality is already covered, maybe by an individual configuration. +- Perform a [search](https://github.com/filipowm/go-unifi/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we + want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. + + + +#### How Do I Submit a Good Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](https://github.com/filipowm/go-unifi/issues). + +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. +- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. +- You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. You can + use [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and the + built-in [screen recorder in GNOME](https://help.gnome.org/users/gnome-help/stable/screen-shot-record.html.en) or [SimpleScreenRecorder](https://github.com/MaartenBaert/ssr) on + Linux. +- **Explain why this enhancement would be useful** to most go-unifi users. You may also want to point out the other projects that solved it better and which could serve as inspiration. + + + +### Your First Code Contribution + +1. Clone the repository + ```bash + git clone https://github.com/filipowm/go-unifi.git + ``` +2. Ensure you have the Go version installed as defined in `go.mod` file + ```bash + go version + ``` + + If you don't have the correct version, you can download it from the [official Go website](https://golang.org/dl/). + + If you have multiple versions of Go installed, you can use a tool like [gvm](https://github.com/moovweb/gvm) + +3. Ensure you have the latest version of the project + ```bash + git pull + ``` + +4. Create a new branch + ```bash + git checkout -b my-new-feature + ``` + +5. Make your changes + +6. Run the tests + ```bash + go test ./... + ``` + +7. Lint your code using [golangci-lint](https://golangci-lint.run/) + ```bash + golangci-lint run --fix + ``` + +8. If tests are passing and code is linted, you can create a commit and then pull request following styleguides below. + +### Improving The Documentation + +Any contribution to the documentation is highly appreciated. This includes both improvements to the existing documentation and adding new content. That's the most lightweight way to start +contributing to the project. + +## Styleguides + +### Commit Messages + +Please follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) standard for commit messages whenever possible. That's the best way to ensure that your commits are easy to +understand and follow the project's versioning. If you are not familiar with the conventional commits, you can also use the following format: + +``` +: +``` + +The `` must be one of the following: +- `feat`: A new feature +- `fix`: A bug fix +- `docs`: Documentation only changes +- `chore`: Changes to the build process, auxiliary tools, documentation, linting etc +- `refactor`: A code change that neither fixes a bug nor adds a feature + +The `` should be a short description of the change. Use the imperative, present tense. + +### Pull Requests + +Please follow conventional commits + +## Join The Project Team + +Create a discussion thread that you would like to join as a team member. New team members are always welcome. We are looking for people who are willing to contribute and help the project grow. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 82732f5..14ed7a4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,17 +1,78 @@ --- -name: Bug report +name: 🐞Bug report about: Create a report to help us improve -title: '' -labels: bug -assignees: '' +title: '[BUG] ' +labels: "bug" +assignees: 'filipowm' --- -## Expected Behavior -## Actual Behavior -## Steps to Reproduce the Problem - 1. - 1. - 1. -## Specifications - - Version: - - Platform: - - Subsystem: + +# **🐞 Bug Report** + +## **Describe the bug** + + +* + +--- + +### **Is this a regression?** + + + +--- + +### **To Reproduce** + + + + + +1. +2. +3. +4. + +--- + +### **Expected behaviour** + + +* + +--- + +### **Media prove** + + +--- + +### **Your environment** + + + +* OS: +* Node version: +* Npm version: +* Browser name and version: + +--- + +### **Additional context** + + +* + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3ba13e0..bd9dfe4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,2 @@ +--- blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index f452955..d617778 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,23 +1,45 @@ --- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: feature -assignees: '' +name: "🚀 Feature Request" +about: "Suggest an idea or possible new feature for this project." +title: "" +labels: "feature" +assignees: "filipowm" --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +# **🚀 Feature Request** -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +## **Is your feature request related to a problem? Please describe.** + -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +* -**Additional context** -Add any other context or screenshots about the feature request here. +--- -**Would you like to contribute?** -[ ] YES! +## **Describe the solution you'd like** + + +* + +--- + +## **Describe alternatives you've considered** + + +* + +--- + +### **Additional context** + + +* + + \ No newline at end of file diff --git a/README.md b/README.md index b73742f..2de9254 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # UniFi Go SDK +[![Docs](https://img.shields.io/badge/docs-reference-blue)](https://pkg.go.dev/github.com/filipowm/go-unifi) [![GoDoc](https://godoc.org/github.com/filipowm/go-unifi?status.svg)](https://godoc.org/github.com/filipowm/go-unifi) ![GitHub Release](https://img.shields.io/github/v/release/filipowm/go-unifi) ![GitHub branch check runs](https://img.shields.io/github/check-runs/filipowm/go-unifi/main) @@ -10,12 +11,18 @@ but can be used independently for any Go project requiring UniFi Network Control ## Features - Great UniFi Network Controller API coverage through automated code generation and manually added code for undocumented endpoints +- Easy to use client with support for API Key and username/password authentication - Generated data models from UniFi Controller API specifications - Daily automated updates to track the latest UniFi Controller versions -- Easy to use client with support for API Key and username/password authentication - Support for multiple UniFi Controller versions - Strong typing for all API models with Go structs +## Supported UniFi Controller Versions + +Any version after 5.12.35 is supported as of now. **Latest version: 9.0.114**. +The SDK is updated daily to track the latest UniFi Controller versions. +If you encounter any issues with the latest UniFi Controller version, please open an issue. + ## Code Generation The data models and basic REST methods are generated from JSON specifications found in the UniFi Controller JAR files. Those JSON specs show all fields and the associated regex/validation information. @@ -176,15 +183,15 @@ user, err := c.CreateUser(ctx, "site-name", &unifi.User{ ## Plans -- [ ] Increase API coverage, or modify code generation to rely on the official UniFi Controller API specifications +- [x] Increase API coverage, or modify code generation to rely on the official UniFi Controller API specifications - [x] Improve error handling (currently only basic error handling is implemented and error details are not propagated) - [x] Improve client code for better usability - [x] Support API Key authentication -- [ ] Generate client code for currently generated API structures, for use within or outside the Terraform provider +- [x] Generate client code for currently generated API structures, for use within or outside the Terraform provider - [ ] Increase test coverage - [x] Implement validation for fields and structures - [ ] Extend validators for more complex cases -- [ ] Add more documentation and examples +- [x] Add more documentation and examples - [ ] Bugfixing... ## Contributing diff --git a/docs/advanced_topics.md b/docs/advanced_topics.md new file mode 100644 index 0000000..0338cb2 --- /dev/null +++ b/docs/advanced_topics.md @@ -0,0 +1,169 @@ +# Advanced Topics + +This document delves into advanced aspects of using the UniFi Go SDK client, explaining how to customize the HTTP client, +use interceptors effectively, handle errors robustly, and extend validations. + +## Making a raw API Call using SDK Methods + +For endpoints that are not directly covered by a specialized client method, the UniFi Go SDK provides a set of helper methods for making requests to UniFi API. These methods simplify API interactions +by handling common tasks such as request construction, JSON marshaling of the request body, authentication, applying interceptors, error handling, and decoding the response: + +- **Do**: The core method that performs an HTTP request with a given method, API path, request body, and destination for decoding the response. It handles validation, URL construction, interceptors, + and error processing. +- **Get**: A convenience wrapper around **Do** that executes an HTTP GET request. +- **Post**: A convenience wrapper to perform an HTTP POST request. +- **Put**: Similar to Post, but for HTTP PUT requests. +- **Delete**: Performs an HTTP DELETE request. + +These methods are used internally by higher level functions, such as those in `unifi/device.generated.go` and `unifi/device.go`. For example, when creating a new device, the SDK calls `Post` to send +the device data to the UniFi Controller API, while `Get` is used to retrieve device information. + +Here is an example of using these methods for a custom API operation: + +```go +// Define a custom response structure +var respData struct { + Meta Meta `json:"meta"` + Data interface{} `json:"data"` +} + +// Use the Get method to fetch data from a custom endpoint +err := c.Get(ctx, "/api/customEndpoint", nil, &respData) +if err != nil { + log.Fatalf("Error performing GET request: %v", err) +} + +// For a POST request, define your request payload and response structure: +reqPayload := struct { + Field1 string `json:"field1"` + Field2 int `json:"field2"` +}{ + Field1: "value", + Field2: 123, +} + +var postResp struct { + Meta Meta `json:"meta"` + Data interface{} `json:"data"` +} + +err = c.Post(ctx, "/api/customPostEndpoint", reqPayload, &postResp) +if err != nil { + log.Fatalf("Error performing POST request: %v", err) +} +// do something with the response +``` + +These helper methods abstract away the boilerplate of manually constructing HTTP requests and processing responses, allowing you to focus on your application's logic while leveraging built-in +validation and error handling provided by the SDK. + +## Customizing the HTTP Client + +While the basic configuration allows simple modifications, you can fully customize the underlying HTTP client for more +control over connection settings, proxy configuration, TLS settings, and connection pooling. + +Example: + +```go +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + APIKey: "your-api-key", + HttpCustomizer: func (transport *http.Transport) error { + transport.MaxIdleConns = 20 + transport.IdleConnTimeout = 90 * time.Second + // Customize TLS settings or add a proxy configuration + return nil + }, +}) +if err != nil { + log.Fatalf("Error creating client: %v", err) +} +``` + +## Interceptors and Middleware + +Interceptors provide hooks into the request/response cycle and can be used for logging, metrics collection, or modifying +requests before they are sent. They implement the [ClientInterceptor](https://pkg.go.dev/github.com/filipowm/go-unifi/unifi#ClientInterceptor) interface. + +### Example: Advanced Logging Interceptor + +```go +// AdvancedLoggingInterceptor logs HTTP details and measures request time +type AdvancedLoggingInterceptor struct {} + +func (a *AdvancedLoggingInterceptor) InterceptRequest(req *http.Request) error { + log.Printf("[Request] %s %s", req.Method, req.URL) + req = req.WithContext(context.WithValue(req.Context(), "start", time.Now())) + return nil +} + +func (a *AdvancedLoggingInterceptor) InterceptResponse(resp *http.Response) error { + if start, ok := resp.Request.Context().Value("start").(time.Time); ok { + duration := time.Since(start) + log.Printf("[Response] %s %s in %v", resp.Request.Method, resp.Request.URL, duration) + } + return nil +} + +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + APIKey: "your-api-key", + Interceptors: []unifi.ClientInterceptor{&AdvancedLoggingInterceptor{}}, +}) +if err != nil { + log.Fatalf("Error creating client: %v", err) +} +``` + +## Debugging and Logging + +For troubleshooting, it might be useful to enable verbose logging. You can implement an interceptor to log additional +details like headers, body content, and timings. This can be enabled conditionally in your application's debug mode. + +## Advanced Error Handling + +The client supports both soft and hard validation modes. When using hard validation, errors returned are of type +`unifi.ValidationError` containing details about which fields failed validation. + +Example error handling snippet: + +```go +n := &unifi.Network{ + Name: "my-network", + Purpose: "invalid-purpose", + IPSubnet: "10.0.0.10/24", +} + +_, err = c.CreateNetwork(ctx, "default", n) +if err != nil { + var validationErr *unifi.ValidationError + if errors.As(err, &validationErr) { + // Process detailed validation errors + for field, errMsg := range validationErr.Root { + log.Printf("Validation error on %s: %s", field, errMsg) + } + } else { + log.Fatalf("Error creating network: %v", err) + } +} +``` + +## Extending Validations + +If the default validations do not meet your needs, you can implement custom validation logic. Extend the SDK's validation rules by wrapping or augmenting the existing ones. For example, +you can create a custom validator function and integrate it into your client initialization. Check [validation.go](../unifi/validation.go) for details. + +## Contributing and Extending the SDK + +The UniFi Go SDK is designed to be adaptable: + +- **Feature Requests:** If the SDK does not support a particular API endpoint, consider contributing by opening an issue or a pull request. +- **Custom Extensions:** You can fork the SDK and add custom methods or enhancements that fit your application needs. But I would greatly appreciate if you could contribute them back to the main repository. +- **Community Support:** Join our community discussions to share improvements and ask for guidance on advanced topics. + +For more details on contributing, see the [Contributing Guidelines](https://github.com/filipowm/go-unifi/blob/main/CONTRIBUTING.md). + +--- + +This document is intended for advanced users who need deeper control and customization over the UniFi client. +For most users, the basic configuration and usage examples should suffice. \ No newline at end of file diff --git a/docs/codegen.md b/docs/codegen.md new file mode 100644 index 0000000..374dba7 --- /dev/null +++ b/docs/codegen.md @@ -0,0 +1,78 @@ +# Code Generation for UniFi Go SDK + +The UniFi Go SDK uses a code generation process to create the client code, data models, and REST methods from JSON API +specifications. This documentation explains how to run the code generator, available parameters, and how to customize +the output using the `customizations.yml` file. + +## Overview + +The code generator reads the UniFi Controller API specifications (typically extracted from the UniFi Controller JAR +or JSON files) and generates Go code that includes data models, REST methods, and validation logic. The generation +process uses templates (such as `api.go.tmpl` or `client.go.tmpl`) and configuration files. + +## Running the Code Generator + +There are two common ways to run the code generator from the command line: + +### 1. Using `go generate` + +The recommended way to regenerate the client code is to use the `go generate` command. This command looks for special +comments in the source code (usually in a file such as `unifi/codegen.go`) and runs the generator as specified. + +Simply navigate to the root of the project and execute: + +```bash +go generate unifi/codegen.go +``` + +This command will: + +- Parse the API specifications +- Apply the templates to generate updated models and REST methods +- Overwrite the generated files with the latest code + +### 2. Running the Generator Directly with `go run` + +Alternatively, you can run the code generator directly using the `go run` command. This approach bypasses `go generate` +and directly executes the generator file. For example: + +```bash +go run ./codegen [OPTIONS] version +``` + +This command performs the same actions as the `go generate` command, but allows you to customize passed parameters as +described in the next section. + +## Options + +The code generator accepts the following flags, which control its behavior. These parameters can be set via command-line +flags when running the generator, either using `go run ./codegen` or through `go generate` (if configured accordingly): + +| Flag | Description | Default Value | +|---------------------|----------------------------------------------------------------------------|---------------| +| `-version-base-dir` | The base directory for version JSON files | `.` | +| `-output-dir` | The output directory of the generated Go code | `.` | +| `-download-only` | Only download and build the API structures JSON directory; do not generate | | +| `-debug` | Enable debug logging | | +| `-trace` | Enable trace logging (takes precedence over `-debug` flag) | | + +*Note:* These flags are defined in the main function of the code generator (see `codegen/main.go`). For further details, +consult the source code comments in that file. + +## Customizing Code Generation with customizations.yml + +The `customizations.yml` file is used to fine-tune the code generation process. It can be used to: + +- **Override Default Mappings**: Change how certain API fields or types are mapped to Go types. +- **Exclude or Modify Endpoints**: Disable generation for specific API endpoints or alter their behavior. + +### How to Use customizations.yml + +1. **Edit the File**: Open `codegen/customizations.yml` in your favorite editor and modify the settings according to your needs. + The file typically contains sections for field mappings, endpoint customizations, and additional template directives. + +2. **Run the Generator**: When you run the generator (using either `go generate` or `go run ./codegen`), + it will read the customizations and apply them to the generated code. + +3. **Review the Output**: Check the generated files to ensure that your customizations have been applied as expected. + Adjust the `customizations.yml` file and re-run the generator if further changes are needed. \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..87bb39e --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,205 @@ +# Client Configuration + +The UniFi Go SDK client is highly configurable to cater to different needs and environments. This document explains the various configuration options available in the client. + +## Authentication Methods + +The client supports two authentication protocols: + +### API Key Authentication + +Use this method for better security and dedicated access. Example: + +```go +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + APIKey: "your-api-key", +}) +if err != nil { + log.Fatalf("Error creating client: %v", err) +} +``` + +### Username/Password Authentication + +Alternatively, you can use username and password: + +```go +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + Username: "your-username", + Password: "your-password", +}) +if err != nil { + log.Fatalf("Error creating client: %v", err) +} +``` + +## Validation Modes + +The client has three modes of validation for the API models. The modes help to ensure that the data sent to the controller is correct. + +- **Soft Validation (`unifi.SoftValidation`)**: Logs warnings for invalid fields, but does not fail the request (default). +- **Hard Validation (`unifi.HardValidation`)**: Returns an error for invalid fields, preventing the request from being sent. +- **Disable Validation (`unifi.DisableValidation`)**: Disables all validations. + +Configure the validation mode as follows: + +```go +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + APIKey: "your-api-key", + ValidationMode: unifi.HardValidation, +}) +if err != nil { + log.Fatalf("Error creating client: %v", err) +} +``` + +## Customizing the HTTP Client + +You can provide your own HTTP client configuration using the `HttpCustomizer` callback. This is useful if you need to tweak connection settings like timeouts, idle connection settings, or TLS configurations: + +```go +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + APIKey: "your-api-key", + HttpCustomizer: func(transport *http.Transport) error { + transport.MaxIdleConns = 10 + // Customize TLS settings, proxy, etc. as needed + return nil + }, +}) +if err != nil { + log.Fatalf("Error creating client: %v", err) +} +``` + +## Using Interceptors + +Interceptors let you hook into the request and response flow. They can be used for logging, metrics, or modifying requests/responses. + +Implement the [ClientInterceptor](https://pkg.go.dev/github.com/filipowm/go-unifi/unifi#ClientInterceptor) interface: + +```go +// LoggingInterceptor logs each request and response +type LoggingInterceptor struct{} + +func (l *LoggingInterceptor) InterceptRequest(req *http.Request) error { + log.Printf("Request: %s %s", req.Method, req.URL) + return nil +} + +func (l *LoggingInterceptor) InterceptResponse(resp *http.Response) error { + log.Printf("Response status: %d", resp.StatusCode) + return nil +} + +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + APIKey: "your-api-key", + Interceptors: []unifi.ClientInterceptor{&LoggingInterceptor{}}, +}) +if err != nil { + log.Fatalf("Error creating client: %v", err) +} +``` + +This flexibility allows you to modify client behavior to suit your application's needs. + + +## Comprehensive Client Configuration Example + +The `ClientConfig` struct is the central configuration for initializing the UniFi client. It allows you to +fine-tune every aspect of the client's behavior such as the controller URL, authentication credentials, HTTP timeout, +SSL verification, custom HTTP transport settings, interceptors, error handling, concurrency locking, and request validation modes. + +Below is a full example demonstrating how to configure and use all available properties of `ClientConfig` when +initializing the client with `unifi.NewClient`: + +```go +package main + +import ( + "context" + "crypto/tls" + "fmt" + "log" + "net/http" + "time" + + "github.com/filipowm/go-unifi/unifi" +) + +// customTransportCustomizer customizes the HTTP transport, e.g., setting idle connection limits and TLS options. +func customTransportCustomizer(transport *http.Transport) error { + transport.MaxIdleConns = 50 + transport.IdleConnTimeout = 120 * time.Second + // Set a custom TLS configuration + transport.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + return nil +} + +// myErrorHandler implements a custom error handler for HTTP responses. +type myErrorHandler struct{} + +func (h *myErrorHandler) HandleError(resp *http.Response) error { + if resp.StatusCode >= 400 { + return fmt.Errorf("custom error: received status code %d", resp.StatusCode) + } + return nil +} + +// customInterceptor is a simple interceptor that adds a custom header to each request. +type customInterceptor struct{} + +func (ci *customInterceptor) InterceptRequest(req *http.Request) error { + req.Header.Set("X-Custom-Header", "CustomValue") + return nil +} + +func (ci *customInterceptor) InterceptResponse(resp *http.Response) error { + // Additional response processing can be added here if needed. + return nil +} + +func main() { + // Create a comprehensive client configuration. + config := &unifi.ClientConfig{ + URL: "https://unifi.example.com", // Base URL of the UniFi controller (without trailing '/api') + APIKey: "your-api-key", // API key for authentication. Alternatively, use User and Pass for user/password auth. + // User: "username", // Uncomment and provide if using user/password authentication + // Password: "password", // Uncomment and provide if using user/password authentication + Timeout: 30 * time.Second, // Maximum duration to wait for a response + VerifySSL: true, // Enable SSL certificate verification + Interceptors: []unifi.ClientInterceptor{&customInterceptor{}}, // Custom interceptors for request/response manipulation + HttpCustomizer: customTransportCustomizer, // Function to customize the underlying HTTP transport + UserAgent: "MyCustomAgent/1.0", // Custom User-Agent string + ErrorHandler: &myErrorHandler{}, // Custom error handler for processing HTTP response errors + UseLocking: true, // Enable internal locking for safe concurrent request processing + ValidationMode: unifi.SoftValidation, // Validation mode: SoftValidation, HardValidation, or DisableValidation + } + + // Initialize the UniFi client with the specified configuration. + client, err := unifi.NewClient(config) + if err != nil { + log.Fatalf("Error creating UniFi client: %v", err) + } + + // Example operation: Retrieve system information from the UniFi controller. + sysInfo, err := client.GetSystemInformation() + if err != nil { + log.Fatalf("Error retrieving system information: %v", err) + } + log.Printf("Connected to UniFi Controller version: %s", sysInfo.Version) + + // Further client operations can be performed using the 'client' instance. + // For example: creating networks, retrieving device information, etc. +} + +``` + +This example demonstrates how to utilize the full range of configuration options provided by `ClientConfig` to create +a highly customizable UniFi client. \ No newline at end of file diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..9ae9124 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,116 @@ +# Getting Started with UniFi Go SDK + +This guide will help you get started with the UniFi Go SDK client. It covers prerequisites, installation, and basic client initialization. +I highly recommend to use the latest version of UniFi Go SDK, as well as update your UniFi Controller to the latest version to ensure compatibility. + +## Prerequisites + +- Go 1.16 or later + +## Installation + +Install the UniFi Go SDK by running: + +```bash +go get github.com/filipowm/go-unifi +``` + +If you need to regenerate the client code from the API specifications, run: + +```bash +go generate unifi/codegen.go +``` + +## Initialization + +The client supports both API Key authentication and username/password authentication. Below are examples for both methods. +Unifi client support both username/password and API Key authentication. It is recommended to use API Key authentication +together with dedicated user restricted to local access only. + +**IMPORTANT:** API Key authentication is available only since UniFi Controller version 9.0.108 + +### Obtaining an API Key + +1. Open your Site in UniFi Site Manager +2. Click on Control Plane -> Admins & Users. +3. Select your Admin user. +4. Click Create API Key. +5. Add a name for your API Key. +6. Copy the key and store it securely, as it will only be displayed once. +7. Click Done to ensure the key is hashed and securely stored. +8. Use the API Key 🎉 + +### API Key Authentication + +```go +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + APIKey: "your-api-key", +}) +if err != nil { + log.Fatalf("Failed to create client: %v", err) +} +``` + +### Username/Password Authentication + +```go +c, err := unifi.NewClient(&unifi.ClientConfig{ + BaseURL: "https://unifi.localdomain", + Username: "your-username", + Password: "your-password", +}) +if err != nil { + log.Fatalf("Failed to create client: %v", err) +} +``` + +### Bare Client Initialization + +You can also use bare client, which creates a `unifi.Client` without initialization like logging in and getting system information. This can be useful in specific scenarios, when doing such initialization might be an uneeded overhead. To create it you can use the `NewBareClient` function provided in the SDK (see `unifi/client.go`). + +Example usage: + +```go +c, err := unifi.NewBareClient(&unifi.ClientConfig{ + BaseURL: "https://unifi-controller.example.com", + APIKey: "your-api-key", // or use Username/Password as needed + // Configuration for a bare client +}) +if err != nil { + log.Fatalf("Error creating bare client: %v", err) +} +err = c.Login() +if err != nil { + log.Fatalf("Error logging in: %v", err) +} +``` + +## Generating Client Code + +The UniFi Go SDK uses code generation to provide complete API coverage. To regenerate the client based on the latest specifications, run: + +```bash +go generate unifi/codegen.go +``` + +This will update the generated models and REST methods according to the current UniFi Controller API specifications. + + +## Usage + +Once instantiated, the Bare Client provides direct access to the generated API methods. You can perform operations without the extra layers of processing provided by interceptors or custom validations. +If you use a default site and didn't create any new ones, you can use the `default` site ID. + +**Example:** + +```go +networks, err := c.ListNetwork(ctx, "default") +if err != nil { + log.Fatalf("Error listing networks: %v", err) +} + +for _, network := range networks { + fmt.Printf("Network: %s\n", network.Name) +} +``` \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..e41c32e --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,15 @@ +# UniFi Client Documentation - Overview + +Welcome to the in-depth documentation for the UniFi Go SDK client. +This client provides a robust, strongly-typed interface to interact with the UniFi Network Controller API, +leveraging auto-generated models and manual enhancements for undocumented endpoints. + +## Table of Contents + +- [Overview](readme) +- [Getting Started](getting_started.md) +- [Client Configuration](configuration.md) +- [Usage Examples](usage_examples.md) +- [Advanced Topics](advanced_topics.md) + +Each section is designed to help you understand and effectively utilize the UniFi client in your Go projects. \ No newline at end of file diff --git a/docs/usage_examples.md b/docs/usage_examples.md new file mode 100644 index 0000000..a00cf7d --- /dev/null +++ b/docs/usage_examples.md @@ -0,0 +1,55 @@ +# Usage Examples + +This document demonstrates several common usage scenarios for the UniFi Go SDK client. + +## Listing Networks + +List all available networks in a given site: + +```go +networks, err := c.ListNetwork(ctx, "site-name") +if err != nil { + log.Fatalf("Error listing networks: %v", err) +} + +for _, network := range networks { + fmt.Printf("Network: %s\n", network.Name) +} +``` + +## Creating a User + +Create a new user assigned to the first available network: + +```go +// Assume networks have been retrieved as shown above +user, err := c.CreateUser(ctx, "site-name", &unifi.User{ + Name: "My Network User", + MAC: "00:00:00:00:00:00", + NetworkID: networks[0].ID, + IP: "10.0.21.37", +}) +if err != nil { + log.Fatalf("Error creating user: %v", err) +} + +fmt.Printf("Created user: %s\n", user.Name) +``` + +## Updating a Guest Access setting + +Update the guest access setting for a network: + +```go +setting := &unifi.SettingGuestAccess{ + PortalCustomizedBoxColor: "#ff0000", + PortalCustomizedSuccessText: "Welcome to the network!", + PortalCustomized: true, +} + +setting, err = c.UpdateSettingGuestAccess(ctx, "site-name", setting) +if err != nil { + log.Fatalf("Error updating guest access setting: %v", err) +} +// Use the updated setting +``` \ No newline at end of file diff --git a/unifi/client.go b/unifi/client.go index be2cf70..7014048 100644 --- a/unifi/client.go +++ b/unifi/client.go @@ -48,8 +48,8 @@ Fields: URL: The base URL of the UniFi controller. Must be a valid URL and should not include the `/api` suffix. APIKey: An API key used for authentication. Provide this if user/password credentials are not used. - User: The username for user/password authentication. Must be provided with Pass if APIKey is not used. - Pass: The password for user/password authentication. Must be provided with User if APIKey is not used. + User: The username for user/password authentication. Must be provided with Password if APIKey is not used. + Password: The password for user/password authentication. Must be provided with User if APIKey is not used. Timeout: The maximum duration to wait for responses; default is no timeout. VerifySSL: When false, disables SSL certificate verification. Interceptors: A slice of ClientInterceptor implementations that can modify requests and responses. @@ -61,9 +61,9 @@ Fields: */ type ClientConfig struct { URL string `validate:"required,http_url"` - APIKey string `validate:"required_without_all=User Pass"` - User string `validate:"excluded_with=APIKey,required_with=Pass"` - Pass string `validate:"excluded_with=APIKey,required_with=User"` + APIKey string `validate:"required_without_all=User Password"` + User string `validate:"excluded_with=APIKey,required_with=Password"` + Password string `validate:"excluded_with=APIKey,required_with=User"` Timeout time.Duration // How long to wait for replies, default: forever. VerifySSL bool Interceptors []ClientInterceptor @@ -99,14 +99,14 @@ func (a APIKeyCredentials) GetPass() string { return "" } // UserPassCredentials holds user/password authentication. type UserPassCredentials struct { - User string - Pass string + User string + Password string } func (u UserPassCredentials) IsAPIKey() bool { return false } func (u UserPassCredentials) GetAPIKey() string { return "" } func (u UserPassCredentials) GetUser() string { return u.User } -func (u UserPassCredentials) GetPass() string { return u.Pass } +func (u UserPassCredentials) GetPass() string { return u.Password } // client represents a UniFi client. type client struct { @@ -185,7 +185,7 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) { credentials = APIKeyCredentials{APIKey: config.APIKey} interceptors = append(interceptors, &APIKeyAuthInterceptor{apiKey: config.APIKey}) } else { - credentials = UserPassCredentials{User: config.User, Pass: config.Pass} + credentials = UserPassCredentials{User: config.User, Password: config.Password} interceptors = append(interceptors, &CSRFInterceptor{}) } if len(config.UserAgent) == 0 { diff --git a/unifi/unifi_test.go b/unifi/unifi_test.go index 015f826..d8cf83d 100644 --- a/unifi/unifi_test.go +++ b/unifi/unifi_test.go @@ -52,7 +52,7 @@ func TestNewBareClient(t *testing.T) { c, err := newBareClient(&ClientConfig{ URL: localUrl, User: "admin", - Pass: "password", + Password: "password", VerifySSL: false, }) require.Error(t, err) @@ -314,7 +314,7 @@ func TestUnifiIntegrationUserPassInjected(t *testing.T) { c, _ := newBareClient(&ClientConfig{ URL: srv.URL, User: "test-user", - Pass: "test-pass", + Password: "test-pass", Interceptors: interceptor.AsList(), }) c.apiPaths = &NewStyleAPI @@ -360,7 +360,7 @@ func TestCsrfHandling(t *testing.T) { c, _ := newBareClient(&ClientConfig{ URL: srv.URL, User: "test-user", - Pass: "test-pass", + Password: "test-pass", Interceptors: interceptor.AsList(), }) c.apiPaths = &NewStyleAPI @@ -425,10 +425,10 @@ func TestAuthConfigurationValidation(t *testing.T) { t.Parallel() // given cc := &ClientConfig{ - URL: testUrl, - User: tc.User, - Pass: tc.Pass, - APIKey: tc.APIKey, + URL: testUrl, + User: tc.User, + Password: tc.Pass, + APIKey: tc.APIKey, } // when