From 4912e3558d29f941c1fa217150ef986a6f5856bf Mon Sep 17 00:00:00 2001 From: neiljpowell <52715665+neiljpowell@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:51:26 -0500 Subject: [PATCH 01/21] initial branch from standard template --- CHANGELOG.md | 17 ++++ CODEOWNERS | 2 + CODE_OF_CONDUCT.md | 35 ++++++++ CONTRIBUTING.md | 18 ++++ INSTALL.md | 3 + LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++++++ NOTICE | 16 ++++ README.md | 98 +++++++++++++++++++++- 8 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 INSTALL.md create mode 100644 LICENSE create mode 100644 NOTICE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8b366fd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +All notable changes to this project will be documented in this file. +We follow the [Semantic Versioning 2.0.0](http://semver.org/) format. + + +## x.y.z - YYYY-MM-DD + +### Added +- Lorem ipsum dolor sit amet + +### Deprecated +- Nothing. + +### Removed +- Nothing. + +### Fixed +- Nothing. \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..31328ba --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Global owners +* [ @Evernorth/team-name or each @username ] \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d248e31 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,35 @@ +### CODE OF CONDUCT +Evernorth Strategic Development, Inc. (“Evernorth”) welcomes contributions to our hosted open source projects. We have adopted this Code of Conduct (the “Code”) to facilitate your participation and to ensure our open source community remains a safe, harassment-free, and collaborative space. + +# Scope +This Code 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 e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +# Our Standards +We strive to make participation in our open source projects open and enjoyable for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex, gender identity, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual orientation. + +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 +The Evernorth Open Source Leadership Group (the “Leadership Group”) is responsible for clarifying and enforcing this Code and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. +The Leadership Group has 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, and will communicate reasons for moderation decisions when appropriate. + +# Reporting +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the Leadership Group at [opensource@evernorth.com](mailto:opensource@evernorth.com). All complaints will be reviewed and investigated promptly and fairly, while respecting the privacy and security of the reporter. We will determine if and what response is appropriate, and every matter may not result in a direct response from us. +Consequences for inappropriate behavior may include: (1) a warning with consequences for continued behavior; (2) restricted or suspended access to certain projects or other community members for a specific period of time; (3) temporary suspensions from participation in the community; and (4) permanent bans from participating in the community. Factors that may be considered when imposing various consequences may include, but are not limited to, the seriousness or nature of the harm caused, repeated violations of this Code, and the impact of the activity on our broader open source community. + +# Attribution +This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 2.0, available at [contributor-covenant.org/version/2/0/](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5f84045 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# Guidance on how to contribute + +Include information that will help a contributor understand expectations and be successful, such as the examples below. +> Want to see a good real world example? Take a look at the [CONTRIBUTING.adoc](https://github.com/spring-projects/spring-boot/blob/main/CONTRIBUTING.adoc) for spring-boot. + +* Where to ask Questions? Please make sure it is SEARCHABLE +* Who are the TC’s (Trusted Committers)? +* How to file an Issue +* How to file a PR +* Dependencies +* Style Guide / Coding standards +* Developer Environment +* Branching/ versioning +* Features +* Testing +* Roadmap +* Calendar +* Links to other documentation \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..a7aa523 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,3 @@ +# Installation instructions + +Detailed instructions on how to install, configure, and get the project running. If the instructions are minimal, they can reside in the Readme Installation section. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e8b0743 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © [ Year or Years Range of the Work ] Evernorth Strategic Development, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..f0d59a6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,16 @@ +[ Project Repo Name ] + +Copyright (c) [ Year or Years Range of the Work ] Evernorth Strategic Development, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/README.md b/README.md index f83bfad..0dd3452 100644 --- a/README.md +++ b/README.md @@ -1 +1,97 @@ -# http-problemdetails-go \ No newline at end of file +#### Github.com/Evernorth OpenSource Project Template Instructions + +1. Clone the Github.com/Evernorth project repo that was created for your approved Open Source contribution by an Org admin. +2. Copy all the *.MD from this repository to your new project. +3. Update the README, replacing the contents below as prescribed. +4. Update CODEOWNERS replacing "\[ @Evernorth/team-name or each @username \]" with the expected value. +5. Update the LICENSE and NOTICE replacing "\[ Year or Years Range of the Work \]" with the expected value. +6. Update NOTICE replacing "\[ Project Repo Name \]" with the expected value. +7. Delete these instructions and everything up to the _Project Title_ from the README. +8. Write some great software and tell people about it. + +> Keep the README fresh! It's the first thing people see and will make the initial impression. + +> Want to see a good real world example? Take a look at the [README](https://github.com/spring-projects/spring-boot/blob/main/README.adoc) for spring-boot. + +---- + +# Project Title + +**Description**: Put a meaningful, short, plain-language description of what +this project is trying to accomplish and why it matters. +Describe the problem(s) this project solves. +Describe how this software can improve the lives of its audience. + +Other things to include: + + - **Technology stack**: Indicate the technological nature of the software, including primary programming language(s) and whether the software is intended as standalone or as a module in a framework or other ecosystem. + - **Status**: Alpha, Beta, 1.1, etc. It's OK to write a sentence, too. The goal is to let interested people know where this project is at. This is also a good place to link to the [CHANGELOG](CHANGELOG.md). + - **Links to production or demo instances** + - Describe what sets this apart from related-projects. Linking to another doc or page is OK if this can't be expressed in a sentence or two. + + +**Screenshot**: If the software has visual components, consider placing a screenshot after the description; + + +## Dependencies + +Describe any dependencies that must be installed for this software to work. +This includes programming languages, databases or other storage mechanisms, build tools, frameworks, and so forth. +If specific versions of other software are required, or known not to work, call that out. + +## Building from Source + +Detailed instructions on how to build the project from source. Also note where to get pre-built distribution, if building is not required. + +## Installation + +Detailed instructions on how to install, configure, and get the project running. +This should be frequently tested to ensure reliability. Alternatively, link to +a separate [INSTALL](INSTALL.md) document. + +## Configuration + +If the software is configurable, describe it in detail, either here or in other documentation to which you link. + +## Usage + +Show users how to use the software. +Be specific. +Use appropriate formatting when showing code snippets. + +## How to test the software + +If the software includes automated tests, detail how to run those tests. + +## Known issues + +Document any known significant shortcomings with the software. + +## Getting help + +Instruct users how to get help with this software; this might include links to an issue tracker, wiki, mailing list, etc. + +**Example** + +If you have questions, concerns, bug reports, etc, please file an issue in this repository's Issue Tracker. + +## Getting involved + +This section should detail why people should get involved and describe key areas you are +currently focusing on; e.g., trying to get feedback on features, fixing certain bugs, building +important pieces, etc. + +General instructions on _how_ to contribute should be stated with a link to [CONTRIBUTING](CONTRIBUTING.md). + +## License +{ Project Title } is Open Source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). + +---- + +## Credits and references + +1. Projects that inspired you +2. Related projects +3. Books, papers, talks, or other sources that have meaningful impact or influence on this project + + From 2f531094545afe8038ca4bbac6a75aee758a80f9 Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:55:38 -0400 Subject: [PATCH 02/21] Initial commit: Add README.md with project overview and installation instructions --- CHANGELOG.md | 19 ++ CODEOWNERS | 2 + CODE_OF_CONDUCT.md | 35 ++++ CONTRIBUTING.md | 21 ++ LICENSE | 201 ++++++++++++++++++++ NOTICE | 15 ++ README.md | 78 +++++++- go.mod | 14 ++ go.sum | 12 ++ http/problemdetails/problemdetails.go | 81 ++++++++ http/problemdetails/problemdetails_test.go | 69 +++++++ internal/examples/problemdetails-example.go | 1 + 12 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 go.mod create mode 100644 go.sum create mode 100644 http/problemdetails/problemdetails.go create mode 100644 http/problemdetails/problemdetails_test.go create mode 100644 internal/examples/problemdetails-example.go diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d2e30c9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +All notable changes to this project will be documented in this file. +We follow the [Semantic Versioning 2.0.0](http://semver.org/) format. + + +## 1.0.0-2024-10-09 + +Initial Release + + diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..a8628d5 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Global owners +* @stevensefton \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..dc25142 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,35 @@ +# CODE OF CONDUCT +Evernorth Strategic Development, Inc. (“Evernorth”) welcomes contributions to our hosted open source projects. We have adopted this Code of Conduct (the “Code”) to facilitate your participation and to ensure our open source community remains a safe, harassment-free, and collaborative space. + +## Scope +This Code 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 e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Our Standards +We strive to make participation in our open source projects open and enjoyable for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex, gender identity, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual orientation. + +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 +The Evernorth Open Source Leadership Group (the “Leadership Group”) is responsible for clarifying and enforcing this Code and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. +The Leadership Group has 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, and will communicate reasons for moderation decisions when appropriate. + +## Reporting +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the Leadership Group at [opensource@evernorth.com](mailto:opensource@evernorth.com). All complaints will be reviewed and investigated promptly and fairly, while respecting the privacy and security of the reporter. We will determine if and what response is appropriate, and every matter may not result in a direct response from us. +Consequences for inappropriate behavior may include: (1) a warning with consequences for continued behavior; (2) restricted or suspended access to certain projects or other community members for a specific period of time; (3) temporary suspensions from participation in the community; and (4) permanent bans from participating in the community. Factors that may be considered when imposing various consequences may include, but are not limited to, the seriousness or nature of the harm caused, repeated violations of this Code, and the impact of the activity on our broader open source community. + +## Attribution +This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 2.0, available at [contributor-covenant.org/version/2/0/](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e358232 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contributing + +Thanks for your interest in contributing to this project! + +We welcome any kind of contribution including: + +- Documentation +- Examples +- New features and feature requests +- Bug fixes + +Please open Pull Requests for small changes. + +For larger changes please submit an issue with additional details. + +This issue should outline the following: + +- The use case that your changes are applicable to. +- Steps to reproduce the issue(s) if applicable. +- Detailed description of what your changes would entail. +- Alternative solutions or approaches if applicable. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..70db6d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © 2024 Evernorth Strategic Development, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..31d17db --- /dev/null +++ b/NOTICE @@ -0,0 +1,15 @@ +aws-lambda-go-adapter + +Copyright (c) 2024 Evernorth Strategic Development, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index f83bfad..43d4c32 100644 --- a/README.md +++ b/README.md @@ -1 +1,77 @@ -# http-problemdetails-go \ No newline at end of file +# http-problemdetails-go + +An implementation of [IETF RFC 9457 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html) which is a specification for a standard error structure for HTTP APIs. + + +## How it works + +## Installation +```go get -u github.com/Evernorth/http-problemdetails-go``` + +## Features + + +## Usage +### Creating a simple HTTP ProblemDetails +You can create a ProblemDetails instance by simply calling functions like `problemdetails.NewInternalServerError()`, `problemdetails.NewNotFound()` and `problemdetails.NewBadRequest()`. +```go +package employee + +import ( + "github.com/Evernorth/common-go/http/problemdetails" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "net/http" +) + +func getEmployee(httpRespWriter http.ResponseWriter, httpReq *http.Request) { + + err := doSomething() + if err != nil { + httpRespWriter.WriteHeader(http.StatusInternalServerError) + render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) + return + } + ... +} +``` +### Creating a complex HTTP ProblemDetails +You can easily override the default ProblemDetails title and detail fields and provide custom extension fields by using the `WithTitle`, `WithDetail` and `WithExtension` functions. +```go +package employee + +import ( + "github.com/Evernorth/common-go/http/problemdetails" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "net/http" +) + +func getEmployee(httpRespWriter http.ResponseWriter, httpReq *http.Request) { + + err := doSomething() + if err != nil { + httpRespWriter.WriteHeader(http.StatusInternalServerError) + render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError().WithTitle( + "KABOOM!!!").WithDetail("The unthinkable has occurred.").WithExtension( + "example1", "test").WithExtension( + "example2", "this could be a struct if you like")) + return + } + ... +} +``` + +## Dependencies +See the [go.mod](go.mod) file. + +## Support +If you have questions, concerns, bug reports, etc. See [CONTRIBUTING](CONTRIBUTING.md). + +## License +http-problemdetails-go is open source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). + +## Original Contributors +- Steve Sefton, Evernorth +- Shellee Stewart, Evernorth +- Neil Powell, Evernorth \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c581d7e --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/Evernorth/http-problemdetails-go + +go 1.23.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5697539 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http/problemdetails/problemdetails.go b/http/problemdetails/problemdetails.go new file mode 100644 index 0000000..8097fff --- /dev/null +++ b/http/problemdetails/problemdetails.go @@ -0,0 +1,81 @@ +package problemdetails + +import ( + "github.com/google/uuid" + "net/http" + "time" +) + +const ( + internalServerErrorType = "urn:problems:internal-server-error" + internalServerErrorTitle = "An unexpected error occurred." + badRequestType = "urn:problems:bad-request" + badRequestTitle = "The provided request could not be processed because it is invalid." + notFoundType = "urn:problems:not-found" + notFoundTitle = "The specified resource could not be found." + uuidUrnPrefix = "urn:uuid:" +) + +// ProblemDetails represents a HTTP Problem Details structure from https://www.rfc-editor.org/rfc/rfc9457.html +type ProblemDetails struct { + Type string `json:"type"` + Status int `json:"status"` + Title string `json:"title,omitempty"` + Detail string `json:"detail,omitempty"` + Instance string `json:"instance"` + Created string `json:"created"` + Extensions map[string]any `json:"extensions,omitempty"` +} + +// WithTitle sets a custom Title value on the ProblemDetails object. The ProblemDetails pointer is +// returned only for developer convenience. +func (p *ProblemDetails) WithTitle(title string) *ProblemDetails { + p.Title = title + return p +} + +// WithDetail sets a custom Detail value on the ProblemDetails object. The ProblemDetails pointer is +// returned only for developer convenience. +func (p *ProblemDetails) WithDetail(detail string) *ProblemDetails { + p.Detail = detail + return p +} + +// WithExtension adds a custom Extension value on the ProblemDetails object. The ProblemDetails pointer is +// returned only for developer convenience. +func (p *ProblemDetails) WithExtension(name string, value any) *ProblemDetails { + if p.Extensions == nil { + p.Extensions = make(map[string]any) + } + p.Extensions[name] = value + return p //&p recreates pointer? +} + +// New creates a new ProblemDetails instance. Do not use this function unless you are creating a +// custom ProblemDetails instance. Prefer using the functions that create standard ProblemDetails instances. +func New(typeUri string, status int) *ProblemDetails { + return &ProblemDetails{ + Type: typeUri, + Status: status, + Instance: uuidUrnPrefix + uuid.NewString(), + Created: time.Now().UTC().Format(time.RFC3339Nano), + } +} + +// NewInternalServerError creates a new ProblemDetails instance that represents an HTTP 500 Internal Server Error. +func NewInternalServerError() *ProblemDetails { + return New(internalServerErrorType, + http.StatusInternalServerError).WithTitle(internalServerErrorTitle) +} + +// NewBadRequest creates a new ProblemDetails instance that represents an HTTP 400 Bad Request. +func NewBadRequest() *ProblemDetails { + return New(badRequestType, + http.StatusBadRequest).WithTitle(badRequestTitle) +} + +// NewNotFound creates a new ProblemDetails instance that represents an HTTP 404 Not Found. +func NewNotFound() *ProblemDetails { + return New(notFoundType, + http.StatusNotFound).WithTitle(notFoundTitle) +} diff --git a/http/problemdetails/problemdetails_test.go b/http/problemdetails/problemdetails_test.go new file mode 100644 index 0000000..1470edc --- /dev/null +++ b/http/problemdetails/problemdetails_test.go @@ -0,0 +1,69 @@ +package problemdetails + +import ( + "github.com/stretchr/testify/assert" + "net/http" + "strings" + "testing" + "time" +) + +func TestNewInternalServerError(t *testing.T) { + problemDetails := NewInternalServerError() + assert.Equal(t, internalServerErrorType, problemDetails.Type) + assert.Equal(t, http.StatusInternalServerError, problemDetails.Status) + assert.Equal(t, internalServerErrorTitle, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNotFound(t *testing.T) { + problemDetails := NewNotFound() + assert.Equal(t, notFoundType, problemDetails.Type) + assert.Equal(t, http.StatusNotFound, problemDetails.Status) + assert.Equal(t, notFoundTitle, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewBadRequest(t *testing.T) { + problemDetails := NewBadRequest() + assert.Equal(t, badRequestType, problemDetails.Type) + assert.Equal(t, http.StatusBadRequest, problemDetails.Status) + assert.Equal(t, badRequestTitle, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewBadRequestCustomized(t *testing.T) { + title := "kaboom" + detail := "the unthinkable has occurred" + extKey1 := "key1" + extValue1 := "value1" + extKey2 := "key2" + extValue2 := "value2" + + problemDetails := NewBadRequest().WithTitle(title).WithDetail(detail).WithExtension( + extKey1, extValue1).WithExtension(extKey2, extValue2) + assert.Equal(t, badRequestType, problemDetails.Type) + assert.Equal(t, http.StatusBadRequest, problemDetails.Status) + assert.Equal(t, title, problemDetails.Title) + assert.Equal(t, detail, problemDetails.Detail) + assert.Equal(t, 2, len(problemDetails.Extensions)) + assert.Equal(t, extValue1, problemDetails.Extensions[extKey1]) + assert.Equal(t, extValue2, problemDetails.Extensions[extKey2]) + doCommonAssertions(t, problemDetails) +} + +func doCommonAssertions(t *testing.T, problemDetails *ProblemDetails) { + assert.Equal(t, true, strings.Contains(problemDetails.Instance, uuidUrnPrefix)) + + var createdTime time.Time + var err error + createdTime, err = time.Parse(time.RFC3339Nano, problemDetails.Created) + assert.Nil(t, err) + assert.NotNil(t, createdTime) +} diff --git a/internal/examples/problemdetails-example.go b/internal/examples/problemdetails-example.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/internal/examples/problemdetails-example.go @@ -0,0 +1 @@ +package main From 0f603d7b9d3751d6770c9a7ea902dab935ee473c Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Wed, 9 Oct 2024 18:50:29 -0400 Subject: [PATCH 03/21] Add functions for additional HTTP status codes in problemdetails package - Added NewContinue for HTTP 100 Continue - Added NewSwitchingProtocols for HTTP 101 Switching Protocols - Added NewOK for HTTP 200 OK - Added NewCreated for HTTP 201 Created - Added NewAccepted for HTTP 202 Accepted - Added NewNoContent for HTTP 204 No Content - Added NewMovedPermanently for HTTP 301 Moved Permanently - Added NewFound for HTTP 302 Found - Added NewNotModified for HTTP 304 Not Modified - Added NewUnauthorized for HTTP 401 Unauthorized - Added NewForbidden for HTTP 403 Forbidden - Added NewMethodNotAllowed for HTTP 405 Method Not Allowed - Added NewConflict for HTTP 409 Conflict - Added NewUnprocessableEntity for HTTP 422 Unprocessable Entity - Added NewNotImplemented for HTTP 501 Not Implemented - Added NewBadGateway for HTTP 502 Bad Gateway - Added NewServiceUnavailable for HTTP 503 Service Unavailable Add tests for all HTTP problem types - Added tests for HTTP 100 Continue - Added tests for HTTP 101 Switching Protocols - Added tests for HTTP 200 OK - Added tests for HTTP 201 Created - Added tests for HTTP 202 Accepted - Added tests for HTTP 204 No Content - Added tests for HTTP 301 Moved Permanently - Added tests for HTTP 302 Found - Added tests for HTTP 304 Not Modified - Added tests for HTTP 400 Bad Request - Added tests for HTTP 401 Unauthorized - Added tests for HTTP 403 Forbidden - Added tests for HTTP 404 Not Found - Added tests for HTTP 405 Method Not Allowed - Added tests for HTTP 409 Conflict - Added tests for HTTP 422 Unprocessable Entity - Added tests for HTTP 500 Internal Server Error - Added tests for HTTP 501 Not Implemented - Added tests for HTTP 502 Bad Gateway - Added tests for HTTP 503 Service Unavailable Add ProblemType definitions for various HTTP status codes - Added ProblemType for HTTP 100 Continue - Added ProblemType for HTTP 101 Switching Protocols - Added ProblemType for HTTP 200 OK - Added ProblemType for HTTP 201 Created - Added ProblemType for HTTP 202 Accepted - Added ProblemType for HTTP 203 Non-Authoritative Information - Added ProblemType for HTTP 204 No Content - Added ProblemType for HTTP 205 Reset Content - Added ProblemType for HTTP 206 Partial Content - Added ProblemType for HTTP 300 Multiple Choices - Added ProblemType for HTTP 301 Moved Permanently - Added ProblemType for HTTP 302 Found - Added ProblemType for HTTP 303 See Other - Added ProblemType for HTTP 304 Not Modified - Added ProblemType for HTTP 305 Use Proxy - Added ProblemType for HTTP 307 Temporary Redirect - Added ProblemType for HTTP 308 Permanent Redirect - Added ProblemType for HTTP 400 Bad Request - Added ProblemType for HTTP 401 Unauthorized - Added ProblemType for HTTP 403 Forbidden - Added ProblemType for HTTP 404 Not Found - Added ProblemType for HTTP 405 Method Not Allowed - Added ProblemType for HTTP 406 Not Acceptable - Added ProblemType for HTTP 409 Conflict - Added ProblemType for HTTP 410 Gone - Added ProblemType for HTTP 422 Unprocessable Entity - Added ProblemType for HTTP 500 Internal Server Error - Added ProblemType for HTTP 501 Not Implemented - Added ProblemType for HTTP 502 Bad Gateway - Added ProblemType for HTTP 503 Service Unavailable - Added ProblemType for HTTP 504 Gateway Timeout - Added ProblemType for HTTP 505 HTTP Version Not Supported --- http/problemdetails/problemdetails.go | 127 ++++++++++++-- http/problemdetails/problemdetails_test.go | 193 +++++++++++++++++++-- http/problemdetails/problemtype.go | 173 ++++++++++++++++++ 3 files changed, 465 insertions(+), 28 deletions(-) create mode 100644 http/problemdetails/problemtype.go diff --git a/http/problemdetails/problemdetails.go b/http/problemdetails/problemdetails.go index 8097fff..2154132 100644 --- a/http/problemdetails/problemdetails.go +++ b/http/problemdetails/problemdetails.go @@ -2,18 +2,11 @@ package problemdetails import ( "github.com/google/uuid" - "net/http" "time" ) const ( - internalServerErrorType = "urn:problems:internal-server-error" - internalServerErrorTitle = "An unexpected error occurred." - badRequestType = "urn:problems:bad-request" - badRequestTitle = "The provided request could not be processed because it is invalid." - notFoundType = "urn:problems:not-found" - notFoundTitle = "The specified resource could not be found." - uuidUrnPrefix = "urn:uuid:" + uuidUrnPrefix = "urn:uuid:" ) // ProblemDetails represents a HTTP Problem Details structure from https://www.rfc-editor.org/rfc/rfc9457.html @@ -62,20 +55,122 @@ func New(typeUri string, status int) *ProblemDetails { } } -// NewInternalServerError creates a new ProblemDetails instance that represents an HTTP 500 Internal Server Error. -func NewInternalServerError() *ProblemDetails { - return New(internalServerErrorType, - http.StatusInternalServerError).WithTitle(internalServerErrorTitle) +// NewContinue creates a new ProblemDetails instance that represents an HTTP 100 Continue. +func NewContinue() *ProblemDetails { + return New(Continue.Type, + Continue.Code).WithTitle(Continue.Title) +} + +// NewSwitchingProtocols creates a new ProblemDetails instance that represents an HTTP 101 Switching Protocols. +func NewSwitchingProtocols() *ProblemDetails { + return New(SwitchingProtocols.Type, + SwitchingProtocols.Code).WithTitle(SwitchingProtocols.Title) +} + +// NewOK creates a new ProblemDetails instance that represents an HTTP 200 OK. +func NewOK() *ProblemDetails { + return New(OK.Type, + OK.Code).WithTitle(OK.Title) +} + +// NewCreated creates a new ProblemDetails instance that represents an HTTP 201 Created. +func NewCreated() *ProblemDetails { + return New(Created.Type, + Created.Code).WithTitle(Created.Title) +} + +// NewAccepted creates a new ProblemDetails instance that represents an HTTP 202 Accepted. +func NewAccepted() *ProblemDetails { + return New(Accepted.Type, + Accepted.Code).WithTitle(Accepted.Title) +} + +// NewNoContent creates a new ProblemDetails instance that represents an HTTP 204 No Content. +func NewNoContent() *ProblemDetails { + return New(NoContent.Type, + NoContent.Code).WithTitle(NoContent.Title) +} + +// NewMovedPermanently creates a new ProblemDetails instance that represents an HTTP 301 Moved Permanently. +func NewMovedPermanently() *ProblemDetails { + return New(MovedPermanently.Type, + MovedPermanently.Code).WithTitle(MovedPermanently.Title) +} + +// NewFound creates a new ProblemDetails instance that represents an HTTP 302 Found. +func NewFound() *ProblemDetails { + return New(Found.Type, + Found.Code).WithTitle(Found.Title) +} + +// NewNotModified creates a new ProblemDetails instance that represents an HTTP 304 Not Modified. +func NewNotModified() *ProblemDetails { + return New(NotModified.Type, + NotModified.Code).WithTitle(NotModified.Title) } // NewBadRequest creates a new ProblemDetails instance that represents an HTTP 400 Bad Request. func NewBadRequest() *ProblemDetails { - return New(badRequestType, - http.StatusBadRequest).WithTitle(badRequestTitle) + return New(BadRequest.Type, + BadRequest.Code).WithTitle(BadRequest.Title) } // NewNotFound creates a new ProblemDetails instance that represents an HTTP 404 Not Found. func NewNotFound() *ProblemDetails { - return New(notFoundType, - http.StatusNotFound).WithTitle(notFoundTitle) + return New(NotFound.Type, + NotFound.Code).WithTitle(NotFound.Title) +} + +// NewUnauthorized creates a new ProblemDetails instance that represents an HTTP 401 Unauthorized. +func NewUnauthorized() *ProblemDetails { + return New(Unauthorized.Type, + Unauthorized.Code).WithTitle(Unauthorized.Title) +} + +// NewForbidden creates a new ProblemDetails instance that represents an HTTP 403 Forbidden. +func NewForbidden() *ProblemDetails { + return New(Forbidden.Type, + Forbidden.Code).WithTitle(Forbidden.Title) +} + +// NewMethodNotAllowed creates a new ProblemDetails instance that represents an HTTP 405 Method Not Allowed. +func NewMethodNotAllowed() *ProblemDetails { + return New(MethodNotAllowed.Type, + MethodNotAllowed.Code).WithTitle(MethodNotAllowed.Title) +} + +// NewConflict creates a new ProblemDetails instance that represents an HTTP 409 Conflict. +func NewConflict() *ProblemDetails { + return New(Conflict.Type, + Conflict.Code).WithTitle(Conflict.Title) +} + +// NewUnprocessableEntity creates a new ProblemDetails instance that represents an HTTP 422 Unprocessable Entity. +func NewUnprocessableEntity() *ProblemDetails { + return New(UnprocessableEntity.Type, + UnprocessableEntity.Code).WithTitle(UnprocessableEntity.Title) +} + +// NewInternalServerError creates a new ProblemDetails instance that represents an HTTP 500 Internal Server Error. +func NewInternalServerError() *ProblemDetails { + return New(InternalServerError.Type, + InternalServerError.Code).WithTitle(InternalServerError.Title) +} + +// NewNotImplemented creates a new ProblemDetails instance that represents an HTTP 501 Not Implemented. +func NewNotImplemented() *ProblemDetails { + return New(NotImplemented.Type, + NotImplemented.Code).WithTitle(NotImplemented.Title) +} + +// NewBadGateway creates a new ProblemDetails instance that represents an HTTP 502 Bad Gateway. +func NewBadGateway() *ProblemDetails { + return New(BadGateway.Type, + BadGateway.Code).WithTitle(BadGateway.Title) +} + +// NewServiceUnavailable creates a new ProblemDetails instance that represents an HTTP 503 Service Unavailable. +func NewServiceUnavailable() *ProblemDetails { + return New(ServiceUnavailable.Type, + ServiceUnavailable.Code).WithTitle(ServiceUnavailable.Title) } diff --git a/http/problemdetails/problemdetails_test.go b/http/problemdetails/problemdetails_test.go index 1470edc..301e1d6 100644 --- a/http/problemdetails/problemdetails_test.go +++ b/http/problemdetails/problemdetails_test.go @@ -2,7 +2,6 @@ package problemdetails import ( "github.com/stretchr/testify/assert" - "net/http" "strings" "testing" "time" @@ -10,9 +9,9 @@ import ( func TestNewInternalServerError(t *testing.T) { problemDetails := NewInternalServerError() - assert.Equal(t, internalServerErrorType, problemDetails.Type) - assert.Equal(t, http.StatusInternalServerError, problemDetails.Status) - assert.Equal(t, internalServerErrorTitle, problemDetails.Title) + assert.Equal(t, InternalServerError.Type, problemDetails.Type) + assert.Equal(t, InternalServerError.Code, problemDetails.Status) + assert.Equal(t, InternalServerError.Title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -20,9 +19,9 @@ func TestNewInternalServerError(t *testing.T) { func TestNewNotFound(t *testing.T) { problemDetails := NewNotFound() - assert.Equal(t, notFoundType, problemDetails.Type) - assert.Equal(t, http.StatusNotFound, problemDetails.Status) - assert.Equal(t, notFoundTitle, problemDetails.Title) + assert.Equal(t, NotFound.Type, problemDetails.Type) + assert.Equal(t, NotFound.Code, problemDetails.Status) + assert.Equal(t, NotFound.Title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -30,9 +29,179 @@ func TestNewNotFound(t *testing.T) { func TestNewBadRequest(t *testing.T) { problemDetails := NewBadRequest() - assert.Equal(t, badRequestType, problemDetails.Type) - assert.Equal(t, http.StatusBadRequest, problemDetails.Status) - assert.Equal(t, badRequestTitle, problemDetails.Title) + assert.Equal(t, BadRequest.Type, problemDetails.Type) + assert.Equal(t, BadRequest.Code, problemDetails.Status) + assert.Equal(t, BadRequest.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewContinue(t *testing.T) { + problemDetails := NewContinue() + assert.Equal(t, Continue.Type, problemDetails.Type) + assert.Equal(t, Continue.Code, problemDetails.Status) + assert.Equal(t, Continue.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewSwitchingProtocols(t *testing.T) { + problemDetails := NewSwitchingProtocols() + assert.Equal(t, SwitchingProtocols.Type, problemDetails.Type) + assert.Equal(t, SwitchingProtocols.Code, problemDetails.Status) + assert.Equal(t, SwitchingProtocols.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewOK(t *testing.T) { + problemDetails := NewOK() + assert.Equal(t, OK.Type, problemDetails.Type) + assert.Equal(t, OK.Code, problemDetails.Status) + assert.Equal(t, OK.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewCreated(t *testing.T) { + problemDetails := NewCreated() + assert.Equal(t, Created.Type, problemDetails.Type) + assert.Equal(t, Created.Code, problemDetails.Status) + assert.Equal(t, Created.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewAccepted(t *testing.T) { + problemDetails := NewAccepted() + assert.Equal(t, Accepted.Type, problemDetails.Type) + assert.Equal(t, Accepted.Code, problemDetails.Status) + assert.Equal(t, Accepted.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNoContent(t *testing.T) { + problemDetails := NewNoContent() + assert.Equal(t, NoContent.Type, problemDetails.Type) + assert.Equal(t, NoContent.Code, problemDetails.Status) + assert.Equal(t, NoContent.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewMovedPermanently(t *testing.T) { + problemDetails := NewMovedPermanently() + assert.Equal(t, MovedPermanently.Type, problemDetails.Type) + assert.Equal(t, MovedPermanently.Code, problemDetails.Status) + assert.Equal(t, MovedPermanently.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewFound(t *testing.T) { + problemDetails := NewFound() + assert.Equal(t, Found.Type, problemDetails.Type) + assert.Equal(t, Found.Code, problemDetails.Status) + assert.Equal(t, Found.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNotModified(t *testing.T) { + problemDetails := NewNotModified() + assert.Equal(t, NotModified.Type, problemDetails.Type) + assert.Equal(t, NotModified.Code, problemDetails.Status) + assert.Equal(t, NotModified.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewUnauthorized(t *testing.T) { + problemDetails := NewUnauthorized() + assert.Equal(t, Unauthorized.Type, problemDetails.Type) + assert.Equal(t, Unauthorized.Code, problemDetails.Status) + assert.Equal(t, Unauthorized.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewForbidden(t *testing.T) { + problemDetails := NewForbidden() + assert.Equal(t, Forbidden.Type, problemDetails.Type) + assert.Equal(t, Forbidden.Code, problemDetails.Status) + assert.Equal(t, Forbidden.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewMethodNotAllowed(t *testing.T) { + problemDetails := NewMethodNotAllowed() + assert.Equal(t, MethodNotAllowed.Type, problemDetails.Type) + assert.Equal(t, MethodNotAllowed.Code, problemDetails.Status) + assert.Equal(t, MethodNotAllowed.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewConflict(t *testing.T) { + problemDetails := NewConflict() + assert.Equal(t, Conflict.Type, problemDetails.Type) + assert.Equal(t, Conflict.Code, problemDetails.Status) + assert.Equal(t, Conflict.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewUnprocessableEntity(t *testing.T) { + problemDetails := NewUnprocessableEntity() + assert.Equal(t, UnprocessableEntity.Type, problemDetails.Type) + assert.Equal(t, UnprocessableEntity.Code, problemDetails.Status) + assert.Equal(t, UnprocessableEntity.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNotImplemented(t *testing.T) { + problemDetails := NewNotImplemented() + assert.Equal(t, NotImplemented.Type, problemDetails.Type) + assert.Equal(t, NotImplemented.Code, problemDetails.Status) + assert.Equal(t, NotImplemented.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewBadGateway(t *testing.T) { + problemDetails := NewBadGateway() + assert.Equal(t, BadGateway.Type, problemDetails.Type) + assert.Equal(t, BadGateway.Code, problemDetails.Status) + assert.Equal(t, BadGateway.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewServiceUnavailable(t *testing.T) { + problemDetails := NewServiceUnavailable() + assert.Equal(t, ServiceUnavailable.Type, problemDetails.Type) + assert.Equal(t, ServiceUnavailable.Code, problemDetails.Status) + assert.Equal(t, ServiceUnavailable.Title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -48,8 +217,8 @@ func TestNewBadRequestCustomized(t *testing.T) { problemDetails := NewBadRequest().WithTitle(title).WithDetail(detail).WithExtension( extKey1, extValue1).WithExtension(extKey2, extValue2) - assert.Equal(t, badRequestType, problemDetails.Type) - assert.Equal(t, http.StatusBadRequest, problemDetails.Status) + assert.Equal(t, BadRequest.Type, problemDetails.Type) + assert.Equal(t, BadRequest.Code, problemDetails.Status) assert.Equal(t, title, problemDetails.Title) assert.Equal(t, detail, problemDetails.Detail) assert.Equal(t, 2, len(problemDetails.Extensions)) diff --git a/http/problemdetails/problemtype.go b/http/problemdetails/problemtype.go new file mode 100644 index 0000000..1bf8160 --- /dev/null +++ b/http/problemdetails/problemtype.go @@ -0,0 +1,173 @@ +package problemdetails + +import "net/http" + +// ProblemType represents a problem type as defined in RFC 9457. This struct encapsulates the details of different types of HTTP problems, such as their type URIs, status codes, and titles. This struct would help in organizing and reusing these details across the codebase. +type ProblemType struct { + Code int + Type string + Title string +} + +var ( + Continue = ProblemType{ + Code: http.StatusContinue, + Type: "urn:problems:continue", + Title: "The server has received the request headers and the client should proceed to send the request body.", + } + SwitchingProtocols = ProblemType{ + Code: http.StatusSwitchingProtocols, + Type: "urn:problems:switching-protocols", + Title: "The server is switching protocols according to the Upgrade header.", + } + OK = ProblemType{ + Code: http.StatusOK, + Type: "urn:problems:ok", + Title: "The request has succeeded.", + } + Created = ProblemType{ + Code: http.StatusCreated, + Type: "urn:problems:created", + Title: "The request has been fulfilled and has resulted in one or more new resources being created.", + } + Accepted = ProblemType{ + Code: http.StatusAccepted, + Type: "urn:problems:accepted", + Title: "The request has been accepted for processing, but the processing has not been completed.", + } + NonAuthoritativeInfo = ProblemType{ + Code: http.StatusNonAuthoritativeInfo, + Type: "urn:problems:non-authoritative-info", + Title: "The request was successful but the enclosed payload has been modified from that of the origin server's 200 (OK) response by a transforming proxy.", + } + NoContent = ProblemType{ + Code: http.StatusNoContent, + Type: "urn:problems:no-content", + Title: "The server has successfully fulfilled the request and that there is no additional content to send in the response payload body.", + } + ResetContent = ProblemType{ + Code: http.StatusResetContent, + Type: "urn:problems:reset-content", + Title: "The server has fulfilled the request and desires that the user agent reset the 'document view' which caused the request to be sent.", + } + PartialContent = ProblemType{ + Code: http.StatusPartialContent, + Type: "urn:problems:partial-content", + Title: "The server is successfully fulfilling a range request for the target resource by transferring one or more parts of the selected representation.", + } + MultipleChoices = ProblemType{ + Code: http.StatusMultipleChoices, + Type: "urn:problems:multiple-choices", + Title: "The request has more than one possible response.", + } + MovedPermanently = ProblemType{ + Code: http.StatusMovedPermanently, + Type: "urn:problems:moved-permanently", + Title: "The requested resource has been assigned a new permanent URI.", + } + Found = ProblemType{ + Code: http.StatusFound, + Type: "urn:problems:found", + Title: "The requested resource resides temporarily under a different URI.", + } + SeeOther = ProblemType{ + Code: http.StatusSeeOther, + Type: "urn:problems:see-other", + Title: "The response to the request can be found under another URI using a GET method.", + } + NotModified = ProblemType{ + Code: http.StatusNotModified, + Type: "urn:problems:not-modified", + Title: "The resource has not been modified since the version specified by the request headers.", + } + UseProxy = ProblemType{ + Code: http.StatusUseProxy, + Type: "urn:problems:use-proxy", + Title: "The requested resource is available only through a proxy.", + } + TemporaryRedirect = ProblemType{ + Code: http.StatusTemporaryRedirect, + Type: "urn:problems:temporary-redirect", + Title: "The request should be repeated with another URI, but future requests can still use the original URI.", + } + PermanentRedirect = ProblemType{ + Code: http.StatusPermanentRedirect, + Type: "urn:problems:permanent-redirect", + Title: "The request and all future requests should be repeated using another URI.", + } + BadRequest = ProblemType{ + Code: http.StatusBadRequest, + Type: "urn:problems:bad-request", + Title: "The provided request could not be processed because it is invalid.", + } + Unauthorized = ProblemType{ + Code: http.StatusUnauthorized, + Type: "urn:problems:unauthorized", + Title: "The request requires user authentication.", + } + Forbidden = ProblemType{ + Code: http.StatusForbidden, + Type: "urn:problems:forbidden", + Title: "The server understood the request, but refuses to authorize it.", + } + NotFound = ProblemType{ + Code: http.StatusNotFound, + Type: "urn:problems:not-found", + Title: "The specified resource could not be found.", + } + MethodNotAllowed = ProblemType{ + Code: http.StatusMethodNotAllowed, + Type: "urn:problems:method-not-allowed", + Title: "The method specified in the request is not allowed for the resource.", + } + NotAcceptable = ProblemType{ + Code: http.StatusNotAcceptable, + Type: "urn:problems:not-acceptable", + Title: "The resource is not capable of generating content acceptable to the Accept headers.", + } + Conflict = ProblemType{ + Code: http.StatusConflict, + Type: "urn:problems:conflict", + Title: "The request could not be completed due to a conflict with the current state of the resource.", + } + Gone = ProblemType{ + Code: http.StatusGone, + Type: "urn:problems:gone", + Title: "The resource requested is no longer available and will not be available again.", + } + UnprocessableEntity = ProblemType{ + Code: http.StatusUnprocessableEntity, + Type: "urn:problems:unprocessable-entity", + Title: "The server understands the content type of the request entity, but was unable to process the contained instructions.", + } + InternalServerError = ProblemType{ + Code: http.StatusInternalServerError, + Type: "urn:problems:internal-server-error", + Title: "The server has encountered a situation it doesn't know how to handle.", + } + NotImplemented = ProblemType{ + Code: http.StatusNotImplemented, + Type: "urn:problems:not-implemented", + Title: "The request method is not supported by the server and cannot be handled.", + } + BadGateway = ProblemType{ + Code: http.StatusBadGateway, + Type: "urn:problems:bad-gateway", + Title: "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.", + } + ServiceUnavailable = ProblemType{ + Code: http.StatusServiceUnavailable, + Type: "urn:problems:service-unavailable", + Title: "The server is not ready to handle the request, often due to maintenance or overload.", + } + GatewayTimeout = ProblemType{ + Code: http.StatusGatewayTimeout, + Type: "urn:problems:gateway-timeout", + Title: "The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.", + } + HTTPVersionNotSupported = ProblemType{ + Code: http.StatusHTTPVersionNotSupported, + Type: "urn:problems:http-version-not-supported", + Title: "The server does not support the HTTP protocol version used in the request.", + } +) From 9ed1f44ca26e5ae5129553c5ad5819ca04f82def Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Wed, 9 Oct 2024 21:43:00 -0400 Subject: [PATCH 04/21] - Defined `ProblemType` struct and various HTTP problem types in `problemtype.go`. - Updated `ProblemDetails` methods for creating instances in `problemdetails.go`. - Added unit tests for `ProblemDetails` creation functions in `problemdetails_test.go`. --- http/problemdetails/problemdetails.go | 224 +++++++++- http/problemdetails/problemdetails_test.go | 371 +++++++++++++-- http/problemdetails/problemtype.go | 472 +++++++++++++++----- internal/examples/problemdetails-example.go | 24 + 4 files changed, 955 insertions(+), 136 deletions(-) diff --git a/http/problemdetails/problemdetails.go b/http/problemdetails/problemdetails.go index 2154132..595147e 100644 --- a/http/problemdetails/problemdetails.go +++ b/http/problemdetails/problemdetails.go @@ -67,6 +67,12 @@ func NewSwitchingProtocols() *ProblemDetails { SwitchingProtocols.Code).WithTitle(SwitchingProtocols.Title) } +// NewEarlyHints creates a new ProblemDetails instance that represents an HTTP 103 Early Hints. +func NewEarlyHints() *ProblemDetails { + return New(EarlyHints.Type, + EarlyHints.Code).WithTitle(EarlyHints.Title) +} + // NewOK creates a new ProblemDetails instance that represents an HTTP 200 OK. func NewOK() *ProblemDetails { return New(OK.Type, @@ -85,12 +91,42 @@ func NewAccepted() *ProblemDetails { Accepted.Code).WithTitle(Accepted.Title) } +// NewNonAuthoritativeInfo creates a new ProblemDetails instance that represents an HTTP 203 Non-Authoritative Information. +func NewNonAuthoritativeInfo() *ProblemDetails { + return New(NonAuthoritativeInfo.Type, + NonAuthoritativeInfo.Code).WithTitle(NonAuthoritativeInfo.Title) +} + // NewNoContent creates a new ProblemDetails instance that represents an HTTP 204 No Content. func NewNoContent() *ProblemDetails { return New(NoContent.Type, NoContent.Code).WithTitle(NoContent.Title) } +// NewResetContent creates a new ProblemDetails instance that represents an HTTP 205 Reset Content. +func NewResetContent() *ProblemDetails { + return New(ResetContent.Type, + ResetContent.Code).WithTitle(ResetContent.Title) +} + +// NewPartialContent creates a new ProblemDetails instance that represents an HTTP 206 Partial Content. +func NewPartialContent() *ProblemDetails { + return New(PartialContent.Type, + PartialContent.Code).WithTitle(PartialContent.Title) +} + +// NewIMUsed creates a new ProblemDetails instance that represents an HTTP 226 IM Used. +func NewIMUsed() *ProblemDetails { + return New(IMUsed.Type, + IMUsed.Code).WithTitle(IMUsed.Title) +} + +// NewMultipleChoices creates a new ProblemDetails instance that represents an HTTP 300 Multiple Choices. +func NewMultipleChoices() *ProblemDetails { + return New(MultipleChoices.Type, + MultipleChoices.Code).WithTitle(MultipleChoices.Title) +} + // NewMovedPermanently creates a new ProblemDetails instance that represents an HTTP 301 Moved Permanently. func NewMovedPermanently() *ProblemDetails { return New(MovedPermanently.Type, @@ -103,52 +139,178 @@ func NewFound() *ProblemDetails { Found.Code).WithTitle(Found.Title) } +// NewSeeOther creates a new ProblemDetails instance that represents an HTTP 303 See Other. +func NewSeeOther() *ProblemDetails { + return New(SeeOther.Type, + SeeOther.Code).WithTitle(SeeOther.Title) +} + // NewNotModified creates a new ProblemDetails instance that represents an HTTP 304 Not Modified. func NewNotModified() *ProblemDetails { return New(NotModified.Type, NotModified.Code).WithTitle(NotModified.Title) } +// NewUseProxy creates a new ProblemDetails instance that represents an HTTP 305 Use Proxy. +func NewUseProxy() *ProblemDetails { + return New(UseProxy.Type, + UseProxy.Code).WithTitle(UseProxy.Title) +} + +// NewTemporaryRedirect creates a new ProblemDetails instance that represents an HTTP 307 Temporary Redirect. +func NewTemporaryRedirect() *ProblemDetails { + return New(TemporaryRedirect.Type, + TemporaryRedirect.Code).WithTitle(TemporaryRedirect.Title) +} + +// NewPermanentRedirect creates a new ProblemDetails instance that represents an HTTP 308 Permanent Redirect. +func NewPermanentRedirect() *ProblemDetails { + return New(PermanentRedirect.Type, + PermanentRedirect.Code).WithTitle(PermanentRedirect.Title) +} + // NewBadRequest creates a new ProblemDetails instance that represents an HTTP 400 Bad Request. func NewBadRequest() *ProblemDetails { return New(BadRequest.Type, BadRequest.Code).WithTitle(BadRequest.Title) } -// NewNotFound creates a new ProblemDetails instance that represents an HTTP 404 Not Found. -func NewNotFound() *ProblemDetails { - return New(NotFound.Type, - NotFound.Code).WithTitle(NotFound.Title) -} - // NewUnauthorized creates a new ProblemDetails instance that represents an HTTP 401 Unauthorized. func NewUnauthorized() *ProblemDetails { return New(Unauthorized.Type, Unauthorized.Code).WithTitle(Unauthorized.Title) } +// NewPaymentRequired creates a new ProblemDetails instance that represents an HTTP 402 Payment Required. +func NewPaymentRequired() *ProblemDetails { + return New(PaymentRequired.Type, + PaymentRequired.Code).WithTitle(PaymentRequired.Title) +} + // NewForbidden creates a new ProblemDetails instance that represents an HTTP 403 Forbidden. func NewForbidden() *ProblemDetails { return New(Forbidden.Type, Forbidden.Code).WithTitle(Forbidden.Title) } +// NewNotFound creates a new ProblemDetails instance that represents an HTTP 404 Not Found. +func NewNotFound() *ProblemDetails { + return New(NotFound.Type, + NotFound.Code).WithTitle(NotFound.Title) +} + // NewMethodNotAllowed creates a new ProblemDetails instance that represents an HTTP 405 Method Not Allowed. func NewMethodNotAllowed() *ProblemDetails { return New(MethodNotAllowed.Type, MethodNotAllowed.Code).WithTitle(MethodNotAllowed.Title) } +// NewNotAcceptable creates a new ProblemDetails instance that represents an HTTP 406 Not Acceptable. +func NewNotAcceptable() *ProblemDetails { + return New(NotAcceptable.Type, + NotAcceptable.Code).WithTitle(NotAcceptable.Title) +} + +// NewProxyAuthenticationRequired creates a new ProblemDetails instance that represents an HTTP 407 Proxy Authentication Required. +func NewProxyAuthenticationRequired() *ProblemDetails { + return New(ProxyAuthenticationRequired.Type, + ProxyAuthenticationRequired.Code).WithTitle(ProxyAuthenticationRequired.Title) +} + +// NewRequestTimeout creates a new ProblemDetails instance that represents an HTTP 408 Request Timeout. +func NewRequestTimeout() *ProblemDetails { + return New(RequestTimeout.Type, + RequestTimeout.Code).WithTitle(RequestTimeout.Title) +} + // NewConflict creates a new ProblemDetails instance that represents an HTTP 409 Conflict. func NewConflict() *ProblemDetails { return New(Conflict.Type, Conflict.Code).WithTitle(Conflict.Title) } -// NewUnprocessableEntity creates a new ProblemDetails instance that represents an HTTP 422 Unprocessable Entity. -func NewUnprocessableEntity() *ProblemDetails { - return New(UnprocessableEntity.Type, - UnprocessableEntity.Code).WithTitle(UnprocessableEntity.Title) +// NewGone creates a new ProblemDetails instance that represents an HTTP 410 Gone. +func NewGone() *ProblemDetails { + return New(Gone.Type, + Gone.Code).WithTitle(Gone.Title) +} + +// NewLengthRequired creates a new ProblemDetails instance that represents an HTTP 411 Length Required. +func NewLengthRequired() *ProblemDetails { + return New(LengthRequired.Type, + LengthRequired.Code).WithTitle(LengthRequired.Title) +} + +// NewPreconditionFailed creates a new ProblemDetails instance that represents an HTTP 412 Precondition Failed. +func NewPreconditionFailed() *ProblemDetails { + return New(PreconditionFailed.Type, + PreconditionFailed.Code).WithTitle(PreconditionFailed.Title) +} + +// NewPayloadTooLarge creates a new ProblemDetails instance that represents an HTTP 413 Payload Too Large. +func NewPayloadTooLarge() *ProblemDetails { + return New(PayloadTooLarge.Type, + PayloadTooLarge.Code).WithTitle(PayloadTooLarge.Title) +} + +// NewURITooLong creates a new ProblemDetails instance that represents an HTTP 414 URI Too Long. +func NewURITooLong() *ProblemDetails { + return New(URITooLong.Type, + URITooLong.Code).WithTitle(URITooLong.Title) +} + +// NewUnsupportedMediaType creates a new ProblemDetails instance that represents an HTTP 415 Unsupported Media Type. +func NewUnsupportedMediaType() *ProblemDetails { + return New(UnsupportedMediaType.Type, + UnsupportedMediaType.Code).WithTitle(UnsupportedMediaType.Title) +} + +// NewRangeNotSatisfiable creates a new ProblemDetails instance that represents an HTTP 416 Range Not Satisfiable. +func NewRangeNotSatisfiable() *ProblemDetails { + return New(RangeNotSatisfiable.Type, + RangeNotSatisfiable.Code).WithTitle(RangeNotSatisfiable.Title) +} + +// NewExpectationFailed creates a new ProblemDetails instance that represents an HTTP 417 Expectation Failed. +func NewExpectationFailed() *ProblemDetails { + return New(ExpectationFailed.Type, + ExpectationFailed.Code).WithTitle(ExpectationFailed.Title) +} + +// NewMisdirectedRequest creates a new ProblemDetails instance that represents an HTTP 421 Misdirected Request. +func NewMisdirectedRequest() *ProblemDetails { + return New(MisdirectedRequest.Type, + MisdirectedRequest.Code).WithTitle(MisdirectedRequest.Title) +} + +// NewUpgradeRequired creates a new ProblemDetails instance that represents an HTTP 426 Upgrade Required. +func NewUpgradeRequired() *ProblemDetails { + return New(UpgradeRequired.Type, + UpgradeRequired.Code).WithTitle(UpgradeRequired.Title) +} + +// NewPreconditionRequired creates a new ProblemDetails instance that represents an HTTP 428 Precondition Required. +func NewPreconditionRequired() *ProblemDetails { + return New(PreconditionRequired.Type, + PreconditionRequired.Code).WithTitle(PreconditionRequired.Title) +} + +// NewTooManyRequests creates a new ProblemDetails instance that represents an HTTP 429 Too Many Requests. +func NewTooManyRequests() *ProblemDetails { + return New(TooManyRequests.Type, + TooManyRequests.Code).WithTitle(TooManyRequests.Title) +} + +// NewRequestHeaderFieldsTooLarge creates a new ProblemDetails instance that represents an HTTP 431 Request Header Fields Too Large. +func NewRequestHeaderFieldsTooLarge() *ProblemDetails { + return New(RequestHeaderFieldsTooLarge.Type, + RequestHeaderFieldsTooLarge.Code).WithTitle(RequestHeaderFieldsTooLarge.Title) +} + +// NewUnavailableForLegalReasons creates a new ProblemDetails instance that represents an HTTP 451 Unavailable For Legal Reasons. +func NewUnavailableForLegalReasons() *ProblemDetails { + return New(UnavailableForLegalReasons.Type, + UnavailableForLegalReasons.Code).WithTitle(UnavailableForLegalReasons.Title) } // NewInternalServerError creates a new ProblemDetails instance that represents an HTTP 500 Internal Server Error. @@ -174,3 +336,45 @@ func NewServiceUnavailable() *ProblemDetails { return New(ServiceUnavailable.Type, ServiceUnavailable.Code).WithTitle(ServiceUnavailable.Title) } + +// NewGatewayTimeout creates a new ProblemDetails instance that represents an HTTP 504 Gateway Timeout. +func NewGatewayTimeout() *ProblemDetails { + return New(GatewayTimeout.Type, + GatewayTimeout.Code).WithTitle(GatewayTimeout.Title) +} + +// NewHTTPVersionNotSupported creates a new ProblemDetails instance that represents an HTTP 505 HTTP Version Not Supported. +func NewHTTPVersionNotSupported() *ProblemDetails { + return New(HTTPVersionNotSupported.Type, + HTTPVersionNotSupported.Code).WithTitle(HTTPVersionNotSupported.Title) +} + +// NewVariantAlsoNegotiates creates a new ProblemDetails instance that represents an HTTP 506 Variant Also Negotiates. +func NewVariantAlsoNegotiates() *ProblemDetails { + return New(VariantAlsoNegotiates.Type, + VariantAlsoNegotiates.Code).WithTitle(VariantAlsoNegotiates.Title) +} + +// NewNotExtended creates a new ProblemDetails instance that represents an HTTP 510 Not Extended. +func NewNotExtended() *ProblemDetails { + return New(NotExtended.Type, + NotExtended.Code).WithTitle(NotExtended.Title) +} + +// NewNetworkAuthenticationRequired creates a new ProblemDetails instance that represents an HTTP 511 Network Authentication Required. +func NewNetworkAuthenticationRequired() *ProblemDetails { + return New(NetworkAuthenticationRequired.Type, + NetworkAuthenticationRequired.Code).WithTitle(NetworkAuthenticationRequired.Title) +} + +// NewTeapot creates a new ProblemDetails instance that represents an HTTP 418 I'm a teapot. +func NewTeapot() *ProblemDetails { + return New(Teapot.Type, + Teapot.Code).WithTitle(Teapot.Title) +} + +// NewTooEarly creates a new ProblemDetails instance that represents an HTTP 425 Too Early. +func NewTooEarly() *ProblemDetails { + return New(TooEarly.Type, + TooEarly.Code).WithTitle(TooEarly.Title) +} diff --git a/http/problemdetails/problemdetails_test.go b/http/problemdetails/problemdetails_test.go index 301e1d6..6107d35 100644 --- a/http/problemdetails/problemdetails_test.go +++ b/http/problemdetails/problemdetails_test.go @@ -167,16 +167,6 @@ func TestNewConflict(t *testing.T) { doCommonAssertions(t, problemDetails) } -func TestNewUnprocessableEntity(t *testing.T) { - problemDetails := NewUnprocessableEntity() - assert.Equal(t, UnprocessableEntity.Type, problemDetails.Type) - assert.Equal(t, UnprocessableEntity.Code, problemDetails.Status) - assert.Equal(t, UnprocessableEntity.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - func TestNewNotImplemented(t *testing.T) { problemDetails := NewNotImplemented() assert.Equal(t, NotImplemented.Type, problemDetails.Type) @@ -207,26 +197,355 @@ func TestNewServiceUnavailable(t *testing.T) { doCommonAssertions(t, problemDetails) } -func TestNewBadRequestCustomized(t *testing.T) { - title := "kaboom" - detail := "the unthinkable has occurred" - extKey1 := "key1" - extValue1 := "value1" - extKey2 := "key2" - extValue2 := "value2" +func TestNewGatewayTimeout(t *testing.T) { + problemDetails := NewGatewayTimeout() + assert.Equal(t, GatewayTimeout.Type, problemDetails.Type) + assert.Equal(t, GatewayTimeout.Code, problemDetails.Status) + assert.Equal(t, GatewayTimeout.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} - problemDetails := NewBadRequest().WithTitle(title).WithDetail(detail).WithExtension( - extKey1, extValue1).WithExtension(extKey2, extValue2) - assert.Equal(t, BadRequest.Type, problemDetails.Type) - assert.Equal(t, BadRequest.Code, problemDetails.Status) - assert.Equal(t, title, problemDetails.Title) - assert.Equal(t, detail, problemDetails.Detail) - assert.Equal(t, 2, len(problemDetails.Extensions)) - assert.Equal(t, extValue1, problemDetails.Extensions[extKey1]) - assert.Equal(t, extValue2, problemDetails.Extensions[extKey2]) +func TestNewTeapot(t *testing.T) { + problemDetails := NewTeapot() + assert.Equal(t, Teapot.Type, problemDetails.Type) + assert.Equal(t, Teapot.Code, problemDetails.Status) + assert.Equal(t, Teapot.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewEarlyHints(t *testing.T) { + problemDetails := NewEarlyHints() + assert.Equal(t, EarlyHints.Type, problemDetails.Type) + assert.Equal(t, EarlyHints.Code, problemDetails.Status) + assert.Equal(t, EarlyHints.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNonAuthoritativeInfo(t *testing.T) { + problemDetails := NewNonAuthoritativeInfo() + assert.Equal(t, NonAuthoritativeInfo.Type, problemDetails.Type) + assert.Equal(t, NonAuthoritativeInfo.Code, problemDetails.Status) + assert.Equal(t, NonAuthoritativeInfo.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) } +func TestNewResetContent(t *testing.T) { + problemDetails := NewResetContent() + assert.Equal(t, ResetContent.Type, problemDetails.Type) + assert.Equal(t, ResetContent.Code, problemDetails.Status) + assert.Equal(t, ResetContent.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewPartialContent(t *testing.T) { + problemDetails := NewPartialContent() + assert.Equal(t, PartialContent.Type, problemDetails.Type) + assert.Equal(t, PartialContent.Code, problemDetails.Status) + assert.Equal(t, PartialContent.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewIMUsed(t *testing.T) { + problemDetails := NewIMUsed() + assert.Equal(t, IMUsed.Type, problemDetails.Type) + assert.Equal(t, IMUsed.Code, problemDetails.Status) + assert.Equal(t, IMUsed.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewMultipleChoices(t *testing.T) { + problemDetails := NewMultipleChoices() + assert.Equal(t, MultipleChoices.Type, problemDetails.Type) + assert.Equal(t, MultipleChoices.Code, problemDetails.Status) + assert.Equal(t, MultipleChoices.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewSeeOther(t *testing.T) { + problemDetails := NewSeeOther() + assert.Equal(t, SeeOther.Type, problemDetails.Type) + assert.Equal(t, SeeOther.Code, problemDetails.Status) + assert.Equal(t, SeeOther.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewUseProxy(t *testing.T) { + problemDetails := NewUseProxy() + assert.Equal(t, UseProxy.Type, problemDetails.Type) + assert.Equal(t, UseProxy.Code, problemDetails.Status) + assert.Equal(t, UseProxy.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewTemporaryRedirect(t *testing.T) { + problemDetails := NewTemporaryRedirect() + assert.Equal(t, TemporaryRedirect.Type, problemDetails.Type) + assert.Equal(t, TemporaryRedirect.Code, problemDetails.Status) + assert.Equal(t, TemporaryRedirect.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewPermanentRedirect(t *testing.T) { + problemDetails := NewPermanentRedirect() + assert.Equal(t, PermanentRedirect.Type, problemDetails.Type) + assert.Equal(t, PermanentRedirect.Code, problemDetails.Status) + assert.Equal(t, PermanentRedirect.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewPaymentRequired(t *testing.T) { + problemDetails := NewPaymentRequired() + assert.Equal(t, PaymentRequired.Type, problemDetails.Type) + assert.Equal(t, PaymentRequired.Code, problemDetails.Status) + assert.Equal(t, PaymentRequired.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNotAcceptable(t *testing.T) { + problemDetails := NewNotAcceptable() + assert.Equal(t, NotAcceptable.Type, problemDetails.Type) + assert.Equal(t, NotAcceptable.Code, problemDetails.Status) + assert.Equal(t, NotAcceptable.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewProxyAuthenticationRequired(t *testing.T) { + problemDetails := NewProxyAuthenticationRequired() + assert.Equal(t, ProxyAuthenticationRequired.Type, problemDetails.Type) + assert.Equal(t, ProxyAuthenticationRequired.Code, problemDetails.Status) + assert.Equal(t, ProxyAuthenticationRequired.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewRequestTimeout(t *testing.T) { + problemDetails := NewRequestTimeout() + assert.Equal(t, RequestTimeout.Type, problemDetails.Type) + assert.Equal(t, RequestTimeout.Code, problemDetails.Status) + assert.Equal(t, RequestTimeout.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewGone(t *testing.T) { + problemDetails := NewGone() + assert.Equal(t, Gone.Type, problemDetails.Type) + assert.Equal(t, Gone.Code, problemDetails.Status) + assert.Equal(t, Gone.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewLengthRequired(t *testing.T) { + problemDetails := NewLengthRequired() + assert.Equal(t, LengthRequired.Type, problemDetails.Type) + assert.Equal(t, LengthRequired.Code, problemDetails.Status) + assert.Equal(t, LengthRequired.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewPreconditionFailed(t *testing.T) { + problemDetails := NewPreconditionFailed() + assert.Equal(t, PreconditionFailed.Type, problemDetails.Type) + assert.Equal(t, PreconditionFailed.Code, problemDetails.Status) + assert.Equal(t, PreconditionFailed.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewPayloadTooLarge(t *testing.T) { + problemDetails := NewPayloadTooLarge() + assert.Equal(t, PayloadTooLarge.Type, problemDetails.Type) + assert.Equal(t, PayloadTooLarge.Code, problemDetails.Status) + assert.Equal(t, PayloadTooLarge.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewURITooLong(t *testing.T) { + problemDetails := NewURITooLong() + assert.Equal(t, URITooLong.Type, problemDetails.Type) + assert.Equal(t, URITooLong.Code, problemDetails.Status) + assert.Equal(t, URITooLong.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewUnsupportedMediaType(t *testing.T) { + problemDetails := NewUnsupportedMediaType() + assert.Equal(t, UnsupportedMediaType.Type, problemDetails.Type) + assert.Equal(t, UnsupportedMediaType.Code, problemDetails.Status) + assert.Equal(t, UnsupportedMediaType.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewRangeNotSatisfiable(t *testing.T) { + problemDetails := NewRangeNotSatisfiable() + assert.Equal(t, RangeNotSatisfiable.Type, problemDetails.Type) + assert.Equal(t, RangeNotSatisfiable.Code, problemDetails.Status) + assert.Equal(t, RangeNotSatisfiable.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewExpectationFailed(t *testing.T) { + problemDetails := NewExpectationFailed() + assert.Equal(t, ExpectationFailed.Type, problemDetails.Type) + assert.Equal(t, ExpectationFailed.Code, problemDetails.Status) + assert.Equal(t, ExpectationFailed.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewMisdirectedRequest(t *testing.T) { + problemDetails := NewMisdirectedRequest() + assert.Equal(t, MisdirectedRequest.Type, problemDetails.Type) + assert.Equal(t, MisdirectedRequest.Code, problemDetails.Status) + assert.Equal(t, MisdirectedRequest.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewUpgradeRequired(t *testing.T) { + problemDetails := NewUpgradeRequired() + assert.Equal(t, UpgradeRequired.Type, problemDetails.Type) + assert.Equal(t, UpgradeRequired.Code, problemDetails.Status) + assert.Equal(t, UpgradeRequired.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewPreconditionRequired(t *testing.T) { + problemDetails := NewPreconditionRequired() + assert.Equal(t, PreconditionRequired.Type, problemDetails.Type) + assert.Equal(t, PreconditionRequired.Code, problemDetails.Status) + assert.Equal(t, PreconditionRequired.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewTooManyRequests(t *testing.T) { + problemDetails := NewTooManyRequests() + assert.Equal(t, TooManyRequests.Type, problemDetails.Type) + assert.Equal(t, TooManyRequests.Code, problemDetails.Status) + assert.Equal(t, TooManyRequests.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewRequestHeaderFieldsTooLarge(t *testing.T) { + problemDetails := NewRequestHeaderFieldsTooLarge() + assert.Equal(t, RequestHeaderFieldsTooLarge.Type, problemDetails.Type) + assert.Equal(t, RequestHeaderFieldsTooLarge.Code, problemDetails.Status) + assert.Equal(t, RequestHeaderFieldsTooLarge.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewUnavailableForLegalReasons(t *testing.T) { + problemDetails := NewUnavailableForLegalReasons() + assert.Equal(t, UnavailableForLegalReasons.Type, problemDetails.Type) + assert.Equal(t, UnavailableForLegalReasons.Code, problemDetails.Status) + assert.Equal(t, UnavailableForLegalReasons.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewHTTPVersionNotSupported(t *testing.T) { + problemDetails := NewHTTPVersionNotSupported() + assert.Equal(t, HTTPVersionNotSupported.Type, problemDetails.Type) + assert.Equal(t, HTTPVersionNotSupported.Code, problemDetails.Status) + assert.Equal(t, HTTPVersionNotSupported.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewVariantAlsoNegotiates(t *testing.T) { + problemDetails := NewVariantAlsoNegotiates() + assert.Equal(t, VariantAlsoNegotiates.Type, problemDetails.Type) + assert.Equal(t, VariantAlsoNegotiates.Code, problemDetails.Status) + assert.Equal(t, VariantAlsoNegotiates.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNotExtended(t *testing.T) { + problemDetails := NewNotExtended() + assert.Equal(t, NotExtended.Type, problemDetails.Type) + assert.Equal(t, NotExtended.Code, problemDetails.Status) + assert.Equal(t, NotExtended.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNetworkAuthenticationRequired(t *testing.T) { + problemDetails := NewNetworkAuthenticationRequired() + assert.Equal(t, NetworkAuthenticationRequired.Type, problemDetails.Type) + assert.Equal(t, NetworkAuthenticationRequired.Code, problemDetails.Status) + assert.Equal(t, NetworkAuthenticationRequired.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewTooEarly(t *testing.T) { + problemDetails := NewTooEarly() + assert.Equal(t, TooEarly.Type, problemDetails.Type) + assert.Equal(t, TooEarly.Code, problemDetails.Status) + assert.Equal(t, TooEarly.Title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} func doCommonAssertions(t *testing.T, problemDetails *ProblemDetails) { assert.Equal(t, true, strings.Contains(problemDetails.Instance, uuidUrnPrefix)) diff --git a/http/problemdetails/problemtype.go b/http/problemdetails/problemtype.go index 1bf8160..8142bc7 100644 --- a/http/problemdetails/problemtype.go +++ b/http/problemdetails/problemtype.go @@ -4,170 +4,442 @@ import "net/http" // ProblemType represents a problem type as defined in RFC 9457. This struct encapsulates the details of different types of HTTP problems, such as their type URIs, status codes, and titles. This struct would help in organizing and reusing these details across the codebase. type ProblemType struct { - Code int - Type string - Title string + Code int + Type string + Title string + Detail string } var ( + // Continue (HTTP 100) indicates that the initial part of a request has been received and has not yet been rejected by the server. Continue = ProblemType{ - Code: http.StatusContinue, - Type: "urn:problems:continue", - Title: "The server has received the request headers and the client should proceed to send the request body.", + Code: http.StatusContinue, + Type: "urn:problems:continue", + Title: "Continue", + Detail: "The server has received the request headers and the client should proceed to send the request body.", } + + // SwitchingProtocols (HTTP 101) indicates that the server is willing to switch protocols as requested by the client. SwitchingProtocols = ProblemType{ - Code: http.StatusSwitchingProtocols, - Type: "urn:problems:switching-protocols", - Title: "The server is switching protocols according to the Upgrade header.", + Code: http.StatusSwitchingProtocols, + Type: "urn:problems:switching-protocols", + Title: "Switching Protocols", + Detail: "The server is switching protocols according to the Upgrade header.", + } + + // EarlyHints (HTTP 103) indicates that the server is likely to send a final response with the header fields included in the informational response. + EarlyHints = ProblemType{ + Code: http.StatusEarlyHints, + Type: "urn:problems:early-hints", + Title: "Early Hints", + Detail: "The server is likely to send a final response with the header fields included in the informational response.", } + + // OK (HTTP 200) indicates that the request has succeeded. OK = ProblemType{ - Code: http.StatusOK, - Type: "urn:problems:ok", - Title: "The request has succeeded.", + Code: http.StatusOK, + Type: "urn:problems:ok", + Title: "OK", + Detail: "The request has succeeded.", } + + // Created (HTTP 201) indicates that the request has been fulfilled and has resulted in one or more new resources being created. Created = ProblemType{ - Code: http.StatusCreated, - Type: "urn:problems:created", - Title: "The request has been fulfilled and has resulted in one or more new resources being created.", + Code: http.StatusCreated, + Type: "urn:problems:created", + Title: "Created", + Detail: "The request has been fulfilled and has resulted in one or more new resources being created.", } + + // Accepted (HTTP 202) indicates that the request has been accepted for processing, but the processing has not been completed. Accepted = ProblemType{ - Code: http.StatusAccepted, - Type: "urn:problems:accepted", - Title: "The request has been accepted for processing, but the processing has not been completed.", + Code: http.StatusAccepted, + Type: "urn:problems:accepted", + Title: "Accepted", + Detail: "The request has been accepted for processing, but the processing has not been completed.", } + + // NonAuthoritativeInfo (HTTP 203) indicates that the request was successful but the enclosed payload has been modified from that of the origin server's 200 (OK) response by a transforming proxy. NonAuthoritativeInfo = ProblemType{ - Code: http.StatusNonAuthoritativeInfo, - Type: "urn:problems:non-authoritative-info", - Title: "The request was successful but the enclosed payload has been modified from that of the origin server's 200 (OK) response by a transforming proxy.", + Code: http.StatusNonAuthoritativeInfo, + Type: "urn:problems:non-authoritative-info", + Title: "Non-Authoritative Information", + Detail: "The request was successful but the enclosed payload has been modified from that of the origin server's 200 (OK) response by a transforming proxy.", } + + // NoContent (HTTP 204) indicates that the server has successfully fulfilled the request and that there is no additional content to send in the response payload body. NoContent = ProblemType{ - Code: http.StatusNoContent, - Type: "urn:problems:no-content", - Title: "The server has successfully fulfilled the request and that there is no additional content to send in the response payload body.", + Code: http.StatusNoContent, + Type: "urn:problems:no-content", + Title: "No Content", + Detail: "The server has successfully fulfilled the request and that there is no additional content to send in the response payload body.", } + + // ResetContent (HTTP 205) indicates that the server has fulfilled the request and desires that the user agent reset the 'document view' which caused the request to be sent. ResetContent = ProblemType{ - Code: http.StatusResetContent, - Type: "urn:problems:reset-content", - Title: "The server has fulfilled the request and desires that the user agent reset the 'document view' which caused the request to be sent.", + Code: http.StatusResetContent, + Type: "urn:problems:reset-content", + Title: "Reset Content", + Detail: "The server has fulfilled the request and desires that the user agent reset the 'document view' which caused the request to be sent.", } + + // PartialContent (HTTP 206) indicates that the server is successfully fulfilling a range request for the target resource by transferring one or more parts of the selected representation. PartialContent = ProblemType{ - Code: http.StatusPartialContent, - Type: "urn:problems:partial-content", - Title: "The server is successfully fulfilling a range request for the target resource by transferring one or more parts of the selected representation.", + Code: http.StatusPartialContent, + Type: "urn:problems:partial-content", + Title: "Partial Content", + Detail: "The server is successfully fulfilling a range request for the target resource by transferring one or more parts of the selected representation.", } + + // IMUsed (HTTP 226) indicates that the server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + IMUsed = ProblemType{ + Code: http.StatusIMUsed, + Type: "urn:problems:im-used", + Title: "IM Used", + Detail: "The server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.", + } + + // MultipleChoices (HTTP 300) indicates that the request has more than one possible response. MultipleChoices = ProblemType{ - Code: http.StatusMultipleChoices, - Type: "urn:problems:multiple-choices", - Title: "The request has more than one possible response.", + Code: http.StatusMultipleChoices, + Type: "urn:problems:multiple-choices", + Title: "Multiple Choices", + Detail: "The request has more than one possible response.", } + + // MovedPermanently (HTTP 301) indicates that the requested resource has been assigned a new permanent URI. MovedPermanently = ProblemType{ - Code: http.StatusMovedPermanently, - Type: "urn:problems:moved-permanently", - Title: "The requested resource has been assigned a new permanent URI.", + Code: http.StatusMovedPermanently, + Type: "urn:problems:moved-permanently", + Title: "Moved Permanently", + Detail: "The requested resource has been assigned a new permanent URI.", } + + // Found (HTTP 302) indicates that the requested resource resides temporarily under a different URI. Found = ProblemType{ - Code: http.StatusFound, - Type: "urn:problems:found", - Title: "The requested resource resides temporarily under a different URI.", + Code: http.StatusFound, + Type: "urn:problems:found", + Title: "Found", + Detail: "The requested resource resides temporarily under a different URI.", } + + // SeeOther (HTTP 303) indicates that the response to the request can be found under another URI using a GET method. SeeOther = ProblemType{ - Code: http.StatusSeeOther, - Type: "urn:problems:see-other", - Title: "The response to the request can be found under another URI using a GET method.", + Code: http.StatusSeeOther, + Type: "urn:problems:see-other", + Title: "See Other", + Detail: "The response to the request can be found under another URI using a GET method.", } + + // NotModified (HTTP 304) indicates that the resource has not been modified since the version specified by the request headers. NotModified = ProblemType{ - Code: http.StatusNotModified, - Type: "urn:problems:not-modified", - Title: "The resource has not been modified since the version specified by the request headers.", + Code: http.StatusNotModified, + Type: "urn:problems:not-modified", + Title: "Not Modified", + Detail: "The resource has not been modified since the version specified by the request headers.", } + + // UseProxy (HTTP 305) indicates that the requested resource is available only through a proxy. UseProxy = ProblemType{ - Code: http.StatusUseProxy, - Type: "urn:problems:use-proxy", - Title: "The requested resource is available only through a proxy.", + Code: http.StatusUseProxy, + Type: "urn:problems:use-proxy", + Title: "Use Proxy", + Detail: "The requested resource is available only through a proxy.", } + + // TemporaryRedirect (HTTP 307) indicates that the request should be repeated with another URI, but future requests can still use the original URI. TemporaryRedirect = ProblemType{ - Code: http.StatusTemporaryRedirect, - Type: "urn:problems:temporary-redirect", - Title: "The request should be repeated with another URI, but future requests can still use the original URI.", + Code: http.StatusTemporaryRedirect, + Type: "urn:problems:temporary-redirect", + Title: "Temporary Redirect", + Detail: "The request should be repeated with another URI, but future requests can still use the original URI.", } + + // PermanentRedirect (HTTP 308) indicates that the request and all future requests should be repeated using another URI. PermanentRedirect = ProblemType{ - Code: http.StatusPermanentRedirect, - Type: "urn:problems:permanent-redirect", - Title: "The request and all future requests should be repeated using another URI.", + Code: http.StatusPermanentRedirect, + Type: "urn:problems:permanent-redirect", + Title: "Permanent Redirect", + Detail: "The request and all future requests should be repeated using another URI.", } + + // BadRequest (HTTP 400) indicates that the provided request could not be processed because it is invalid. BadRequest = ProblemType{ - Code: http.StatusBadRequest, - Type: "urn:problems:bad-request", - Title: "The provided request could not be processed because it is invalid.", + Code: http.StatusBadRequest, + Type: "urn:problems:bad-request", + Title: "Bad Request", + Detail: "The provided request could not be processed because it is invalid.", } + + // Unauthorized (HTTP 401) indicates that the request requires user authentication. Unauthorized = ProblemType{ - Code: http.StatusUnauthorized, - Type: "urn:problems:unauthorized", - Title: "The request requires user authentication.", + Code: http.StatusUnauthorized, + Type: "urn:problems:unauthorized", + Title: "Unauthorized", + Detail: "The request requires user authentication.", + } + + // PaymentRequired (HTTP 402) indicates that the request requires payment before it can be processed. + PaymentRequired = ProblemType{ + Code: http.StatusPaymentRequired, + Type: "urn:problems:payment-required", + Title: "Payment Required", + Detail: "The request requires payment before it can be processed.", } + + // Forbidden (HTTP 403) indicates that the server understood the request, but refuses to authorize it. Forbidden = ProblemType{ - Code: http.StatusForbidden, - Type: "urn:problems:forbidden", - Title: "The server understood the request, but refuses to authorize it.", + Code: http.StatusForbidden, + Type: "urn:problems:forbidden", + Title: "Forbidden", + Detail: "The server understood the request, but refuses to authorize it.", } + + // NotFound (HTTP 404) indicates that the specified resource could not be found. NotFound = ProblemType{ - Code: http.StatusNotFound, - Type: "urn:problems:not-found", - Title: "The specified resource could not be found.", + Code: http.StatusNotFound, + Type: "urn:problems:not-found", + Title: "Not Found", + Detail: "The specified resource could not be found.", } + + // MethodNotAllowed (HTTP 405) indicates that the method specified in the request is not allowed for the resource. MethodNotAllowed = ProblemType{ - Code: http.StatusMethodNotAllowed, - Type: "urn:problems:method-not-allowed", - Title: "The method specified in the request is not allowed for the resource.", + Code: http.StatusMethodNotAllowed, + Type: "urn:problems:method-not-allowed", + Title: "Method Not Allowed", + Detail: "The method specified in the request is not allowed for the resource.", } + + // NotAcceptable (HTTP 406) indicates that the resource is not capable of generating content acceptable to the Accept headers. NotAcceptable = ProblemType{ - Code: http.StatusNotAcceptable, - Type: "urn:problems:not-acceptable", - Title: "The resource is not capable of generating content acceptable to the Accept headers.", + Code: http.StatusNotAcceptable, + Type: "urn:problems:not-acceptable", + Title: "Not Acceptable", + Detail: "The resource is not capable of generating content acceptable to the Accept headers.", } + + // ProxyAuthenticationRequired (HTTP 407) indicates that the client must first authenticate itself with the proxy. + ProxyAuthenticationRequired = ProblemType{ + Code: http.StatusProxyAuthRequired, + Type: "urn:problems:proxy-authentication-required", + Title: "Proxy Authentication Required", + Detail: "The client must first authenticate itself with the proxy.", + } + + // RequestTimeout (HTTP 408) indicates that the server timed out waiting for the request. + RequestTimeout = ProblemType{ + Code: http.StatusRequestTimeout, + Type: "urn:problems:request-timeout", + Title: "Request Timeout", + Detail: "The server timed out waiting for the request.", + } + + // Conflict (HTTP 409) indicates that the request could not be completed due to a conflict with the current state of the resource. Conflict = ProblemType{ - Code: http.StatusConflict, - Type: "urn:problems:conflict", - Title: "The request could not be completed due to a conflict with the current state of the resource.", + Code: http.StatusConflict, + Type: "urn:problems:conflict", + Title: "Conflict", + Detail: "The request could not be completed due to a conflict with the current state of the resource.", } + + // Gone (HTTP 410) indicates that the resource requested is no longer available and will not be available again. Gone = ProblemType{ - Code: http.StatusGone, - Type: "urn:problems:gone", - Title: "The resource requested is no longer available and will not be available again.", + Code: http.StatusGone, + Type: "urn:problems:gone", + Title: "Gone", + Detail: "The resource requested is no longer available and will not be available again.", } - UnprocessableEntity = ProblemType{ - Code: http.StatusUnprocessableEntity, - Type: "urn:problems:unprocessable-entity", - Title: "The server understands the content type of the request entity, but was unable to process the contained instructions.", + + // LengthRequired (HTTP 411) indicates that the request did not specify the length of its content, which is required by the requested resource. + LengthRequired = ProblemType{ + Code: http.StatusLengthRequired, + Type: "urn:problems:length-required", + Title: "Length Required", + Detail: "The request did not specify the length of its content, which is required by the requested resource.", + } + + // PreconditionFailed (HTTP 412) indicates that the server does not meet one of the preconditions that the requester put on the request. + PreconditionFailed = ProblemType{ + Code: http.StatusPreconditionFailed, + Type: "urn:problems:precondition-failed", + Title: "Precondition Failed", + Detail: "The server does not meet one of the preconditions that the requester put on the request.", } + + // PayloadTooLarge (HTTP 413) indicates that the request is larger than the server is willing or able to process. + PayloadTooLarge = ProblemType{ + Code: http.StatusRequestEntityTooLarge, + Type: "urn:problems:payload-too-large", + Title: "Payload Too Large", + Detail: "The request is larger than the server is willing or able to process.", + } + + // URITooLong (HTTP 414) indicates that the URI provided was too long for the server to process. + URITooLong = ProblemType{ + Code: http.StatusRequestURITooLong, + Type: "urn:problems:uri-too-long", + Title: "URI Too Long", + Detail: "The URI provided was too long for the server to process.", + } + + // UnsupportedMediaType (HTTP 415) indicates that the request entity has a media type which the server or resource does not support. + UnsupportedMediaType = ProblemType{ + Code: http.StatusUnsupportedMediaType, + Type: "urn:problems:unsupported-media-type", + Title: "Unsupported Media Type", + Detail: "The request entity has a media type which the server or resource does not support.", + } + + // RangeNotSatisfiable (HTTP 416) indicates that the client has asked for a portion of the file, but the server cannot supply that portion. + RangeNotSatisfiable = ProblemType{ + Code: http.StatusRequestedRangeNotSatisfiable, + Type: "urn:problems:range-not-satisfiable", + Title: "Range Not Satisfiable", + Detail: "The client has asked for a portion of the file, but the server cannot supply that portion.", + } + + // ExpectationFailed (HTTP 417) indicates that the server cannot meet the requirements of the Expect request-header field. + ExpectationFailed = ProblemType{ + Code: http.StatusExpectationFailed, + Type: "urn:problems:expectation-failed", + Title: "Expectation Failed", + Detail: "The server cannot meet the requirements of the Expect request-header field.", + } + + // Teapot (HTTP 418) indicates that the server is a teapot and cannot brew coffee. + Teapot = ProblemType{ + Code: http.StatusTeapot, + Type: "urn:problems:teapot", + Title: "I'm a teapot", + Detail: "I'm a teapot.", + } + + // MisdirectedRequest (HTTP 421) indicates that the request was directed at a server that is not able to produce a response. + MisdirectedRequest = ProblemType{ + Code: http.StatusMisdirectedRequest, + Type: "urn:problems:misdirected-request", + Title: "Misdirected Request", + Detail: "The request was directed at a server that is not able to produce a response.", + } + + // TooEarly (HTTP 425) indicates that the server is unwilling to risk processing a request that might be replayed. + TooEarly = ProblemType{ + Code: http.StatusTooEarly, + Type: "urn:problems:too-early", + Title: "Too Early", + Detail: "The server is unwilling to risk processing a request that might be replayed.", + } + + // UpgradeRequired (HTTP 426) indicates that the client should switch to a different protocol. + UpgradeRequired = ProblemType{ + Code: http.StatusUpgradeRequired, + Type: "urn:problems:upgrade-required", + Title: "Upgrade Required", + Detail: "The client should switch to a different protocol.", + } + + // PreconditionRequired (HTTP 428) indicates that the server requires the request to be conditional. + PreconditionRequired = ProblemType{ + Code: http.StatusPreconditionRequired, + Type: "urn:problems:precondition-required", + Title: "Precondition Required", + Detail: "The server requires the request to be conditional.", + } + + // TooManyRequests (HTTP 429) indicates that the user has sent too many requests in a given amount of time. + TooManyRequests = ProblemType{ + Code: http.StatusTooManyRequests, + Type: "urn:problems:too-many-requests", + Title: "Too Many Requests", + Detail: "The user has sent too many requests in a given amount of time.", + } + + // RequestHeaderFieldsTooLarge (HTTP 431) indicates that the server is unwilling to process the request because its header fields are too large. + RequestHeaderFieldsTooLarge = ProblemType{ + Code: http.StatusRequestHeaderFieldsTooLarge, + Type: "urn:problems:request-header-fields-too-large", + Title: "Request Header Fields Too Large", + Detail: "The server is unwilling to process the request because its header fields are too large.", + } + + // UnavailableForLegalReasons (HTTP 451) indicates that the resource is unavailable for legal reasons. + UnavailableForLegalReasons = ProblemType{ + Code: http.StatusUnavailableForLegalReasons, + Type: "urn:problems:unavailable-for-legal-reasons", + Title: "Unavailable For Legal Reasons", + Detail: "The resource is unavailable for legal reasons.", + } + + // InternalServerError (HTTP 500) indicates that the server has encountered a situation it doesn't know how to handle. InternalServerError = ProblemType{ - Code: http.StatusInternalServerError, - Type: "urn:problems:internal-server-error", - Title: "The server has encountered a situation it doesn't know how to handle.", + Code: http.StatusInternalServerError, + Type: "urn:problems:internal-server-error", + Title: "Internal Server Error", + Detail: "The server has encountered a situation it doesn't know how to handle.", } + + // NotImplemented (HTTP 501) indicates that the request method is not supported by the server and cannot be handled. NotImplemented = ProblemType{ - Code: http.StatusNotImplemented, - Type: "urn:problems:not-implemented", - Title: "The request method is not supported by the server and cannot be handled.", + Code: http.StatusNotImplemented, + Type: "urn:problems:not-implemented", + Title: "Not Implemented", + Detail: "The request method is not supported by the server and cannot be handled.", } + + // BadGateway (HTTP 502) indicates that the server, while acting as a gateway or proxy, received an invalid response from the upstream server. BadGateway = ProblemType{ - Code: http.StatusBadGateway, - Type: "urn:problems:bad-gateway", - Title: "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.", + Code: http.StatusBadGateway, + Type: "urn:problems:bad-gateway", + Title: "Bad Gateway", + Detail: "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.", } + + // ServiceUnavailable (HTTP 503) indicates that the server is not ready to handle the request, often due to maintenance or overload. ServiceUnavailable = ProblemType{ - Code: http.StatusServiceUnavailable, - Type: "urn:problems:service-unavailable", - Title: "The server is not ready to handle the request, often due to maintenance or overload.", + Code: http.StatusServiceUnavailable, + Type: "urn:problems:service-unavailable", + Title: "Service Unavailable", + Detail: "The server is not ready to handle the request, often due to maintenance or overload.", } + + // GatewayTimeout (HTTP 504) indicates that the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server. GatewayTimeout = ProblemType{ - Code: http.StatusGatewayTimeout, - Type: "urn:problems:gateway-timeout", - Title: "The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.", + Code: http.StatusGatewayTimeout, + Type: "urn:problems:gateway-timeout", + Title: "Gateway Timeout", + Detail: "The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.", } + + // HTTPVersionNotSupported (HTTP 505) indicates that the server does not support the HTTP protocol version used in the request. HTTPVersionNotSupported = ProblemType{ - Code: http.StatusHTTPVersionNotSupported, - Type: "urn:problems:http-version-not-supported", - Title: "The server does not support the HTTP protocol version used in the request.", + Code: http.StatusHTTPVersionNotSupported, + Type: "urn:problems:http-version-not-supported", + Title: "HTTP Version Not Supported", + Detail: "The server does not support the HTTP protocol version used in the request.", + } + + // VariantAlsoNegotiates (HTTP 506) indicates that the server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process. + VariantAlsoNegotiates = ProblemType{ + Code: http.StatusVariantAlsoNegotiates, + Type: "urn:problems:variant-also-negotiates", + Title: "Variant Also Negotiates", + Detail: "The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.", + } + + // NotExtended (HTTP 510) indicates that further extensions to the request are required for the server to fulfill it. + NotExtended = ProblemType{ + Code: http.StatusNotExtended, + Type: "urn:problems:not-extended", + Title: "Not Extended", + Detail: "Further extensions to the request are required for the server to fulfill it.", + } + + // NetworkAuthenticationRequired (HTTP 511) indicates that the client needs to authenticate to gain network access. + NetworkAuthenticationRequired = ProblemType{ + Code: http.StatusNetworkAuthenticationRequired, + Type: "urn:problems:network-authentication-required", + Title: "Network Authentication Required", + Detail: "The client needs to authenticate to gain network access.", } ) diff --git a/internal/examples/problemdetails-example.go b/internal/examples/problemdetails-example.go index 06ab7d0..8a73a9e 100644 --- a/internal/examples/problemdetails-example.go +++ b/internal/examples/problemdetails-example.go @@ -1 +1,25 @@ package main + +import ( + "fmt" + "github.com/Evernorth/http-problemdetails-go/http/problemdetails" +) + +func main() { + // Example of creating a ProblemDetails instance for HTTP 500 Internal Server Error + internalServerError := problemdetails.NewInternalServerError() + fmt.Printf("Type: %s, Status: %d, Title: %s\n", internalServerError.Type, internalServerError.Status, internalServerError.Title) + + // Example of creating a ProblemDetails instance for HTTP 400 Bad Request + badRequest := problemdetails.NewBadRequest().WithTitle("This is bad").WithDetail("This is a very bad request") + fmt.Printf("Type: %s, Status: %d, Title: %s, Detail: %s\n", badRequest.Type, badRequest.Status, badRequest.Title, badRequest.Detail) + + // Example of creating a ProblemDetails instance for HTTP 404 Not Found + notFound := problemdetails.NewNotFound() + fmt.Printf("Type: %s, Status: %d, Title: %s\n", notFound.Type, notFound.Status, notFound.Title) + + // Example of creating a ProblemDetails instance for HTTP 418 I'm a teapot + teapot := problemdetails.NewTeapot().WithDetail(problemdetails.Teapot.Detail) + fmt.Printf("Type: %s, Status: %d, Title: %s\n", teapot.Type, teapot.Status, teapot.Title) + +} From 298ade5fa5a9a681e3b54965ecec266b14b151d5 Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:08:33 -0400 Subject: [PATCH 05/21] Initial Commit - Defined `ProblemType` struct and various HTTP problem types in `problemtype.go`. - Updated `ProblemDetails` methods for creating instances in `problemdetails.go`. - Added unit tests for `ProblemDetails` creation functions in `problemdetails_test.go`. - Added examples to `internal/examples` - Updated README.md --- CODEOWNERS | 2 +- CONTRIBUTING.md | 35 +- INSTALL.md | 3 - README.md | 67 +-- go.mod | 2 +- http/problemdetails/problemdetails.go | 380 ------------- http/problemdetails/problemdetails_test.go | 557 -------------------- http/problemdetails/problemtype.go | 445 ---------------- internal/examples/advanced-example.go | 25 + internal/examples/basic-example.go | 23 + internal/examples/go.mod | 3 + internal/examples/problemdetails-example.go | 25 - problemdetails.go | 123 +++++ problemdetails_test.go | 128 +++++ problemtype.go | 90 ++++ 15 files changed, 450 insertions(+), 1458 deletions(-) delete mode 100644 INSTALL.md delete mode 100644 http/problemdetails/problemdetails.go delete mode 100644 http/problemdetails/problemdetails_test.go delete mode 100644 http/problemdetails/problemtype.go create mode 100644 internal/examples/advanced-example.go create mode 100644 internal/examples/basic-example.go create mode 100644 internal/examples/go.mod delete mode 100644 internal/examples/problemdetails-example.go create mode 100644 problemdetails.go create mode 100644 problemdetails_test.go create mode 100644 problemtype.go diff --git a/CODEOWNERS b/CODEOWNERS index b5a7e0c..78cf01f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,3 +1,3 @@ # Global owners * @stevensefton -* @shelleeboo \ No newline at end of file +* @shelleeboo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f84045..e358232 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,18 +1,21 @@ -# Guidance on how to contribute +# Contributing -Include information that will help a contributor understand expectations and be successful, such as the examples below. -> Want to see a good real world example? Take a look at the [CONTRIBUTING.adoc](https://github.com/spring-projects/spring-boot/blob/main/CONTRIBUTING.adoc) for spring-boot. +Thanks for your interest in contributing to this project! -* Where to ask Questions? Please make sure it is SEARCHABLE -* Who are the TC’s (Trusted Committers)? -* How to file an Issue -* How to file a PR -* Dependencies -* Style Guide / Coding standards -* Developer Environment -* Branching/ versioning -* Features -* Testing -* Roadmap -* Calendar -* Links to other documentation \ No newline at end of file +We welcome any kind of contribution including: + +- Documentation +- Examples +- New features and feature requests +- Bug fixes + +Please open Pull Requests for small changes. + +For larger changes please submit an issue with additional details. + +This issue should outline the following: + +- The use case that your changes are applicable to. +- Steps to reproduce the issue(s) if applicable. +- Detailed description of what your changes would entail. +- Alternative solutions or approaches if applicable. \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index a7aa523..0000000 --- a/INSTALL.md +++ /dev/null @@ -1,3 +0,0 @@ -# Installation instructions - -Detailed instructions on how to install, configure, and get the project running. If the instructions are minimal, they can reside in the Readme Installation section. \ No newline at end of file diff --git a/README.md b/README.md index 285ef96..eb61ccd 100644 --- a/README.md +++ b/README.md @@ -13,48 +13,65 @@ Detailed instructions on how to build the project from source. Also note where t ## Installation ```go get -u github.com/Evernorth/http-problemdetails-go``` -## Configuration - -If the software is configurable, describe it in detail, either here or in other documentation to which you link. - +## Features + - The MIME type of the response is `application/problem+json`. + - Supports the following HTTP status codes: + +| HTTP Code | Method | +|-------------------------------------|-------------------------------------------------| +| 400 Bad Request | [`NewBadRequest()`](problemdetails.go) | +| 401 Unauthorized | [`NewUnauthorized()`](problemdetails.go) | +| 403 Forbidden | [`NewForbidden()`](problemdetails.go) | +| 404 Not Found | [`NewNotFound()`](problemdetails.go) | +| 409 Conflict | [`NewConflict()`](problemdetails.go) | +| 429 Too Many Requests | [`NewTooManyRequests()`](problemdetails.go) | +| 500 Internal Server Error | [`NewInternalServerError()`](problemdetails.go) | +| 501 Not Implemented | [`NewNotImplemented()`](problemdetails.go) | +| 502 Bad Gateway | [`NewBadGateway()`](problemdetails.go) | +| 503 Service Unavailable | [`NewServiceUnavailable()`](problemdetails.go) | +| 504 Gateway Timeout | [`NewGatewayTimeout()`](problemdetails.go) | + + +>Note: MIME type must be set in the response header to comply with the RFC 9457 specification. ## Usage ### Creating a simple HTTP ProblemDetails You can create a ProblemDetails instance by simply calling functions like `problemdetails.NewInternalServerError()`, `problemdetails.NewNotFound()` and `problemdetails.NewBadRequest()`. -```go -package employee +``` +package main import ( - "github.com/Evernorth/http-problemdetails-go/http/problemdetails" + "github.com/Evernorth/http-problemdetails-go/problemdetails" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "net/http" ) -func getEmployee(httpRespWriter http.ResponseWriter, httpReq *http.Request) { +func getSomething(httpRespWriter http.ResponseWriter, httpReq *http.Request) { err := doSomething() if err != nil { httpRespWriter.WriteHeader(http.StatusInternalServerError) render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) - return + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + return } ... } ``` ### Creating a complex HTTP ProblemDetails You can easily override the default ProblemDetails title and detail fields and provide custom extension fields by using the `WithTitle`, `WithDetail` and `WithExtension` functions. -```go -package employee +``` +package main import ( - "github.com/Evernorth/http-problemdetails-go/http/problemdetails" + "github.com/Evernorth/http-problemdetails-go/problemdetails" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "net/http" ) -func getEmployee(httpRespWriter http.ResponseWriter, httpReq *http.Request) { +func getSomething(httpRespWriter http.ResponseWriter, httpReq *http.Request) { err := doSomething() if err != nil { @@ -63,30 +80,20 @@ func getEmployee(httpRespWriter http.ResponseWriter, httpReq *http.Request) { "KABOOM!!!").WithDetail("The unthinkable has occurred.").WithExtension( "example1", "test").WithExtension( "example2", "this could be a struct if you like")) + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + return } ... } ``` -## How to test the software - -If the software includes automated tests, detail how to run those tests. - -## Known issues - -Document any known significant shortcomings with the software. - -## Getting help +## Support If you have questions, concerns, bug reports, etc. See [CONTRIBUTING](CONTRIBUTING.md). -## Getting involved - -This section should detail why people should get involved and describe key areas you are -currently focusing on; e.g., trying to get feedback on features, fixing certain bugs, building -important pieces, etc. - -General instructions on _how_ to contribute should be stated with a link to [CONTRIBUTING](CONTRIBUTING.md). - ## License http-problemdetails-go is Open Source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). + +## Original Contributors +- Steve Sefton, Evernorth +- Shellee Stewart, Evernorth diff --git a/go.mod b/go.mod index c581d7e..5cf9259 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Evernorth/http-problemdetails-go +module github.com/Evernorth/problemdetails go 1.23.1 diff --git a/http/problemdetails/problemdetails.go b/http/problemdetails/problemdetails.go deleted file mode 100644 index 595147e..0000000 --- a/http/problemdetails/problemdetails.go +++ /dev/null @@ -1,380 +0,0 @@ -package problemdetails - -import ( - "github.com/google/uuid" - "time" -) - -const ( - uuidUrnPrefix = "urn:uuid:" -) - -// ProblemDetails represents a HTTP Problem Details structure from https://www.rfc-editor.org/rfc/rfc9457.html -type ProblemDetails struct { - Type string `json:"type"` - Status int `json:"status"` - Title string `json:"title,omitempty"` - Detail string `json:"detail,omitempty"` - Instance string `json:"instance"` - Created string `json:"created"` - Extensions map[string]any `json:"extensions,omitempty"` -} - -// WithTitle sets a custom Title value on the ProblemDetails object. The ProblemDetails pointer is -// returned only for developer convenience. -func (p *ProblemDetails) WithTitle(title string) *ProblemDetails { - p.Title = title - return p -} - -// WithDetail sets a custom Detail value on the ProblemDetails object. The ProblemDetails pointer is -// returned only for developer convenience. -func (p *ProblemDetails) WithDetail(detail string) *ProblemDetails { - p.Detail = detail - return p -} - -// WithExtension adds a custom Extension value on the ProblemDetails object. The ProblemDetails pointer is -// returned only for developer convenience. -func (p *ProblemDetails) WithExtension(name string, value any) *ProblemDetails { - if p.Extensions == nil { - p.Extensions = make(map[string]any) - } - p.Extensions[name] = value - return p //&p recreates pointer? -} - -// New creates a new ProblemDetails instance. Do not use this function unless you are creating a -// custom ProblemDetails instance. Prefer using the functions that create standard ProblemDetails instances. -func New(typeUri string, status int) *ProblemDetails { - return &ProblemDetails{ - Type: typeUri, - Status: status, - Instance: uuidUrnPrefix + uuid.NewString(), - Created: time.Now().UTC().Format(time.RFC3339Nano), - } -} - -// NewContinue creates a new ProblemDetails instance that represents an HTTP 100 Continue. -func NewContinue() *ProblemDetails { - return New(Continue.Type, - Continue.Code).WithTitle(Continue.Title) -} - -// NewSwitchingProtocols creates a new ProblemDetails instance that represents an HTTP 101 Switching Protocols. -func NewSwitchingProtocols() *ProblemDetails { - return New(SwitchingProtocols.Type, - SwitchingProtocols.Code).WithTitle(SwitchingProtocols.Title) -} - -// NewEarlyHints creates a new ProblemDetails instance that represents an HTTP 103 Early Hints. -func NewEarlyHints() *ProblemDetails { - return New(EarlyHints.Type, - EarlyHints.Code).WithTitle(EarlyHints.Title) -} - -// NewOK creates a new ProblemDetails instance that represents an HTTP 200 OK. -func NewOK() *ProblemDetails { - return New(OK.Type, - OK.Code).WithTitle(OK.Title) -} - -// NewCreated creates a new ProblemDetails instance that represents an HTTP 201 Created. -func NewCreated() *ProblemDetails { - return New(Created.Type, - Created.Code).WithTitle(Created.Title) -} - -// NewAccepted creates a new ProblemDetails instance that represents an HTTP 202 Accepted. -func NewAccepted() *ProblemDetails { - return New(Accepted.Type, - Accepted.Code).WithTitle(Accepted.Title) -} - -// NewNonAuthoritativeInfo creates a new ProblemDetails instance that represents an HTTP 203 Non-Authoritative Information. -func NewNonAuthoritativeInfo() *ProblemDetails { - return New(NonAuthoritativeInfo.Type, - NonAuthoritativeInfo.Code).WithTitle(NonAuthoritativeInfo.Title) -} - -// NewNoContent creates a new ProblemDetails instance that represents an HTTP 204 No Content. -func NewNoContent() *ProblemDetails { - return New(NoContent.Type, - NoContent.Code).WithTitle(NoContent.Title) -} - -// NewResetContent creates a new ProblemDetails instance that represents an HTTP 205 Reset Content. -func NewResetContent() *ProblemDetails { - return New(ResetContent.Type, - ResetContent.Code).WithTitle(ResetContent.Title) -} - -// NewPartialContent creates a new ProblemDetails instance that represents an HTTP 206 Partial Content. -func NewPartialContent() *ProblemDetails { - return New(PartialContent.Type, - PartialContent.Code).WithTitle(PartialContent.Title) -} - -// NewIMUsed creates a new ProblemDetails instance that represents an HTTP 226 IM Used. -func NewIMUsed() *ProblemDetails { - return New(IMUsed.Type, - IMUsed.Code).WithTitle(IMUsed.Title) -} - -// NewMultipleChoices creates a new ProblemDetails instance that represents an HTTP 300 Multiple Choices. -func NewMultipleChoices() *ProblemDetails { - return New(MultipleChoices.Type, - MultipleChoices.Code).WithTitle(MultipleChoices.Title) -} - -// NewMovedPermanently creates a new ProblemDetails instance that represents an HTTP 301 Moved Permanently. -func NewMovedPermanently() *ProblemDetails { - return New(MovedPermanently.Type, - MovedPermanently.Code).WithTitle(MovedPermanently.Title) -} - -// NewFound creates a new ProblemDetails instance that represents an HTTP 302 Found. -func NewFound() *ProblemDetails { - return New(Found.Type, - Found.Code).WithTitle(Found.Title) -} - -// NewSeeOther creates a new ProblemDetails instance that represents an HTTP 303 See Other. -func NewSeeOther() *ProblemDetails { - return New(SeeOther.Type, - SeeOther.Code).WithTitle(SeeOther.Title) -} - -// NewNotModified creates a new ProblemDetails instance that represents an HTTP 304 Not Modified. -func NewNotModified() *ProblemDetails { - return New(NotModified.Type, - NotModified.Code).WithTitle(NotModified.Title) -} - -// NewUseProxy creates a new ProblemDetails instance that represents an HTTP 305 Use Proxy. -func NewUseProxy() *ProblemDetails { - return New(UseProxy.Type, - UseProxy.Code).WithTitle(UseProxy.Title) -} - -// NewTemporaryRedirect creates a new ProblemDetails instance that represents an HTTP 307 Temporary Redirect. -func NewTemporaryRedirect() *ProblemDetails { - return New(TemporaryRedirect.Type, - TemporaryRedirect.Code).WithTitle(TemporaryRedirect.Title) -} - -// NewPermanentRedirect creates a new ProblemDetails instance that represents an HTTP 308 Permanent Redirect. -func NewPermanentRedirect() *ProblemDetails { - return New(PermanentRedirect.Type, - PermanentRedirect.Code).WithTitle(PermanentRedirect.Title) -} - -// NewBadRequest creates a new ProblemDetails instance that represents an HTTP 400 Bad Request. -func NewBadRequest() *ProblemDetails { - return New(BadRequest.Type, - BadRequest.Code).WithTitle(BadRequest.Title) -} - -// NewUnauthorized creates a new ProblemDetails instance that represents an HTTP 401 Unauthorized. -func NewUnauthorized() *ProblemDetails { - return New(Unauthorized.Type, - Unauthorized.Code).WithTitle(Unauthorized.Title) -} - -// NewPaymentRequired creates a new ProblemDetails instance that represents an HTTP 402 Payment Required. -func NewPaymentRequired() *ProblemDetails { - return New(PaymentRequired.Type, - PaymentRequired.Code).WithTitle(PaymentRequired.Title) -} - -// NewForbidden creates a new ProblemDetails instance that represents an HTTP 403 Forbidden. -func NewForbidden() *ProblemDetails { - return New(Forbidden.Type, - Forbidden.Code).WithTitle(Forbidden.Title) -} - -// NewNotFound creates a new ProblemDetails instance that represents an HTTP 404 Not Found. -func NewNotFound() *ProblemDetails { - return New(NotFound.Type, - NotFound.Code).WithTitle(NotFound.Title) -} - -// NewMethodNotAllowed creates a new ProblemDetails instance that represents an HTTP 405 Method Not Allowed. -func NewMethodNotAllowed() *ProblemDetails { - return New(MethodNotAllowed.Type, - MethodNotAllowed.Code).WithTitle(MethodNotAllowed.Title) -} - -// NewNotAcceptable creates a new ProblemDetails instance that represents an HTTP 406 Not Acceptable. -func NewNotAcceptable() *ProblemDetails { - return New(NotAcceptable.Type, - NotAcceptable.Code).WithTitle(NotAcceptable.Title) -} - -// NewProxyAuthenticationRequired creates a new ProblemDetails instance that represents an HTTP 407 Proxy Authentication Required. -func NewProxyAuthenticationRequired() *ProblemDetails { - return New(ProxyAuthenticationRequired.Type, - ProxyAuthenticationRequired.Code).WithTitle(ProxyAuthenticationRequired.Title) -} - -// NewRequestTimeout creates a new ProblemDetails instance that represents an HTTP 408 Request Timeout. -func NewRequestTimeout() *ProblemDetails { - return New(RequestTimeout.Type, - RequestTimeout.Code).WithTitle(RequestTimeout.Title) -} - -// NewConflict creates a new ProblemDetails instance that represents an HTTP 409 Conflict. -func NewConflict() *ProblemDetails { - return New(Conflict.Type, - Conflict.Code).WithTitle(Conflict.Title) -} - -// NewGone creates a new ProblemDetails instance that represents an HTTP 410 Gone. -func NewGone() *ProblemDetails { - return New(Gone.Type, - Gone.Code).WithTitle(Gone.Title) -} - -// NewLengthRequired creates a new ProblemDetails instance that represents an HTTP 411 Length Required. -func NewLengthRequired() *ProblemDetails { - return New(LengthRequired.Type, - LengthRequired.Code).WithTitle(LengthRequired.Title) -} - -// NewPreconditionFailed creates a new ProblemDetails instance that represents an HTTP 412 Precondition Failed. -func NewPreconditionFailed() *ProblemDetails { - return New(PreconditionFailed.Type, - PreconditionFailed.Code).WithTitle(PreconditionFailed.Title) -} - -// NewPayloadTooLarge creates a new ProblemDetails instance that represents an HTTP 413 Payload Too Large. -func NewPayloadTooLarge() *ProblemDetails { - return New(PayloadTooLarge.Type, - PayloadTooLarge.Code).WithTitle(PayloadTooLarge.Title) -} - -// NewURITooLong creates a new ProblemDetails instance that represents an HTTP 414 URI Too Long. -func NewURITooLong() *ProblemDetails { - return New(URITooLong.Type, - URITooLong.Code).WithTitle(URITooLong.Title) -} - -// NewUnsupportedMediaType creates a new ProblemDetails instance that represents an HTTP 415 Unsupported Media Type. -func NewUnsupportedMediaType() *ProblemDetails { - return New(UnsupportedMediaType.Type, - UnsupportedMediaType.Code).WithTitle(UnsupportedMediaType.Title) -} - -// NewRangeNotSatisfiable creates a new ProblemDetails instance that represents an HTTP 416 Range Not Satisfiable. -func NewRangeNotSatisfiable() *ProblemDetails { - return New(RangeNotSatisfiable.Type, - RangeNotSatisfiable.Code).WithTitle(RangeNotSatisfiable.Title) -} - -// NewExpectationFailed creates a new ProblemDetails instance that represents an HTTP 417 Expectation Failed. -func NewExpectationFailed() *ProblemDetails { - return New(ExpectationFailed.Type, - ExpectationFailed.Code).WithTitle(ExpectationFailed.Title) -} - -// NewMisdirectedRequest creates a new ProblemDetails instance that represents an HTTP 421 Misdirected Request. -func NewMisdirectedRequest() *ProblemDetails { - return New(MisdirectedRequest.Type, - MisdirectedRequest.Code).WithTitle(MisdirectedRequest.Title) -} - -// NewUpgradeRequired creates a new ProblemDetails instance that represents an HTTP 426 Upgrade Required. -func NewUpgradeRequired() *ProblemDetails { - return New(UpgradeRequired.Type, - UpgradeRequired.Code).WithTitle(UpgradeRequired.Title) -} - -// NewPreconditionRequired creates a new ProblemDetails instance that represents an HTTP 428 Precondition Required. -func NewPreconditionRequired() *ProblemDetails { - return New(PreconditionRequired.Type, - PreconditionRequired.Code).WithTitle(PreconditionRequired.Title) -} - -// NewTooManyRequests creates a new ProblemDetails instance that represents an HTTP 429 Too Many Requests. -func NewTooManyRequests() *ProblemDetails { - return New(TooManyRequests.Type, - TooManyRequests.Code).WithTitle(TooManyRequests.Title) -} - -// NewRequestHeaderFieldsTooLarge creates a new ProblemDetails instance that represents an HTTP 431 Request Header Fields Too Large. -func NewRequestHeaderFieldsTooLarge() *ProblemDetails { - return New(RequestHeaderFieldsTooLarge.Type, - RequestHeaderFieldsTooLarge.Code).WithTitle(RequestHeaderFieldsTooLarge.Title) -} - -// NewUnavailableForLegalReasons creates a new ProblemDetails instance that represents an HTTP 451 Unavailable For Legal Reasons. -func NewUnavailableForLegalReasons() *ProblemDetails { - return New(UnavailableForLegalReasons.Type, - UnavailableForLegalReasons.Code).WithTitle(UnavailableForLegalReasons.Title) -} - -// NewInternalServerError creates a new ProblemDetails instance that represents an HTTP 500 Internal Server Error. -func NewInternalServerError() *ProblemDetails { - return New(InternalServerError.Type, - InternalServerError.Code).WithTitle(InternalServerError.Title) -} - -// NewNotImplemented creates a new ProblemDetails instance that represents an HTTP 501 Not Implemented. -func NewNotImplemented() *ProblemDetails { - return New(NotImplemented.Type, - NotImplemented.Code).WithTitle(NotImplemented.Title) -} - -// NewBadGateway creates a new ProblemDetails instance that represents an HTTP 502 Bad Gateway. -func NewBadGateway() *ProblemDetails { - return New(BadGateway.Type, - BadGateway.Code).WithTitle(BadGateway.Title) -} - -// NewServiceUnavailable creates a new ProblemDetails instance that represents an HTTP 503 Service Unavailable. -func NewServiceUnavailable() *ProblemDetails { - return New(ServiceUnavailable.Type, - ServiceUnavailable.Code).WithTitle(ServiceUnavailable.Title) -} - -// NewGatewayTimeout creates a new ProblemDetails instance that represents an HTTP 504 Gateway Timeout. -func NewGatewayTimeout() *ProblemDetails { - return New(GatewayTimeout.Type, - GatewayTimeout.Code).WithTitle(GatewayTimeout.Title) -} - -// NewHTTPVersionNotSupported creates a new ProblemDetails instance that represents an HTTP 505 HTTP Version Not Supported. -func NewHTTPVersionNotSupported() *ProblemDetails { - return New(HTTPVersionNotSupported.Type, - HTTPVersionNotSupported.Code).WithTitle(HTTPVersionNotSupported.Title) -} - -// NewVariantAlsoNegotiates creates a new ProblemDetails instance that represents an HTTP 506 Variant Also Negotiates. -func NewVariantAlsoNegotiates() *ProblemDetails { - return New(VariantAlsoNegotiates.Type, - VariantAlsoNegotiates.Code).WithTitle(VariantAlsoNegotiates.Title) -} - -// NewNotExtended creates a new ProblemDetails instance that represents an HTTP 510 Not Extended. -func NewNotExtended() *ProblemDetails { - return New(NotExtended.Type, - NotExtended.Code).WithTitle(NotExtended.Title) -} - -// NewNetworkAuthenticationRequired creates a new ProblemDetails instance that represents an HTTP 511 Network Authentication Required. -func NewNetworkAuthenticationRequired() *ProblemDetails { - return New(NetworkAuthenticationRequired.Type, - NetworkAuthenticationRequired.Code).WithTitle(NetworkAuthenticationRequired.Title) -} - -// NewTeapot creates a new ProblemDetails instance that represents an HTTP 418 I'm a teapot. -func NewTeapot() *ProblemDetails { - return New(Teapot.Type, - Teapot.Code).WithTitle(Teapot.Title) -} - -// NewTooEarly creates a new ProblemDetails instance that represents an HTTP 425 Too Early. -func NewTooEarly() *ProblemDetails { - return New(TooEarly.Type, - TooEarly.Code).WithTitle(TooEarly.Title) -} diff --git a/http/problemdetails/problemdetails_test.go b/http/problemdetails/problemdetails_test.go deleted file mode 100644 index 6107d35..0000000 --- a/http/problemdetails/problemdetails_test.go +++ /dev/null @@ -1,557 +0,0 @@ -package problemdetails - -import ( - "github.com/stretchr/testify/assert" - "strings" - "testing" - "time" -) - -func TestNewInternalServerError(t *testing.T) { - problemDetails := NewInternalServerError() - assert.Equal(t, InternalServerError.Type, problemDetails.Type) - assert.Equal(t, InternalServerError.Code, problemDetails.Status) - assert.Equal(t, InternalServerError.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewNotFound(t *testing.T) { - problemDetails := NewNotFound() - assert.Equal(t, NotFound.Type, problemDetails.Type) - assert.Equal(t, NotFound.Code, problemDetails.Status) - assert.Equal(t, NotFound.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewBadRequest(t *testing.T) { - problemDetails := NewBadRequest() - assert.Equal(t, BadRequest.Type, problemDetails.Type) - assert.Equal(t, BadRequest.Code, problemDetails.Status) - assert.Equal(t, BadRequest.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewContinue(t *testing.T) { - problemDetails := NewContinue() - assert.Equal(t, Continue.Type, problemDetails.Type) - assert.Equal(t, Continue.Code, problemDetails.Status) - assert.Equal(t, Continue.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewSwitchingProtocols(t *testing.T) { - problemDetails := NewSwitchingProtocols() - assert.Equal(t, SwitchingProtocols.Type, problemDetails.Type) - assert.Equal(t, SwitchingProtocols.Code, problemDetails.Status) - assert.Equal(t, SwitchingProtocols.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewOK(t *testing.T) { - problemDetails := NewOK() - assert.Equal(t, OK.Type, problemDetails.Type) - assert.Equal(t, OK.Code, problemDetails.Status) - assert.Equal(t, OK.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewCreated(t *testing.T) { - problemDetails := NewCreated() - assert.Equal(t, Created.Type, problemDetails.Type) - assert.Equal(t, Created.Code, problemDetails.Status) - assert.Equal(t, Created.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewAccepted(t *testing.T) { - problemDetails := NewAccepted() - assert.Equal(t, Accepted.Type, problemDetails.Type) - assert.Equal(t, Accepted.Code, problemDetails.Status) - assert.Equal(t, Accepted.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewNoContent(t *testing.T) { - problemDetails := NewNoContent() - assert.Equal(t, NoContent.Type, problemDetails.Type) - assert.Equal(t, NoContent.Code, problemDetails.Status) - assert.Equal(t, NoContent.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewMovedPermanently(t *testing.T) { - problemDetails := NewMovedPermanently() - assert.Equal(t, MovedPermanently.Type, problemDetails.Type) - assert.Equal(t, MovedPermanently.Code, problemDetails.Status) - assert.Equal(t, MovedPermanently.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewFound(t *testing.T) { - problemDetails := NewFound() - assert.Equal(t, Found.Type, problemDetails.Type) - assert.Equal(t, Found.Code, problemDetails.Status) - assert.Equal(t, Found.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewNotModified(t *testing.T) { - problemDetails := NewNotModified() - assert.Equal(t, NotModified.Type, problemDetails.Type) - assert.Equal(t, NotModified.Code, problemDetails.Status) - assert.Equal(t, NotModified.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewUnauthorized(t *testing.T) { - problemDetails := NewUnauthorized() - assert.Equal(t, Unauthorized.Type, problemDetails.Type) - assert.Equal(t, Unauthorized.Code, problemDetails.Status) - assert.Equal(t, Unauthorized.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewForbidden(t *testing.T) { - problemDetails := NewForbidden() - assert.Equal(t, Forbidden.Type, problemDetails.Type) - assert.Equal(t, Forbidden.Code, problemDetails.Status) - assert.Equal(t, Forbidden.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewMethodNotAllowed(t *testing.T) { - problemDetails := NewMethodNotAllowed() - assert.Equal(t, MethodNotAllowed.Type, problemDetails.Type) - assert.Equal(t, MethodNotAllowed.Code, problemDetails.Status) - assert.Equal(t, MethodNotAllowed.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewConflict(t *testing.T) { - problemDetails := NewConflict() - assert.Equal(t, Conflict.Type, problemDetails.Type) - assert.Equal(t, Conflict.Code, problemDetails.Status) - assert.Equal(t, Conflict.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewNotImplemented(t *testing.T) { - problemDetails := NewNotImplemented() - assert.Equal(t, NotImplemented.Type, problemDetails.Type) - assert.Equal(t, NotImplemented.Code, problemDetails.Status) - assert.Equal(t, NotImplemented.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewBadGateway(t *testing.T) { - problemDetails := NewBadGateway() - assert.Equal(t, BadGateway.Type, problemDetails.Type) - assert.Equal(t, BadGateway.Code, problemDetails.Status) - assert.Equal(t, BadGateway.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewServiceUnavailable(t *testing.T) { - problemDetails := NewServiceUnavailable() - assert.Equal(t, ServiceUnavailable.Type, problemDetails.Type) - assert.Equal(t, ServiceUnavailable.Code, problemDetails.Status) - assert.Equal(t, ServiceUnavailable.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewGatewayTimeout(t *testing.T) { - problemDetails := NewGatewayTimeout() - assert.Equal(t, GatewayTimeout.Type, problemDetails.Type) - assert.Equal(t, GatewayTimeout.Code, problemDetails.Status) - assert.Equal(t, GatewayTimeout.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewTeapot(t *testing.T) { - problemDetails := NewTeapot() - assert.Equal(t, Teapot.Type, problemDetails.Type) - assert.Equal(t, Teapot.Code, problemDetails.Status) - assert.Equal(t, Teapot.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewEarlyHints(t *testing.T) { - problemDetails := NewEarlyHints() - assert.Equal(t, EarlyHints.Type, problemDetails.Type) - assert.Equal(t, EarlyHints.Code, problemDetails.Status) - assert.Equal(t, EarlyHints.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewNonAuthoritativeInfo(t *testing.T) { - problemDetails := NewNonAuthoritativeInfo() - assert.Equal(t, NonAuthoritativeInfo.Type, problemDetails.Type) - assert.Equal(t, NonAuthoritativeInfo.Code, problemDetails.Status) - assert.Equal(t, NonAuthoritativeInfo.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewResetContent(t *testing.T) { - problemDetails := NewResetContent() - assert.Equal(t, ResetContent.Type, problemDetails.Type) - assert.Equal(t, ResetContent.Code, problemDetails.Status) - assert.Equal(t, ResetContent.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewPartialContent(t *testing.T) { - problemDetails := NewPartialContent() - assert.Equal(t, PartialContent.Type, problemDetails.Type) - assert.Equal(t, PartialContent.Code, problemDetails.Status) - assert.Equal(t, PartialContent.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewIMUsed(t *testing.T) { - problemDetails := NewIMUsed() - assert.Equal(t, IMUsed.Type, problemDetails.Type) - assert.Equal(t, IMUsed.Code, problemDetails.Status) - assert.Equal(t, IMUsed.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewMultipleChoices(t *testing.T) { - problemDetails := NewMultipleChoices() - assert.Equal(t, MultipleChoices.Type, problemDetails.Type) - assert.Equal(t, MultipleChoices.Code, problemDetails.Status) - assert.Equal(t, MultipleChoices.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewSeeOther(t *testing.T) { - problemDetails := NewSeeOther() - assert.Equal(t, SeeOther.Type, problemDetails.Type) - assert.Equal(t, SeeOther.Code, problemDetails.Status) - assert.Equal(t, SeeOther.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewUseProxy(t *testing.T) { - problemDetails := NewUseProxy() - assert.Equal(t, UseProxy.Type, problemDetails.Type) - assert.Equal(t, UseProxy.Code, problemDetails.Status) - assert.Equal(t, UseProxy.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewTemporaryRedirect(t *testing.T) { - problemDetails := NewTemporaryRedirect() - assert.Equal(t, TemporaryRedirect.Type, problemDetails.Type) - assert.Equal(t, TemporaryRedirect.Code, problemDetails.Status) - assert.Equal(t, TemporaryRedirect.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewPermanentRedirect(t *testing.T) { - problemDetails := NewPermanentRedirect() - assert.Equal(t, PermanentRedirect.Type, problemDetails.Type) - assert.Equal(t, PermanentRedirect.Code, problemDetails.Status) - assert.Equal(t, PermanentRedirect.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewPaymentRequired(t *testing.T) { - problemDetails := NewPaymentRequired() - assert.Equal(t, PaymentRequired.Type, problemDetails.Type) - assert.Equal(t, PaymentRequired.Code, problemDetails.Status) - assert.Equal(t, PaymentRequired.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewNotAcceptable(t *testing.T) { - problemDetails := NewNotAcceptable() - assert.Equal(t, NotAcceptable.Type, problemDetails.Type) - assert.Equal(t, NotAcceptable.Code, problemDetails.Status) - assert.Equal(t, NotAcceptable.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewProxyAuthenticationRequired(t *testing.T) { - problemDetails := NewProxyAuthenticationRequired() - assert.Equal(t, ProxyAuthenticationRequired.Type, problemDetails.Type) - assert.Equal(t, ProxyAuthenticationRequired.Code, problemDetails.Status) - assert.Equal(t, ProxyAuthenticationRequired.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewRequestTimeout(t *testing.T) { - problemDetails := NewRequestTimeout() - assert.Equal(t, RequestTimeout.Type, problemDetails.Type) - assert.Equal(t, RequestTimeout.Code, problemDetails.Status) - assert.Equal(t, RequestTimeout.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewGone(t *testing.T) { - problemDetails := NewGone() - assert.Equal(t, Gone.Type, problemDetails.Type) - assert.Equal(t, Gone.Code, problemDetails.Status) - assert.Equal(t, Gone.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewLengthRequired(t *testing.T) { - problemDetails := NewLengthRequired() - assert.Equal(t, LengthRequired.Type, problemDetails.Type) - assert.Equal(t, LengthRequired.Code, problemDetails.Status) - assert.Equal(t, LengthRequired.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewPreconditionFailed(t *testing.T) { - problemDetails := NewPreconditionFailed() - assert.Equal(t, PreconditionFailed.Type, problemDetails.Type) - assert.Equal(t, PreconditionFailed.Code, problemDetails.Status) - assert.Equal(t, PreconditionFailed.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewPayloadTooLarge(t *testing.T) { - problemDetails := NewPayloadTooLarge() - assert.Equal(t, PayloadTooLarge.Type, problemDetails.Type) - assert.Equal(t, PayloadTooLarge.Code, problemDetails.Status) - assert.Equal(t, PayloadTooLarge.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewURITooLong(t *testing.T) { - problemDetails := NewURITooLong() - assert.Equal(t, URITooLong.Type, problemDetails.Type) - assert.Equal(t, URITooLong.Code, problemDetails.Status) - assert.Equal(t, URITooLong.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewUnsupportedMediaType(t *testing.T) { - problemDetails := NewUnsupportedMediaType() - assert.Equal(t, UnsupportedMediaType.Type, problemDetails.Type) - assert.Equal(t, UnsupportedMediaType.Code, problemDetails.Status) - assert.Equal(t, UnsupportedMediaType.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewRangeNotSatisfiable(t *testing.T) { - problemDetails := NewRangeNotSatisfiable() - assert.Equal(t, RangeNotSatisfiable.Type, problemDetails.Type) - assert.Equal(t, RangeNotSatisfiable.Code, problemDetails.Status) - assert.Equal(t, RangeNotSatisfiable.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewExpectationFailed(t *testing.T) { - problemDetails := NewExpectationFailed() - assert.Equal(t, ExpectationFailed.Type, problemDetails.Type) - assert.Equal(t, ExpectationFailed.Code, problemDetails.Status) - assert.Equal(t, ExpectationFailed.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewMisdirectedRequest(t *testing.T) { - problemDetails := NewMisdirectedRequest() - assert.Equal(t, MisdirectedRequest.Type, problemDetails.Type) - assert.Equal(t, MisdirectedRequest.Code, problemDetails.Status) - assert.Equal(t, MisdirectedRequest.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewUpgradeRequired(t *testing.T) { - problemDetails := NewUpgradeRequired() - assert.Equal(t, UpgradeRequired.Type, problemDetails.Type) - assert.Equal(t, UpgradeRequired.Code, problemDetails.Status) - assert.Equal(t, UpgradeRequired.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewPreconditionRequired(t *testing.T) { - problemDetails := NewPreconditionRequired() - assert.Equal(t, PreconditionRequired.Type, problemDetails.Type) - assert.Equal(t, PreconditionRequired.Code, problemDetails.Status) - assert.Equal(t, PreconditionRequired.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewTooManyRequests(t *testing.T) { - problemDetails := NewTooManyRequests() - assert.Equal(t, TooManyRequests.Type, problemDetails.Type) - assert.Equal(t, TooManyRequests.Code, problemDetails.Status) - assert.Equal(t, TooManyRequests.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewRequestHeaderFieldsTooLarge(t *testing.T) { - problemDetails := NewRequestHeaderFieldsTooLarge() - assert.Equal(t, RequestHeaderFieldsTooLarge.Type, problemDetails.Type) - assert.Equal(t, RequestHeaderFieldsTooLarge.Code, problemDetails.Status) - assert.Equal(t, RequestHeaderFieldsTooLarge.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewUnavailableForLegalReasons(t *testing.T) { - problemDetails := NewUnavailableForLegalReasons() - assert.Equal(t, UnavailableForLegalReasons.Type, problemDetails.Type) - assert.Equal(t, UnavailableForLegalReasons.Code, problemDetails.Status) - assert.Equal(t, UnavailableForLegalReasons.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewHTTPVersionNotSupported(t *testing.T) { - problemDetails := NewHTTPVersionNotSupported() - assert.Equal(t, HTTPVersionNotSupported.Type, problemDetails.Type) - assert.Equal(t, HTTPVersionNotSupported.Code, problemDetails.Status) - assert.Equal(t, HTTPVersionNotSupported.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewVariantAlsoNegotiates(t *testing.T) { - problemDetails := NewVariantAlsoNegotiates() - assert.Equal(t, VariantAlsoNegotiates.Type, problemDetails.Type) - assert.Equal(t, VariantAlsoNegotiates.Code, problemDetails.Status) - assert.Equal(t, VariantAlsoNegotiates.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewNotExtended(t *testing.T) { - problemDetails := NewNotExtended() - assert.Equal(t, NotExtended.Type, problemDetails.Type) - assert.Equal(t, NotExtended.Code, problemDetails.Status) - assert.Equal(t, NotExtended.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewNetworkAuthenticationRequired(t *testing.T) { - problemDetails := NewNetworkAuthenticationRequired() - assert.Equal(t, NetworkAuthenticationRequired.Type, problemDetails.Type) - assert.Equal(t, NetworkAuthenticationRequired.Code, problemDetails.Status) - assert.Equal(t, NetworkAuthenticationRequired.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - -func TestNewTooEarly(t *testing.T) { - problemDetails := NewTooEarly() - assert.Equal(t, TooEarly.Type, problemDetails.Type) - assert.Equal(t, TooEarly.Code, problemDetails.Status) - assert.Equal(t, TooEarly.Title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} -func doCommonAssertions(t *testing.T, problemDetails *ProblemDetails) { - assert.Equal(t, true, strings.Contains(problemDetails.Instance, uuidUrnPrefix)) - - var createdTime time.Time - var err error - createdTime, err = time.Parse(time.RFC3339Nano, problemDetails.Created) - assert.Nil(t, err) - assert.NotNil(t, createdTime) -} diff --git a/http/problemdetails/problemtype.go b/http/problemdetails/problemtype.go deleted file mode 100644 index 8142bc7..0000000 --- a/http/problemdetails/problemtype.go +++ /dev/null @@ -1,445 +0,0 @@ -package problemdetails - -import "net/http" - -// ProblemType represents a problem type as defined in RFC 9457. This struct encapsulates the details of different types of HTTP problems, such as their type URIs, status codes, and titles. This struct would help in organizing and reusing these details across the codebase. -type ProblemType struct { - Code int - Type string - Title string - Detail string -} - -var ( - // Continue (HTTP 100) indicates that the initial part of a request has been received and has not yet been rejected by the server. - Continue = ProblemType{ - Code: http.StatusContinue, - Type: "urn:problems:continue", - Title: "Continue", - Detail: "The server has received the request headers and the client should proceed to send the request body.", - } - - // SwitchingProtocols (HTTP 101) indicates that the server is willing to switch protocols as requested by the client. - SwitchingProtocols = ProblemType{ - Code: http.StatusSwitchingProtocols, - Type: "urn:problems:switching-protocols", - Title: "Switching Protocols", - Detail: "The server is switching protocols according to the Upgrade header.", - } - - // EarlyHints (HTTP 103) indicates that the server is likely to send a final response with the header fields included in the informational response. - EarlyHints = ProblemType{ - Code: http.StatusEarlyHints, - Type: "urn:problems:early-hints", - Title: "Early Hints", - Detail: "The server is likely to send a final response with the header fields included in the informational response.", - } - - // OK (HTTP 200) indicates that the request has succeeded. - OK = ProblemType{ - Code: http.StatusOK, - Type: "urn:problems:ok", - Title: "OK", - Detail: "The request has succeeded.", - } - - // Created (HTTP 201) indicates that the request has been fulfilled and has resulted in one or more new resources being created. - Created = ProblemType{ - Code: http.StatusCreated, - Type: "urn:problems:created", - Title: "Created", - Detail: "The request has been fulfilled and has resulted in one or more new resources being created.", - } - - // Accepted (HTTP 202) indicates that the request has been accepted for processing, but the processing has not been completed. - Accepted = ProblemType{ - Code: http.StatusAccepted, - Type: "urn:problems:accepted", - Title: "Accepted", - Detail: "The request has been accepted for processing, but the processing has not been completed.", - } - - // NonAuthoritativeInfo (HTTP 203) indicates that the request was successful but the enclosed payload has been modified from that of the origin server's 200 (OK) response by a transforming proxy. - NonAuthoritativeInfo = ProblemType{ - Code: http.StatusNonAuthoritativeInfo, - Type: "urn:problems:non-authoritative-info", - Title: "Non-Authoritative Information", - Detail: "The request was successful but the enclosed payload has been modified from that of the origin server's 200 (OK) response by a transforming proxy.", - } - - // NoContent (HTTP 204) indicates that the server has successfully fulfilled the request and that there is no additional content to send in the response payload body. - NoContent = ProblemType{ - Code: http.StatusNoContent, - Type: "urn:problems:no-content", - Title: "No Content", - Detail: "The server has successfully fulfilled the request and that there is no additional content to send in the response payload body.", - } - - // ResetContent (HTTP 205) indicates that the server has fulfilled the request and desires that the user agent reset the 'document view' which caused the request to be sent. - ResetContent = ProblemType{ - Code: http.StatusResetContent, - Type: "urn:problems:reset-content", - Title: "Reset Content", - Detail: "The server has fulfilled the request and desires that the user agent reset the 'document view' which caused the request to be sent.", - } - - // PartialContent (HTTP 206) indicates that the server is successfully fulfilling a range request for the target resource by transferring one or more parts of the selected representation. - PartialContent = ProblemType{ - Code: http.StatusPartialContent, - Type: "urn:problems:partial-content", - Title: "Partial Content", - Detail: "The server is successfully fulfilling a range request for the target resource by transferring one or more parts of the selected representation.", - } - - // IMUsed (HTTP 226) indicates that the server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance. - IMUsed = ProblemType{ - Code: http.StatusIMUsed, - Type: "urn:problems:im-used", - Title: "IM Used", - Detail: "The server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.", - } - - // MultipleChoices (HTTP 300) indicates that the request has more than one possible response. - MultipleChoices = ProblemType{ - Code: http.StatusMultipleChoices, - Type: "urn:problems:multiple-choices", - Title: "Multiple Choices", - Detail: "The request has more than one possible response.", - } - - // MovedPermanently (HTTP 301) indicates that the requested resource has been assigned a new permanent URI. - MovedPermanently = ProblemType{ - Code: http.StatusMovedPermanently, - Type: "urn:problems:moved-permanently", - Title: "Moved Permanently", - Detail: "The requested resource has been assigned a new permanent URI.", - } - - // Found (HTTP 302) indicates that the requested resource resides temporarily under a different URI. - Found = ProblemType{ - Code: http.StatusFound, - Type: "urn:problems:found", - Title: "Found", - Detail: "The requested resource resides temporarily under a different URI.", - } - - // SeeOther (HTTP 303) indicates that the response to the request can be found under another URI using a GET method. - SeeOther = ProblemType{ - Code: http.StatusSeeOther, - Type: "urn:problems:see-other", - Title: "See Other", - Detail: "The response to the request can be found under another URI using a GET method.", - } - - // NotModified (HTTP 304) indicates that the resource has not been modified since the version specified by the request headers. - NotModified = ProblemType{ - Code: http.StatusNotModified, - Type: "urn:problems:not-modified", - Title: "Not Modified", - Detail: "The resource has not been modified since the version specified by the request headers.", - } - - // UseProxy (HTTP 305) indicates that the requested resource is available only through a proxy. - UseProxy = ProblemType{ - Code: http.StatusUseProxy, - Type: "urn:problems:use-proxy", - Title: "Use Proxy", - Detail: "The requested resource is available only through a proxy.", - } - - // TemporaryRedirect (HTTP 307) indicates that the request should be repeated with another URI, but future requests can still use the original URI. - TemporaryRedirect = ProblemType{ - Code: http.StatusTemporaryRedirect, - Type: "urn:problems:temporary-redirect", - Title: "Temporary Redirect", - Detail: "The request should be repeated with another URI, but future requests can still use the original URI.", - } - - // PermanentRedirect (HTTP 308) indicates that the request and all future requests should be repeated using another URI. - PermanentRedirect = ProblemType{ - Code: http.StatusPermanentRedirect, - Type: "urn:problems:permanent-redirect", - Title: "Permanent Redirect", - Detail: "The request and all future requests should be repeated using another URI.", - } - - // BadRequest (HTTP 400) indicates that the provided request could not be processed because it is invalid. - BadRequest = ProblemType{ - Code: http.StatusBadRequest, - Type: "urn:problems:bad-request", - Title: "Bad Request", - Detail: "The provided request could not be processed because it is invalid.", - } - - // Unauthorized (HTTP 401) indicates that the request requires user authentication. - Unauthorized = ProblemType{ - Code: http.StatusUnauthorized, - Type: "urn:problems:unauthorized", - Title: "Unauthorized", - Detail: "The request requires user authentication.", - } - - // PaymentRequired (HTTP 402) indicates that the request requires payment before it can be processed. - PaymentRequired = ProblemType{ - Code: http.StatusPaymentRequired, - Type: "urn:problems:payment-required", - Title: "Payment Required", - Detail: "The request requires payment before it can be processed.", - } - - // Forbidden (HTTP 403) indicates that the server understood the request, but refuses to authorize it. - Forbidden = ProblemType{ - Code: http.StatusForbidden, - Type: "urn:problems:forbidden", - Title: "Forbidden", - Detail: "The server understood the request, but refuses to authorize it.", - } - - // NotFound (HTTP 404) indicates that the specified resource could not be found. - NotFound = ProblemType{ - Code: http.StatusNotFound, - Type: "urn:problems:not-found", - Title: "Not Found", - Detail: "The specified resource could not be found.", - } - - // MethodNotAllowed (HTTP 405) indicates that the method specified in the request is not allowed for the resource. - MethodNotAllowed = ProblemType{ - Code: http.StatusMethodNotAllowed, - Type: "urn:problems:method-not-allowed", - Title: "Method Not Allowed", - Detail: "The method specified in the request is not allowed for the resource.", - } - - // NotAcceptable (HTTP 406) indicates that the resource is not capable of generating content acceptable to the Accept headers. - NotAcceptable = ProblemType{ - Code: http.StatusNotAcceptable, - Type: "urn:problems:not-acceptable", - Title: "Not Acceptable", - Detail: "The resource is not capable of generating content acceptable to the Accept headers.", - } - - // ProxyAuthenticationRequired (HTTP 407) indicates that the client must first authenticate itself with the proxy. - ProxyAuthenticationRequired = ProblemType{ - Code: http.StatusProxyAuthRequired, - Type: "urn:problems:proxy-authentication-required", - Title: "Proxy Authentication Required", - Detail: "The client must first authenticate itself with the proxy.", - } - - // RequestTimeout (HTTP 408) indicates that the server timed out waiting for the request. - RequestTimeout = ProblemType{ - Code: http.StatusRequestTimeout, - Type: "urn:problems:request-timeout", - Title: "Request Timeout", - Detail: "The server timed out waiting for the request.", - } - - // Conflict (HTTP 409) indicates that the request could not be completed due to a conflict with the current state of the resource. - Conflict = ProblemType{ - Code: http.StatusConflict, - Type: "urn:problems:conflict", - Title: "Conflict", - Detail: "The request could not be completed due to a conflict with the current state of the resource.", - } - - // Gone (HTTP 410) indicates that the resource requested is no longer available and will not be available again. - Gone = ProblemType{ - Code: http.StatusGone, - Type: "urn:problems:gone", - Title: "Gone", - Detail: "The resource requested is no longer available and will not be available again.", - } - - // LengthRequired (HTTP 411) indicates that the request did not specify the length of its content, which is required by the requested resource. - LengthRequired = ProblemType{ - Code: http.StatusLengthRequired, - Type: "urn:problems:length-required", - Title: "Length Required", - Detail: "The request did not specify the length of its content, which is required by the requested resource.", - } - - // PreconditionFailed (HTTP 412) indicates that the server does not meet one of the preconditions that the requester put on the request. - PreconditionFailed = ProblemType{ - Code: http.StatusPreconditionFailed, - Type: "urn:problems:precondition-failed", - Title: "Precondition Failed", - Detail: "The server does not meet one of the preconditions that the requester put on the request.", - } - - // PayloadTooLarge (HTTP 413) indicates that the request is larger than the server is willing or able to process. - PayloadTooLarge = ProblemType{ - Code: http.StatusRequestEntityTooLarge, - Type: "urn:problems:payload-too-large", - Title: "Payload Too Large", - Detail: "The request is larger than the server is willing or able to process.", - } - - // URITooLong (HTTP 414) indicates that the URI provided was too long for the server to process. - URITooLong = ProblemType{ - Code: http.StatusRequestURITooLong, - Type: "urn:problems:uri-too-long", - Title: "URI Too Long", - Detail: "The URI provided was too long for the server to process.", - } - - // UnsupportedMediaType (HTTP 415) indicates that the request entity has a media type which the server or resource does not support. - UnsupportedMediaType = ProblemType{ - Code: http.StatusUnsupportedMediaType, - Type: "urn:problems:unsupported-media-type", - Title: "Unsupported Media Type", - Detail: "The request entity has a media type which the server or resource does not support.", - } - - // RangeNotSatisfiable (HTTP 416) indicates that the client has asked for a portion of the file, but the server cannot supply that portion. - RangeNotSatisfiable = ProblemType{ - Code: http.StatusRequestedRangeNotSatisfiable, - Type: "urn:problems:range-not-satisfiable", - Title: "Range Not Satisfiable", - Detail: "The client has asked for a portion of the file, but the server cannot supply that portion.", - } - - // ExpectationFailed (HTTP 417) indicates that the server cannot meet the requirements of the Expect request-header field. - ExpectationFailed = ProblemType{ - Code: http.StatusExpectationFailed, - Type: "urn:problems:expectation-failed", - Title: "Expectation Failed", - Detail: "The server cannot meet the requirements of the Expect request-header field.", - } - - // Teapot (HTTP 418) indicates that the server is a teapot and cannot brew coffee. - Teapot = ProblemType{ - Code: http.StatusTeapot, - Type: "urn:problems:teapot", - Title: "I'm a teapot", - Detail: "I'm a teapot.", - } - - // MisdirectedRequest (HTTP 421) indicates that the request was directed at a server that is not able to produce a response. - MisdirectedRequest = ProblemType{ - Code: http.StatusMisdirectedRequest, - Type: "urn:problems:misdirected-request", - Title: "Misdirected Request", - Detail: "The request was directed at a server that is not able to produce a response.", - } - - // TooEarly (HTTP 425) indicates that the server is unwilling to risk processing a request that might be replayed. - TooEarly = ProblemType{ - Code: http.StatusTooEarly, - Type: "urn:problems:too-early", - Title: "Too Early", - Detail: "The server is unwilling to risk processing a request that might be replayed.", - } - - // UpgradeRequired (HTTP 426) indicates that the client should switch to a different protocol. - UpgradeRequired = ProblemType{ - Code: http.StatusUpgradeRequired, - Type: "urn:problems:upgrade-required", - Title: "Upgrade Required", - Detail: "The client should switch to a different protocol.", - } - - // PreconditionRequired (HTTP 428) indicates that the server requires the request to be conditional. - PreconditionRequired = ProblemType{ - Code: http.StatusPreconditionRequired, - Type: "urn:problems:precondition-required", - Title: "Precondition Required", - Detail: "The server requires the request to be conditional.", - } - - // TooManyRequests (HTTP 429) indicates that the user has sent too many requests in a given amount of time. - TooManyRequests = ProblemType{ - Code: http.StatusTooManyRequests, - Type: "urn:problems:too-many-requests", - Title: "Too Many Requests", - Detail: "The user has sent too many requests in a given amount of time.", - } - - // RequestHeaderFieldsTooLarge (HTTP 431) indicates that the server is unwilling to process the request because its header fields are too large. - RequestHeaderFieldsTooLarge = ProblemType{ - Code: http.StatusRequestHeaderFieldsTooLarge, - Type: "urn:problems:request-header-fields-too-large", - Title: "Request Header Fields Too Large", - Detail: "The server is unwilling to process the request because its header fields are too large.", - } - - // UnavailableForLegalReasons (HTTP 451) indicates that the resource is unavailable for legal reasons. - UnavailableForLegalReasons = ProblemType{ - Code: http.StatusUnavailableForLegalReasons, - Type: "urn:problems:unavailable-for-legal-reasons", - Title: "Unavailable For Legal Reasons", - Detail: "The resource is unavailable for legal reasons.", - } - - // InternalServerError (HTTP 500) indicates that the server has encountered a situation it doesn't know how to handle. - InternalServerError = ProblemType{ - Code: http.StatusInternalServerError, - Type: "urn:problems:internal-server-error", - Title: "Internal Server Error", - Detail: "The server has encountered a situation it doesn't know how to handle.", - } - - // NotImplemented (HTTP 501) indicates that the request method is not supported by the server and cannot be handled. - NotImplemented = ProblemType{ - Code: http.StatusNotImplemented, - Type: "urn:problems:not-implemented", - Title: "Not Implemented", - Detail: "The request method is not supported by the server and cannot be handled.", - } - - // BadGateway (HTTP 502) indicates that the server, while acting as a gateway or proxy, received an invalid response from the upstream server. - BadGateway = ProblemType{ - Code: http.StatusBadGateway, - Type: "urn:problems:bad-gateway", - Title: "Bad Gateway", - Detail: "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.", - } - - // ServiceUnavailable (HTTP 503) indicates that the server is not ready to handle the request, often due to maintenance or overload. - ServiceUnavailable = ProblemType{ - Code: http.StatusServiceUnavailable, - Type: "urn:problems:service-unavailable", - Title: "Service Unavailable", - Detail: "The server is not ready to handle the request, often due to maintenance or overload.", - } - - // GatewayTimeout (HTTP 504) indicates that the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server. - GatewayTimeout = ProblemType{ - Code: http.StatusGatewayTimeout, - Type: "urn:problems:gateway-timeout", - Title: "Gateway Timeout", - Detail: "The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.", - } - - // HTTPVersionNotSupported (HTTP 505) indicates that the server does not support the HTTP protocol version used in the request. - HTTPVersionNotSupported = ProblemType{ - Code: http.StatusHTTPVersionNotSupported, - Type: "urn:problems:http-version-not-supported", - Title: "HTTP Version Not Supported", - Detail: "The server does not support the HTTP protocol version used in the request.", - } - - // VariantAlsoNegotiates (HTTP 506) indicates that the server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process. - VariantAlsoNegotiates = ProblemType{ - Code: http.StatusVariantAlsoNegotiates, - Type: "urn:problems:variant-also-negotiates", - Title: "Variant Also Negotiates", - Detail: "The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.", - } - - // NotExtended (HTTP 510) indicates that further extensions to the request are required for the server to fulfill it. - NotExtended = ProblemType{ - Code: http.StatusNotExtended, - Type: "urn:problems:not-extended", - Title: "Not Extended", - Detail: "Further extensions to the request are required for the server to fulfill it.", - } - - // NetworkAuthenticationRequired (HTTP 511) indicates that the client needs to authenticate to gain network access. - NetworkAuthenticationRequired = ProblemType{ - Code: http.StatusNetworkAuthenticationRequired, - Type: "urn:problems:network-authentication-required", - Title: "Network Authentication Required", - Detail: "The client needs to authenticate to gain network access.", - } -) diff --git a/internal/examples/advanced-example.go b/internal/examples/advanced-example.go new file mode 100644 index 0000000..802d9be --- /dev/null +++ b/internal/examples/advanced-example.go @@ -0,0 +1,25 @@ +package main + +import ( + "errors" + "github.com/Evernorth/problemdetails" + "net/http" +) + +func getSomethingAdvanced(httpRespWriter http.ResponseWriter, httpReq *http.Request) { + + err := doSomethingAdvanced() + if err != nil { + httpRespWriter.WriteHeader(http.StatusInternalServerError) + render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError().WithTitle("KABOOM!!!").WithDetail("The unthinkable has occurred.").WithExtension( + "example1", "test").WithExtension( + "example2", "this could be a struct if you like")) + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + + return + } +} + +func doSomethingAdvanced() error { + return errors.New("look who stepped in the room") +} diff --git a/internal/examples/basic-example.go b/internal/examples/basic-example.go new file mode 100644 index 0000000..7463f55 --- /dev/null +++ b/internal/examples/basic-example.go @@ -0,0 +1,23 @@ +package main + +import ( + "errors" + "github.com/Evernorth/problemdetails" + "github.com/go-chi/render" + "net/http" +) + +func getSomethingBasic(httpRespWriter http.ResponseWriter, httpReq *http.Request) { + + err := doSomethingBasic() + if err != nil { + httpRespWriter.WriteHeader(http.StatusInternalServerError) + render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + return + } +} + +func doSomethingBasic() error { + return errors.New("look who stepped in the room") +} diff --git a/internal/examples/go.mod b/internal/examples/go.mod new file mode 100644 index 0000000..ee17002 --- /dev/null +++ b/internal/examples/go.mod @@ -0,0 +1,3 @@ +module github.com/Evernorth/problemdetails/examples + +go 1.23.1 diff --git a/internal/examples/problemdetails-example.go b/internal/examples/problemdetails-example.go deleted file mode 100644 index 8a73a9e..0000000 --- a/internal/examples/problemdetails-example.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "fmt" - "github.com/Evernorth/http-problemdetails-go/http/problemdetails" -) - -func main() { - // Example of creating a ProblemDetails instance for HTTP 500 Internal Server Error - internalServerError := problemdetails.NewInternalServerError() - fmt.Printf("Type: %s, Status: %d, Title: %s\n", internalServerError.Type, internalServerError.Status, internalServerError.Title) - - // Example of creating a ProblemDetails instance for HTTP 400 Bad Request - badRequest := problemdetails.NewBadRequest().WithTitle("This is bad").WithDetail("This is a very bad request") - fmt.Printf("Type: %s, Status: %d, Title: %s, Detail: %s\n", badRequest.Type, badRequest.Status, badRequest.Title, badRequest.Detail) - - // Example of creating a ProblemDetails instance for HTTP 404 Not Found - notFound := problemdetails.NewNotFound() - fmt.Printf("Type: %s, Status: %d, Title: %s\n", notFound.Type, notFound.Status, notFound.Title) - - // Example of creating a ProblemDetails instance for HTTP 418 I'm a teapot - teapot := problemdetails.NewTeapot().WithDetail(problemdetails.Teapot.Detail) - fmt.Printf("Type: %s, Status: %d, Title: %s\n", teapot.Type, teapot.Status, teapot.Title) - -} diff --git a/problemdetails.go b/problemdetails.go new file mode 100644 index 0000000..bb4cd4e --- /dev/null +++ b/problemdetails.go @@ -0,0 +1,123 @@ +package problemdetails + +import ( + "github.com/google/uuid" + "time" +) + +const ( + uuidUrnPrefix = "urn:uuid:" + MimeType = "application/problem+json" +) + +// ProblemDetails represents a HTTP Problem Details structure from https://www.rfc-editor.org/rfc/rfc9457.html +type ProblemDetails struct { + Type string `json:"type"` + Status int `json:"status"` + Title string `json:"title,omitempty"` + Detail string `json:"detail,omitempty"` + Instance string `json:"instance"` + Created string `json:"created"` + Extensions map[string]any `json:"extensions,omitempty"` +} + +// WithTitle sets a custom Title value on the ProblemDetails object. The ProblemDetails pointer is +// returned only for developer convenience. +func (p *ProblemDetails) WithTitle(title string) *ProblemDetails { + p.Title = title + return p +} + +// WithDetail sets a custom Detail value on the ProblemDetails object. The ProblemDetails pointer is +// returned only for developer convenience. +func (p *ProblemDetails) WithDetail(detail string) *ProblemDetails { + p.Detail = detail + return p +} + +// WithExtension adds a custom Extension value on the ProblemDetails object. The ProblemDetails pointer is +// returned only for developer convenience. +func (p *ProblemDetails) WithExtension(name string, value any) *ProblemDetails { + if p.Extensions == nil { + p.Extensions = make(map[string]any) + } + p.Extensions[name] = value + return p //&p recreates pointer? +} + +// New creates a new ProblemDetails instance. Do not use this function unless you are creating a +// custom ProblemDetails instance. Prefer using the functions that create standard ProblemDetails instances. +func New(typeUri string, status int) *ProblemDetails { + return &ProblemDetails{ + Type: typeUri, + Status: status, + Instance: uuidUrnPrefix + uuid.NewString(), + Created: time.Now().UTC().Format(time.RFC3339Nano), + } +} + +// NewBadRequest creates a new ProblemDetails instance that represents an HTTP 400 Bad Request. +func NewBadRequest() *ProblemDetails { + return New(BadRequest.urn, + BadRequest.code).WithTitle(BadRequest.title) +} + +// NewUnauthorized creates a new ProblemDetails instance that represents an HTTP 401 Unauthorized. +func NewUnauthorized() *ProblemDetails { + return New(Unauthorized.urn, + Unauthorized.code).WithTitle(Unauthorized.title) +} + +// NewForbidden creates a new ProblemDetails instance that represents an HTTP 403 Forbidden. +func NewForbidden() *ProblemDetails { + return New(Forbidden.urn, + Forbidden.code).WithTitle(Forbidden.title) +} + +// NewNotFound creates a new ProblemDetails instance that represents an HTTP 404 Not Found. +func NewNotFound() *ProblemDetails { + return New(NotFound.urn, + NotFound.code).WithTitle(NotFound.title) +} + +// NewConflict creates a new ProblemDetails instance that represents an HTTP 409 Conflict. +func NewConflict() *ProblemDetails { + return New(Conflict.urn, + Conflict.code).WithTitle(Conflict.title) +} + +// NewTooManyRequests creates a new ProblemDetails instance that represents an HTTP 429 Too Many Requests. +func NewTooManyRequests() *ProblemDetails { + return New(TooManyRequests.urn, + TooManyRequests.code).WithTitle(TooManyRequests.title) +} + +// NewInternalServerError creates a new ProblemDetails instance that represents an HTTP 500 Internal Server Error. +func NewInternalServerError() *ProblemDetails { + return New(InternalServerError.urn, + InternalServerError.code).WithTitle(InternalServerError.title) +} + +// NewNotImplemented creates a new ProblemDetails instance that represents an HTTP 501 Not Implemented. +func NewNotImplemented() *ProblemDetails { + return New(NotImplemented.urn, + NotImplemented.code).WithTitle(NotImplemented.title) +} + +// NewBadGateway creates a new ProblemDetails instance that represents an HTTP 502 Bad Gateway. +func NewBadGateway() *ProblemDetails { + return New(BadGateway.urn, + BadGateway.code).WithTitle(BadGateway.title) +} + +// NewServiceUnavailable creates a new ProblemDetails instance that represents an HTTP 503 Service Unavailable. +func NewServiceUnavailable() *ProblemDetails { + return New(ServiceUnavailable.urn, + ServiceUnavailable.code).WithTitle(ServiceUnavailable.title) +} + +// NewGatewayTimeout creates a new ProblemDetails instance that represents an HTTP 504 Gateway Timeout. +func NewGatewayTimeout() *ProblemDetails { + return New(GatewayTimeout.urn, + GatewayTimeout.code).WithTitle(GatewayTimeout.title) +} diff --git a/problemdetails_test.go b/problemdetails_test.go new file mode 100644 index 0000000..ef2bbf7 --- /dev/null +++ b/problemdetails_test.go @@ -0,0 +1,128 @@ +package problemdetails + +import ( + "github.com/stretchr/testify/assert" + "strings" + "testing" + "time" +) + +func TestNewInternalServerError(t *testing.T) { + problemDetails := NewInternalServerError() + assert.Equal(t, InternalServerError.urn, problemDetails.Type) + assert.Equal(t, InternalServerError.code, problemDetails.Status) + assert.Equal(t, InternalServerError.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNotFound(t *testing.T) { + problemDetails := NewNotFound() + assert.Equal(t, NotFound.urn, problemDetails.Type) + assert.Equal(t, NotFound.code, problemDetails.Status) + assert.Equal(t, NotFound.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewBadRequest(t *testing.T) { + problemDetails := NewBadRequest() + assert.Equal(t, BadRequest.urn, problemDetails.Type) + assert.Equal(t, BadRequest.code, problemDetails.Status) + assert.Equal(t, BadRequest.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewUnauthorized(t *testing.T) { + problemDetails := NewUnauthorized() + assert.Equal(t, Unauthorized.urn, problemDetails.Type) + assert.Equal(t, Unauthorized.code, problemDetails.Status) + assert.Equal(t, Unauthorized.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewForbidden(t *testing.T) { + problemDetails := NewForbidden() + assert.Equal(t, Forbidden.urn, problemDetails.Type) + assert.Equal(t, Forbidden.code, problemDetails.Status) + assert.Equal(t, Forbidden.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewConflict(t *testing.T) { + problemDetails := NewConflict() + assert.Equal(t, Conflict.urn, problemDetails.Type) + assert.Equal(t, Conflict.code, problemDetails.Status) + assert.Equal(t, Conflict.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewNotImplemented(t *testing.T) { + problemDetails := NewNotImplemented() + assert.Equal(t, NotImplemented.urn, problemDetails.Type) + assert.Equal(t, NotImplemented.code, problemDetails.Status) + assert.Equal(t, NotImplemented.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewBadGateway(t *testing.T) { + problemDetails := NewBadGateway() + assert.Equal(t, BadGateway.urn, problemDetails.Type) + assert.Equal(t, BadGateway.code, problemDetails.Status) + assert.Equal(t, BadGateway.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewServiceUnavailable(t *testing.T) { + problemDetails := NewServiceUnavailable() + assert.Equal(t, ServiceUnavailable.urn, problemDetails.Type) + assert.Equal(t, ServiceUnavailable.code, problemDetails.Status) + assert.Equal(t, ServiceUnavailable.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewGatewayTimeout(t *testing.T) { + problemDetails := NewGatewayTimeout() + assert.Equal(t, GatewayTimeout.urn, problemDetails.Type) + assert.Equal(t, GatewayTimeout.code, problemDetails.Status) + assert.Equal(t, GatewayTimeout.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func TestNewTooManyRequests(t *testing.T) { + problemDetails := NewTooManyRequests() + assert.Equal(t, TooManyRequests.urn, problemDetails.Type) + assert.Equal(t, TooManyRequests.code, problemDetails.Status) + assert.Equal(t, TooManyRequests.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + +func doCommonAssertions(t *testing.T, problemDetails *ProblemDetails) { + assert.Equal(t, true, strings.Contains(problemDetails.Instance, uuidUrnPrefix)) + + var createdTime time.Time + var err error + createdTime, err = time.Parse(time.RFC3339Nano, problemDetails.Created) + assert.Nil(t, err) + assert.NotNil(t, createdTime) +} diff --git a/problemtype.go b/problemtype.go new file mode 100644 index 0000000..c537fb2 --- /dev/null +++ b/problemtype.go @@ -0,0 +1,90 @@ +package problemdetails + +import "net/http" + +// ProblemType represents a problem type as defined in RFC 9457. This struct encapsulates the details of different types of HTTP problems, such as their type URIs, status codes, and titles. This struct would help in organizing and reusing these details across the codebase. +type ProblemType struct { + code int + urn string + title string +} + +var ( + + // BadRequest (HTTP 400) indicates that the provided request could not be processed because it is invalid. + BadRequest = ProblemType{ + code: http.StatusBadRequest, + urn: "urn:problems:bad-request", + title: "The provided request could not be processed because it is invalid.", + } + + // Unauthorized (HTTP 401) indicates that the request requires user authentication. + Unauthorized = ProblemType{ + code: http.StatusUnauthorized, + urn: "urn:problems:unauthorized", + title: "The request requires user authentication.", + } + + // Forbidden (HTTP 403) indicates that the server understood the request, but refuses to authorize it. + Forbidden = ProblemType{ + code: http.StatusForbidden, + urn: "urn:problems:forbidden", + title: "The server understood the request, but refuses to authorize it.", + } + + // NotFound (HTTP 404) indicates that the specified resource could not be found. + NotFound = ProblemType{ + code: http.StatusNotFound, + urn: "urn:problems:not-found", + title: "The specified resource could not be found.", + } + + // Conflict (HTTP 409) indicates that the request could not be completed due to a conflict with the current state of the resource. + Conflict = ProblemType{ + code: http.StatusConflict, + urn: "urn:problems:conflict", + title: "The request could not be completed due to a conflict with the current state of the resource.", + } + + // TooManyRequests (HTTP 429) indicates that the user has sent too many requests in a given amount of time. + TooManyRequests = ProblemType{ + code: http.StatusTooManyRequests, + urn: "urn:problems:too-many-requests", + title: "The user has sent too many requests in a given amount of time.", + } + + // InternalServerError (HTTP 500) indicates that the server has encountered a situation it doesn't know how to handle. + InternalServerError = ProblemType{ + code: http.StatusInternalServerError, + urn: "urn:problems:internal-server-error", + title: "The server has encountered a situation it doesn't know how to handle.", + } + + // NotImplemented (HTTP 501) indicates that the request method is not supported by the server and cannot be handled. + NotImplemented = ProblemType{ + code: http.StatusNotImplemented, + urn: "urn:problems:not-implemented", + title: "The request method is not supported by the server and cannot be handled.", + } + + // BadGateway (HTTP 502) indicates that the server, while acting as a gateway or proxy, received an invalid response from the upstream server. + BadGateway = ProblemType{ + code: http.StatusBadGateway, + urn: "urn:problems:bad-gateway", + title: "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.", + } + + // ServiceUnavailable (HTTP 503) indicates that the server is not ready to handle the request, often due to maintenance or overload. + ServiceUnavailable = ProblemType{ + code: http.StatusServiceUnavailable, + urn: "urn:problems:service-unavailable", + title: "The server is not ready to handle the request, often due to maintenance or overload.", + } + + // GatewayTimeout (HTTP 504) indicates that the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server. + GatewayTimeout = ProblemType{ + code: http.StatusGatewayTimeout, + urn: "urn:problems:gateway-timeout", + title: "The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.", + } +) From a70e4711483dbec01c67f15420b0a9ff35f52b7b Mon Sep 17 00:00:00 2001 From: stevensefton <176721477+stevensefton@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:17:50 -0500 Subject: [PATCH 06/21] formatting changes --- CODE_OF_CONDUCT.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d248e31..ab97a2a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,10 +1,10 @@ -### CODE OF CONDUCT +# CODE OF CONDUCT Evernorth Strategic Development, Inc. (“Evernorth”) welcomes contributions to our hosted open source projects. We have adopted this Code of Conduct (the “Code”) to facilitate your participation and to ensure our open source community remains a safe, harassment-free, and collaborative space. -# Scope +## Scope This Code 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 e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. -# Our Standards +## Our Standards We strive to make participation in our open source projects open and enjoyable for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex, gender identity, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual orientation. Examples of behavior that contributes to a positive environment for our community include: @@ -21,15 +21,13 @@ Examples of unacceptable behavior include: * 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 +## Enforcement The Evernorth Open Source Leadership Group (the “Leadership Group”) is responsible for clarifying and enforcing this Code and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. The Leadership Group has 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, and will communicate reasons for moderation decisions when appropriate. -# Reporting +## Reporting Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the Leadership Group at [opensource@evernorth.com](mailto:opensource@evernorth.com). All complaints will be reviewed and investigated promptly and fairly, while respecting the privacy and security of the reporter. We will determine if and what response is appropriate, and every matter may not result in a direct response from us. Consequences for inappropriate behavior may include: (1) a warning with consequences for continued behavior; (2) restricted or suspended access to certain projects or other community members for a specific period of time; (3) temporary suspensions from participation in the community; and (4) permanent bans from participating in the community. Factors that may be considered when imposing various consequences may include, but are not limited to, the seriousness or nature of the harm caused, repeated violations of this Code, and the impact of the activity on our broader open source community. -# Attribution -This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 2.0, available at [contributor-covenant.org/version/2/0/](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). - - +## Attribution +This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 2.0, available at [contributor-covenant.org/version/2/0/](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). \ No newline at end of file From 4f3962b7c943866c034c612944fc9e4bda6766a9 Mon Sep 17 00:00:00 2001 From: stevensefton <176721477+stevensefton@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:18:47 -0500 Subject: [PATCH 07/21] merged into single example --- internal/examples/advanced-example.go | 25 ------------------------- internal/examples/basic-example.go | 23 ----------------------- 2 files changed, 48 deletions(-) delete mode 100644 internal/examples/advanced-example.go delete mode 100644 internal/examples/basic-example.go diff --git a/internal/examples/advanced-example.go b/internal/examples/advanced-example.go deleted file mode 100644 index 802d9be..0000000 --- a/internal/examples/advanced-example.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "errors" - "github.com/Evernorth/problemdetails" - "net/http" -) - -func getSomethingAdvanced(httpRespWriter http.ResponseWriter, httpReq *http.Request) { - - err := doSomethingAdvanced() - if err != nil { - httpRespWriter.WriteHeader(http.StatusInternalServerError) - render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError().WithTitle("KABOOM!!!").WithDetail("The unthinkable has occurred.").WithExtension( - "example1", "test").WithExtension( - "example2", "this could be a struct if you like")) - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) - - return - } -} - -func doSomethingAdvanced() error { - return errors.New("look who stepped in the room") -} diff --git a/internal/examples/basic-example.go b/internal/examples/basic-example.go deleted file mode 100644 index 7463f55..0000000 --- a/internal/examples/basic-example.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "errors" - "github.com/Evernorth/problemdetails" - "github.com/go-chi/render" - "net/http" -) - -func getSomethingBasic(httpRespWriter http.ResponseWriter, httpReq *http.Request) { - - err := doSomethingBasic() - if err != nil { - httpRespWriter.WriteHeader(http.StatusInternalServerError) - render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) - return - } -} - -func doSomethingBasic() error { - return errors.New("look who stepped in the room") -} From 258a6ef3a1706e8474c02671bb8e3e89851c61bd Mon Sep 17 00:00:00 2001 From: stevensefton <176721477+stevensefton@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:19:27 -0500 Subject: [PATCH 08/21] initial --- internal/examples/example.go | 61 ++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 internal/examples/example.go diff --git a/internal/examples/example.go b/internal/examples/example.go new file mode 100644 index 0000000..e429312 --- /dev/null +++ b/internal/examples/example.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "github.com/Evernorth/http-problemdetails-go" + "github.com/go-chi/render" + "net/http" +) + +func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { + + // If the request is not a GET, return a 405 Method Not Allowed + if httpReq.Method != http.MethodGet { + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.WriteHeader(http.StatusMethodNotAllowed) + render.JSON(httpRespWriter, httpReq, problemdetails.NewMethodNotAllowed()) + return + } + + // Get the example-type from the query parameters + queryParams := httpReq.URL.Query() + exampleType := queryParams.Get("example-type") + + if exampleType == "basic" { + + // Return a 500 Internal Server Error, using the NewInternalServerError function. + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.WriteHeader(http.StatusInternalServerError) + render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) + + } else if exampleType == "advanced" { + + // Return a 500 Internal Server Error, using the NewInternalServerError and With* functions. + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.WriteHeader(http.StatusInternalServerError) + render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError(). + WithTitle("KABOOM!!!"). + WithDetail("The unthinkable has occurred."). + WithExtension("example1", "test"). + WithExtension("example2", "this could be a struct if you like")) + + } else { + + // Return a 400 Bad Request, using the NewBadRequest function. + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.WriteHeader(http.StatusBadRequest) + render.JSON(httpRespWriter, httpReq, problemdetails.NewBadRequest(). + WithDetail("example-type must be basic or advanced.")) + + } +} + +func main() { + http.HandleFunc("/", handler) + + fmt.Println("Starting server on 8080") + err := http.ListenAndServe(":8080", nil) + if err != nil { + fmt.Println("Server failed to start:", err) + } +} From 584d9356aa7976e821409435da44c354eac9a613 Mon Sep 17 00:00:00 2001 From: stevensefton <176721477+stevensefton@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:20:01 -0500 Subject: [PATCH 09/21] not needed --- internal/examples/go.mod | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 internal/examples/go.mod diff --git a/internal/examples/go.mod b/internal/examples/go.mod deleted file mode 100644 index ee17002..0000000 --- a/internal/examples/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/Evernorth/problemdetails/examples - -go 1.23.1 From c609e08e9921f333c59dea6909fbd4901c090cb3 Mon Sep 17 00:00:00 2001 From: stevensefton <176721477+stevensefton@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:20:37 -0500 Subject: [PATCH 10/21] cleanup --- problemdetails.go | 41 ++++++++-------------- problemdetails_test.go | 66 +++++++++++++++++------------------ problemtype.go | 78 ++++++++++++++++++++++-------------------- 3 files changed, 88 insertions(+), 97 deletions(-) diff --git a/problemdetails.go b/problemdetails.go index bb4cd4e..19d20de 100644 --- a/problemdetails.go +++ b/problemdetails.go @@ -58,66 +58,55 @@ func New(typeUri string, status int) *ProblemDetails { // NewBadRequest creates a new ProblemDetails instance that represents an HTTP 400 Bad Request. func NewBadRequest() *ProblemDetails { - return New(BadRequest.urn, - BadRequest.code).WithTitle(BadRequest.title) + return New(badRequest.urn, badRequest.code).WithTitle(badRequest.title) } // NewUnauthorized creates a new ProblemDetails instance that represents an HTTP 401 Unauthorized. func NewUnauthorized() *ProblemDetails { - return New(Unauthorized.urn, - Unauthorized.code).WithTitle(Unauthorized.title) + return New(unauthorized.urn, unauthorized.code).WithTitle(unauthorized.title) } // NewForbidden creates a new ProblemDetails instance that represents an HTTP 403 Forbidden. func NewForbidden() *ProblemDetails { - return New(Forbidden.urn, - Forbidden.code).WithTitle(Forbidden.title) + return New(forbidden.urn, forbidden.code).WithTitle(forbidden.title) } // NewNotFound creates a new ProblemDetails instance that represents an HTTP 404 Not Found. func NewNotFound() *ProblemDetails { - return New(NotFound.urn, - NotFound.code).WithTitle(NotFound.title) + return New(notFound.urn, notFound.code).WithTitle(notFound.title) +} + +// NewNotFound creates a new ProblemDetails instance that represents an HTTP 405 Method Not Allowed. +func NewMethodNotAllowed() *ProblemDetails { + return New(methodNotAllowed.urn, methodNotAllowed.code).WithTitle(methodNotAllowed.title) } // NewConflict creates a new ProblemDetails instance that represents an HTTP 409 Conflict. func NewConflict() *ProblemDetails { - return New(Conflict.urn, - Conflict.code).WithTitle(Conflict.title) + return New(conflict.urn, conflict.code).WithTitle(conflict.title) } // NewTooManyRequests creates a new ProblemDetails instance that represents an HTTP 429 Too Many Requests. func NewTooManyRequests() *ProblemDetails { - return New(TooManyRequests.urn, - TooManyRequests.code).WithTitle(TooManyRequests.title) + return New(tooManyRequests.urn, tooManyRequests.code).WithTitle(tooManyRequests.title) } // NewInternalServerError creates a new ProblemDetails instance that represents an HTTP 500 Internal Server Error. func NewInternalServerError() *ProblemDetails { - return New(InternalServerError.urn, - InternalServerError.code).WithTitle(InternalServerError.title) -} - -// NewNotImplemented creates a new ProblemDetails instance that represents an HTTP 501 Not Implemented. -func NewNotImplemented() *ProblemDetails { - return New(NotImplemented.urn, - NotImplemented.code).WithTitle(NotImplemented.title) + return New(internalServerError.urn, internalServerError.code).WithTitle(internalServerError.title) } // NewBadGateway creates a new ProblemDetails instance that represents an HTTP 502 Bad Gateway. func NewBadGateway() *ProblemDetails { - return New(BadGateway.urn, - BadGateway.code).WithTitle(BadGateway.title) + return New(badGateway.urn, badGateway.code).WithTitle(badGateway.title) } // NewServiceUnavailable creates a new ProblemDetails instance that represents an HTTP 503 Service Unavailable. func NewServiceUnavailable() *ProblemDetails { - return New(ServiceUnavailable.urn, - ServiceUnavailable.code).WithTitle(ServiceUnavailable.title) + return New(serviceUnavailable.urn, serviceUnavailable.code).WithTitle(serviceUnavailable.title) } // NewGatewayTimeout creates a new ProblemDetails instance that represents an HTTP 504 Gateway Timeout. func NewGatewayTimeout() *ProblemDetails { - return New(GatewayTimeout.urn, - GatewayTimeout.code).WithTitle(GatewayTimeout.title) + return New(gatewayTimeout.urn, gatewayTimeout.code).WithTitle(gatewayTimeout.title) } diff --git a/problemdetails_test.go b/problemdetails_test.go index ef2bbf7..e73b89d 100644 --- a/problemdetails_test.go +++ b/problemdetails_test.go @@ -9,9 +9,9 @@ import ( func TestNewInternalServerError(t *testing.T) { problemDetails := NewInternalServerError() - assert.Equal(t, InternalServerError.urn, problemDetails.Type) - assert.Equal(t, InternalServerError.code, problemDetails.Status) - assert.Equal(t, InternalServerError.title, problemDetails.Title) + assert.Equal(t, internalServerError.urn, problemDetails.Type) + assert.Equal(t, internalServerError.code, problemDetails.Status) + assert.Equal(t, internalServerError.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -19,9 +19,9 @@ func TestNewInternalServerError(t *testing.T) { func TestNewNotFound(t *testing.T) { problemDetails := NewNotFound() - assert.Equal(t, NotFound.urn, problemDetails.Type) - assert.Equal(t, NotFound.code, problemDetails.Status) - assert.Equal(t, NotFound.title, problemDetails.Title) + assert.Equal(t, notFound.urn, problemDetails.Type) + assert.Equal(t, notFound.code, problemDetails.Status) + assert.Equal(t, notFound.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -29,9 +29,9 @@ func TestNewNotFound(t *testing.T) { func TestNewBadRequest(t *testing.T) { problemDetails := NewBadRequest() - assert.Equal(t, BadRequest.urn, problemDetails.Type) - assert.Equal(t, BadRequest.code, problemDetails.Status) - assert.Equal(t, BadRequest.title, problemDetails.Title) + assert.Equal(t, badRequest.urn, problemDetails.Type) + assert.Equal(t, badRequest.code, problemDetails.Status) + assert.Equal(t, badRequest.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -39,9 +39,9 @@ func TestNewBadRequest(t *testing.T) { func TestNewUnauthorized(t *testing.T) { problemDetails := NewUnauthorized() - assert.Equal(t, Unauthorized.urn, problemDetails.Type) - assert.Equal(t, Unauthorized.code, problemDetails.Status) - assert.Equal(t, Unauthorized.title, problemDetails.Title) + assert.Equal(t, unauthorized.urn, problemDetails.Type) + assert.Equal(t, unauthorized.code, problemDetails.Status) + assert.Equal(t, unauthorized.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -49,9 +49,9 @@ func TestNewUnauthorized(t *testing.T) { func TestNewForbidden(t *testing.T) { problemDetails := NewForbidden() - assert.Equal(t, Forbidden.urn, problemDetails.Type) - assert.Equal(t, Forbidden.code, problemDetails.Status) - assert.Equal(t, Forbidden.title, problemDetails.Title) + assert.Equal(t, forbidden.urn, problemDetails.Type) + assert.Equal(t, forbidden.code, problemDetails.Status) + assert.Equal(t, forbidden.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -59,9 +59,9 @@ func TestNewForbidden(t *testing.T) { func TestNewConflict(t *testing.T) { problemDetails := NewConflict() - assert.Equal(t, Conflict.urn, problemDetails.Type) - assert.Equal(t, Conflict.code, problemDetails.Status) - assert.Equal(t, Conflict.title, problemDetails.Title) + assert.Equal(t, conflict.urn, problemDetails.Type) + assert.Equal(t, conflict.code, problemDetails.Status) + assert.Equal(t, conflict.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -69,9 +69,9 @@ func TestNewConflict(t *testing.T) { func TestNewNotImplemented(t *testing.T) { problemDetails := NewNotImplemented() - assert.Equal(t, NotImplemented.urn, problemDetails.Type) - assert.Equal(t, NotImplemented.code, problemDetails.Status) - assert.Equal(t, NotImplemented.title, problemDetails.Title) + assert.Equal(t, notImplemented.urn, problemDetails.Type) + assert.Equal(t, notImplemented.code, problemDetails.Status) + assert.Equal(t, notImplemented.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -79,9 +79,9 @@ func TestNewNotImplemented(t *testing.T) { func TestNewBadGateway(t *testing.T) { problemDetails := NewBadGateway() - assert.Equal(t, BadGateway.urn, problemDetails.Type) - assert.Equal(t, BadGateway.code, problemDetails.Status) - assert.Equal(t, BadGateway.title, problemDetails.Title) + assert.Equal(t, badGateway.urn, problemDetails.Type) + assert.Equal(t, badGateway.code, problemDetails.Status) + assert.Equal(t, badGateway.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -89,9 +89,9 @@ func TestNewBadGateway(t *testing.T) { func TestNewServiceUnavailable(t *testing.T) { problemDetails := NewServiceUnavailable() - assert.Equal(t, ServiceUnavailable.urn, problemDetails.Type) - assert.Equal(t, ServiceUnavailable.code, problemDetails.Status) - assert.Equal(t, ServiceUnavailable.title, problemDetails.Title) + assert.Equal(t, serviceUnavailable.urn, problemDetails.Type) + assert.Equal(t, serviceUnavailable.code, problemDetails.Status) + assert.Equal(t, serviceUnavailable.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -99,9 +99,9 @@ func TestNewServiceUnavailable(t *testing.T) { func TestNewGatewayTimeout(t *testing.T) { problemDetails := NewGatewayTimeout() - assert.Equal(t, GatewayTimeout.urn, problemDetails.Type) - assert.Equal(t, GatewayTimeout.code, problemDetails.Status) - assert.Equal(t, GatewayTimeout.title, problemDetails.Title) + assert.Equal(t, gatewayTimeout.urn, problemDetails.Type) + assert.Equal(t, gatewayTimeout.code, problemDetails.Status) + assert.Equal(t, gatewayTimeout.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) @@ -109,9 +109,9 @@ func TestNewGatewayTimeout(t *testing.T) { func TestNewTooManyRequests(t *testing.T) { problemDetails := NewTooManyRequests() - assert.Equal(t, TooManyRequests.urn, problemDetails.Type) - assert.Equal(t, TooManyRequests.code, problemDetails.Status) - assert.Equal(t, TooManyRequests.title, problemDetails.Title) + assert.Equal(t, tooManyRequests.urn, problemDetails.Type) + assert.Equal(t, tooManyRequests.code, problemDetails.Status) + assert.Equal(t, tooManyRequests.title, problemDetails.Title) assert.Equal(t, "", problemDetails.Detail) assert.Equal(t, 0, len(problemDetails.Extensions)) doCommonAssertions(t, problemDetails) diff --git a/problemtype.go b/problemtype.go index c537fb2..cacd7a7 100644 --- a/problemtype.go +++ b/problemtype.go @@ -2,8 +2,10 @@ package problemdetails import "net/http" -// ProblemType represents a problem type as defined in RFC 9457. This struct encapsulates the details of different types of HTTP problems, such as their type URIs, status codes, and titles. This struct would help in organizing and reusing these details across the codebase. -type ProblemType struct { +// problemType represents a problem type as defined in RFC 9457. This struct encapsulates the details of different +// types of HTTP problems, such as their type URIs, status codes, and titles. This struct would help in organizing +// and reusing these details across the codebase. +type problemType struct { code int urn string title string @@ -11,80 +13,80 @@ type ProblemType struct { var ( - // BadRequest (HTTP 400) indicates that the provided request could not be processed because it is invalid. - BadRequest = ProblemType{ + // badRequest (HTTP 400) + badRequest = problemType{ code: http.StatusBadRequest, urn: "urn:problems:bad-request", - title: "The provided request could not be processed because it is invalid.", + title: "Request could not be processed because it is invalid.", } - // Unauthorized (HTTP 401) indicates that the request requires user authentication. - Unauthorized = ProblemType{ + // unauthorized (HTTP 401) + unauthorized = problemType{ code: http.StatusUnauthorized, urn: "urn:problems:unauthorized", - title: "The request requires user authentication.", + title: "Authentication required.", } - // Forbidden (HTTP 403) indicates that the server understood the request, but refuses to authorize it. - Forbidden = ProblemType{ + // forbidden (HTTP 403) + forbidden = problemType{ code: http.StatusForbidden, urn: "urn:problems:forbidden", - title: "The server understood the request, but refuses to authorize it.", + title: "User is not authorized to perform the requested operation.", } - // NotFound (HTTP 404) indicates that the specified resource could not be found. - NotFound = ProblemType{ + // notFound (HTTP 404) + notFound = problemType{ code: http.StatusNotFound, urn: "urn:problems:not-found", title: "The specified resource could not be found.", } - // Conflict (HTTP 409) indicates that the request could not be completed due to a conflict with the current state of the resource. - Conflict = ProblemType{ + // notFound (HTTP 405) + methodNotAllowed = problemType{ + code: http.StatusMethodNotAllowed, + urn: "urn:problems:method-not-allowed", + title: "The specified method is not allowed.", + } + + // conflict (HTTP 409) + conflict = problemType{ code: http.StatusConflict, urn: "urn:problems:conflict", - title: "The request could not be completed due to a conflict with the current state of the resource.", + title: "Request could not be completed due to a conflict with the current state of the resource.", } - // TooManyRequests (HTTP 429) indicates that the user has sent too many requests in a given amount of time. - TooManyRequests = ProblemType{ + // tooManyRequests (HTTP 429) + tooManyRequests = problemType{ code: http.StatusTooManyRequests, urn: "urn:problems:too-many-requests", - title: "The user has sent too many requests in a given amount of time.", + title: "User has sent too many requests.", } - // InternalServerError (HTTP 500) indicates that the server has encountered a situation it doesn't know how to handle. - InternalServerError = ProblemType{ + // internalServerError (HTTP 500) + internalServerError = problemType{ code: http.StatusInternalServerError, urn: "urn:problems:internal-server-error", - title: "The server has encountered a situation it doesn't know how to handle.", - } - - // NotImplemented (HTTP 501) indicates that the request method is not supported by the server and cannot be handled. - NotImplemented = ProblemType{ - code: http.StatusNotImplemented, - urn: "urn:problems:not-implemented", - title: "The request method is not supported by the server and cannot be handled.", + title: "An unexpected error occurred.", } - // BadGateway (HTTP 502) indicates that the server, while acting as a gateway or proxy, received an invalid response from the upstream server. - BadGateway = ProblemType{ + // badGateway (HTTP 502) + badGateway = problemType{ code: http.StatusBadGateway, urn: "urn:problems:bad-gateway", - title: "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.", + title: "Invalid response from upstream server.", } - // ServiceUnavailable (HTTP 503) indicates that the server is not ready to handle the request, often due to maintenance or overload. - ServiceUnavailable = ProblemType{ + // serviceUnavailable (HTTP 503) + serviceUnavailable = problemType{ code: http.StatusServiceUnavailable, urn: "urn:problems:service-unavailable", - title: "The server is not ready to handle the request, often due to maintenance or overload.", + title: "Service is temporarily unavailable.", } - // GatewayTimeout (HTTP 504) indicates that the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server. - GatewayTimeout = ProblemType{ + // gatewayTimeout (HTTP 504) + gatewayTimeout = problemType{ code: http.StatusGatewayTimeout, urn: "urn:problems:gateway-timeout", - title: "The server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.", + title: "Timeout invoking upstream server.", } ) From c13fcf94ef3973ce5de3ad8abe4421eac29b8aff Mon Sep 17 00:00:00 2001 From: stevensefton <176721477+stevensefton@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:21:37 -0500 Subject: [PATCH 11/21] updated --- go.mod | 4 +++- go.sum | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5cf9259..15e9e40 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,15 @@ -module github.com/Evernorth/problemdetails +module github.com/Evernorth/http-problemdetails-go go 1.23.1 require ( + github.com/go-chi/render v1.0.3 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.9.0 ) require ( + github.com/ajg/form v1.5.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5697539..7c6f9fc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From 55b6b4a722e89bd4bee1a43bd3a3ec2784aabdc7 Mon Sep 17 00:00:00 2001 From: stevensefton <176721477+stevensefton@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:21:23 -0500 Subject: [PATCH 12/21] refactored to problemdetails directory --- internal/examples/example.go | 2 +- problemdetails.go => problemdetails/problemdetails.go | 0 .../problemdetails_test.go | 10 ---------- problemtype.go => problemdetails/problemtype.go | 0 4 files changed, 1 insertion(+), 11 deletions(-) rename problemdetails.go => problemdetails/problemdetails.go (100%) rename problemdetails_test.go => problemdetails/problemdetails_test.go (91%) rename problemtype.go => problemdetails/problemtype.go (100%) diff --git a/internal/examples/example.go b/internal/examples/example.go index e429312..a2af5cf 100644 --- a/internal/examples/example.go +++ b/internal/examples/example.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/Evernorth/http-problemdetails-go" + "github.com/Evernorth/http-problemdetails-go/problemdetails" "github.com/go-chi/render" "net/http" ) diff --git a/problemdetails.go b/problemdetails/problemdetails.go similarity index 100% rename from problemdetails.go rename to problemdetails/problemdetails.go diff --git a/problemdetails_test.go b/problemdetails/problemdetails_test.go similarity index 91% rename from problemdetails_test.go rename to problemdetails/problemdetails_test.go index e73b89d..66a4679 100644 --- a/problemdetails_test.go +++ b/problemdetails/problemdetails_test.go @@ -67,16 +67,6 @@ func TestNewConflict(t *testing.T) { doCommonAssertions(t, problemDetails) } -func TestNewNotImplemented(t *testing.T) { - problemDetails := NewNotImplemented() - assert.Equal(t, notImplemented.urn, problemDetails.Type) - assert.Equal(t, notImplemented.code, problemDetails.Status) - assert.Equal(t, notImplemented.title, problemDetails.Title) - assert.Equal(t, "", problemDetails.Detail) - assert.Equal(t, 0, len(problemDetails.Extensions)) - doCommonAssertions(t, problemDetails) -} - func TestNewBadGateway(t *testing.T) { problemDetails := NewBadGateway() assert.Equal(t, badGateway.urn, problemDetails.Type) diff --git a/problemtype.go b/problemdetails/problemtype.go similarity index 100% rename from problemtype.go rename to problemdetails/problemtype.go From 64b2815436546e683088461a9aee8c9fe8c2757a Mon Sep 17 00:00:00 2001 From: stevensefton <176721477+stevensefton@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:38:53 -0500 Subject: [PATCH 13/21] updated --- README.md | 139 +++++++++++++++++++++++++++++------------------------- 1 file changed, 74 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index eb61ccd..860d2c4 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,102 @@ # http-problemdetails-go - An implementation of [IETF RFC 9457 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html) which is a specification for a standard error structure for HTTP APIs. - -## Dependencies -See the [go.mod](go.mod) file. - -## Building from Source - -Detailed instructions on how to build the project from source. Also note where to get pre-built distribution, if building is not required. - ## Installation ```go get -u github.com/Evernorth/http-problemdetails-go``` ## Features - - The MIME type of the response is `application/problem+json`. - Supports the following HTTP status codes: -| HTTP Code | Method | -|-------------------------------------|-------------------------------------------------| -| 400 Bad Request | [`NewBadRequest()`](problemdetails.go) | -| 401 Unauthorized | [`NewUnauthorized()`](problemdetails.go) | -| 403 Forbidden | [`NewForbidden()`](problemdetails.go) | -| 404 Not Found | [`NewNotFound()`](problemdetails.go) | -| 409 Conflict | [`NewConflict()`](problemdetails.go) | -| 429 Too Many Requests | [`NewTooManyRequests()`](problemdetails.go) | -| 500 Internal Server Error | [`NewInternalServerError()`](problemdetails.go) | -| 501 Not Implemented | [`NewNotImplemented()`](problemdetails.go) | -| 502 Bad Gateway | [`NewBadGateway()`](problemdetails.go) | -| 503 Service Unavailable | [`NewServiceUnavailable()`](problemdetails.go) | -| 504 Gateway Timeout | [`NewGatewayTimeout()`](problemdetails.go) | - - ->Note: MIME type must be set in the response header to comply with the RFC 9457 specification. +| HTTP Code | Method | +|---------------------------|----------------------------------------------------------------| +| 400 Bad Request | [`NewBadRequest()`](problemdetails/problemdetails.go) | +| 401 Unauthorized | [`NewUnauthorized()`](problemdetails/problemdetails.go) | +| 403 Forbidden | [`NewForbidden()`](problemdetails/problemdetails.go) | +| 404 Not Found | [`NewNotFound()`](problemdetails/problemdetails.go) | +| 405 Method Not Allowed | [`NewMethodNotAllowed()`](problemdetails/problemdetails.go) | +| 409 Conflict | [`NewConflict()`](problemdetails/problemdetails.go) | +| 429 Too Many Requests | [`NewTooManyRequests()`](problemdetails/problemdetails.go) | +| 500 Internal Server Error | [`NewInternalServerError()`](problemdetails/problemdetails.go) | +| 502 Bad Gateway | [`NewBadGateway()`](problemdetails/problemdetails.go) | +| 503 Service Unavailable | [`NewServiceUnavailable()`](problemdetails/problemdetails.go) | +| 504 Gateway Timeout | [`NewGatewayTimeout()`](problemdetails/problemdetails.go) | + +>Note: MIME type must be set in the response header to comply with the RFC 9457 specification. The MimeType constant is provided for this purpose. ## Usage ### Creating a simple HTTP ProblemDetails +The example code below demonstrates how to create simple ProblemDetails instances as well as how to add information to them using the With* functions. + You can create a ProblemDetails instance by simply calling functions like `problemdetails.NewInternalServerError()`, `problemdetails.NewNotFound()` and `problemdetails.NewBadRequest()`. + +You can easily override the default ProblemDetails title and detail fields and provide custom extension fields by using the `WithTitle`, `WithDetail` and `WithExtension` functions. ``` package main import ( - "github.com/Evernorth/http-problemdetails-go/problemdetails" - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "net/http" + "fmt" + "github.com/Evernorth/http-problemdetails-go/problemdetails" + "github.com/go-chi/render" + "net/http" ) -func getSomething(httpRespWriter http.ResponseWriter, httpReq *http.Request) { - - err := doSomething() - if err != nil { - httpRespWriter.WriteHeader(http.StatusInternalServerError) - render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) - return - } - ... +func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { + + // If the request is not a GET, return a 405 Method Not Allowed + if httpReq.Method != http.MethodGet { + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.WriteHeader(http.StatusMethodNotAllowed) + render.JSON(httpRespWriter, httpReq, problemdetails.NewMethodNotAllowed()) + return + } + + // Get the example-type from the query parameters + queryParams := httpReq.URL.Query() + exampleType := queryParams.Get("example-type") + + if exampleType == "basic" { + + // Return a 500 Internal Server Error, using the NewInternalServerError function. + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.WriteHeader(http.StatusInternalServerError) + render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) + + } else if exampleType == "advanced" { + + // Return a 500 Internal Server Error, using the NewInternalServerError and With* functions. + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.WriteHeader(http.StatusInternalServerError) + render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError(). + WithTitle("KABOOM!!!"). + WithDetail("The unthinkable has occurred."). + WithExtension("example1", "test"). + WithExtension("example2", "this could be a struct if you like")) + + } else { + + // Return a 400 Bad Request, using the NewBadRequest function. + httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.WriteHeader(http.StatusBadRequest) + render.JSON(httpRespWriter, httpReq, problemdetails.NewBadRequest(). + WithDetail("example-type must be basic or advanced.")) + + } } -``` -### Creating a complex HTTP ProblemDetails -You can easily override the default ProblemDetails title and detail fields and provide custom extension fields by using the `WithTitle`, `WithDetail` and `WithExtension` functions. -``` -package main -import ( - "github.com/Evernorth/http-problemdetails-go/problemdetails" - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "net/http" -) +func main() { + http.HandleFunc("/", handler) -func getSomething(httpRespWriter http.ResponseWriter, httpReq *http.Request) { - - err := doSomething() - if err != nil { - httpRespWriter.WriteHeader(http.StatusInternalServerError) - render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError().WithTitle( - "KABOOM!!!").WithDetail("The unthinkable has occurred.").WithExtension( - "example1", "test").WithExtension( - "example2", "this could be a struct if you like")) - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) - - return - } - ... + fmt.Println("Starting server on 8080") + err := http.ListenAndServe(":8080", nil) + if err != nil { + fmt.Println("Server failed to start:", err) + } } ``` +## Dependencies +See the [go.mod](go.mod) file. + ## Support If you have questions, concerns, bug reports, etc. See [CONTRIBUTING](CONTRIBUTING.md). From 88dda0ac8ecaed592304438824b281cb1e428617 Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:25:26 -0400 Subject: [PATCH 14/21] Initial Commit --- README.md | 8 ++++++++ problemdetails/problemdetails.go | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 860d2c4..179cdee 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ # http-problemdetails-go + +[![Go Reference](https://pkg.go.dev/badge/github.com/Evernorth/http-problemdetails-go.svg)](https://pkg.go.dev/github.com/Evernorth/http-problemdetails-go) +[![Go Report Card](https://goreportcard.com/badge/github.com/Evernorth/http-problemdetails-go)](https://goreportcard.com/report/github.com/Evernorth/http-problemdetails-go) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Release](https://img.shields.io/github/v/release/Evernorth/http-problemdetails-go)](https://gtihub.com/Evernorth/http-problemdetails-go/releases) + +## Description An implementation of [IETF RFC 9457 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html) which is a specification for a standard error structure for HTTP APIs. ## Installation @@ -21,6 +28,7 @@ An implementation of [IETF RFC 9457 Problem Details for HTTP APIs](https://www.r | 503 Service Unavailable | [`NewServiceUnavailable()`](problemdetails/problemdetails.go) | | 504 Gateway Timeout | [`NewGatewayTimeout()`](problemdetails/problemdetails.go) | + >Note: MIME type must be set in the response header to comply with the RFC 9457 specification. The MimeType constant is provided for this purpose. ## Usage diff --git a/problemdetails/problemdetails.go b/problemdetails/problemdetails.go index 19d20de..1be7b3a 100644 --- a/problemdetails/problemdetails.go +++ b/problemdetails/problemdetails.go @@ -76,7 +76,7 @@ func NewNotFound() *ProblemDetails { return New(notFound.urn, notFound.code).WithTitle(notFound.title) } -// NewNotFound creates a new ProblemDetails instance that represents an HTTP 405 Method Not Allowed. +// NewMethodNotAllowed creates a new ProblemDetails instance that represents an HTTP 405 Method Not Allowed. func NewMethodNotAllowed() *ProblemDetails { return New(methodNotAllowed.urn, methodNotAllowed.code).WithTitle(methodNotAllowed.title) } From 93b08ed521762aad30b8c9d7e3edbe08dda8265e Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:40:17 -0400 Subject: [PATCH 15/21] Renmaade the MimeType const to MIMETypeJSON --- internal/examples/example.go | 8 ++++---- problemdetails/problemdetails.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/examples/example.go b/internal/examples/example.go index a2af5cf..caa22c0 100644 --- a/internal/examples/example.go +++ b/internal/examples/example.go @@ -11,7 +11,7 @@ func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { // If the request is not a GET, return a 405 Method Not Allowed if httpReq.Method != http.MethodGet { - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSON) httpRespWriter.WriteHeader(http.StatusMethodNotAllowed) render.JSON(httpRespWriter, httpReq, problemdetails.NewMethodNotAllowed()) return @@ -24,14 +24,14 @@ func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { if exampleType == "basic" { // Return a 500 Internal Server Error, using the NewInternalServerError function. - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSON) httpRespWriter.WriteHeader(http.StatusInternalServerError) render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) } else if exampleType == "advanced" { // Return a 500 Internal Server Error, using the NewInternalServerError and With* functions. - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSON) httpRespWriter.WriteHeader(http.StatusInternalServerError) render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError(). WithTitle("KABOOM!!!"). @@ -42,7 +42,7 @@ func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { } else { // Return a 400 Bad Request, using the NewBadRequest function. - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSON) httpRespWriter.WriteHeader(http.StatusBadRequest) render.JSON(httpRespWriter, httpReq, problemdetails.NewBadRequest(). WithDetail("example-type must be basic or advanced.")) diff --git a/problemdetails/problemdetails.go b/problemdetails/problemdetails.go index 1be7b3a..3017871 100644 --- a/problemdetails/problemdetails.go +++ b/problemdetails/problemdetails.go @@ -7,7 +7,7 @@ import ( const ( uuidUrnPrefix = "urn:uuid:" - MimeType = "application/problem+json" + MIMETypeJSON = "application/problem+json" ) // ProblemDetails represents a HTTP Problem Details structure from https://www.rfc-editor.org/rfc/rfc9457.html From a5729e2525e28cf25dab04d74ede66a00997b632 Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:20:15 -0400 Subject: [PATCH 16/21] Added test functions for TestNewMethodNotAllowed. Added test functions for methods with additional details and extensions: - TestNewWithDetail - TestNewWithExtension --- problemdetails/problemdetails_test.go | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/problemdetails/problemdetails_test.go b/problemdetails/problemdetails_test.go index 66a4679..0535718 100644 --- a/problemdetails/problemdetails_test.go +++ b/problemdetails/problemdetails_test.go @@ -27,6 +27,16 @@ func TestNewNotFound(t *testing.T) { doCommonAssertions(t, problemDetails) } +func TestNewMethodNotAllowed(t *testing.T) { + problemDetails := NewMethodNotAllowed() + assert.Equal(t, methodNotAllowed.urn, problemDetails.Type) + assert.Equal(t, methodNotAllowed.code, problemDetails.Status) + assert.Equal(t, methodNotAllowed.title, problemDetails.Title) + assert.Equal(t, "", problemDetails.Detail) + assert.Equal(t, 0, len(problemDetails.Extensions)) + doCommonAssertions(t, problemDetails) +} + func TestNewBadRequest(t *testing.T) { problemDetails := NewBadRequest() assert.Equal(t, badRequest.urn, problemDetails.Type) @@ -107,6 +117,28 @@ func TestNewTooManyRequests(t *testing.T) { doCommonAssertions(t, problemDetails) } +func TestNewWithDetail(t *testing.T) { + const detail = "This is a detailed error message" + + problemDetails := NewInternalServerError().WithDetail(detail) + assert.Equal(t, internalServerError.urn, problemDetails.Type) + assert.Equal(t, internalServerError.code, problemDetails.Status) + assert.Equal(t, internalServerError.title, problemDetails.Title) + +} +func TestNewWithExtension(t *testing.T) { + const key = "example1" + const value = "test" + + problemDetails := NewInternalServerError().WithExtension(key, value) + assert.Equal(t, internalServerError.urn, problemDetails.Type) + assert.Equal(t, internalServerError.code, problemDetails.Status) + assert.Equal(t, internalServerError.title, problemDetails.Title) + assert.Equal(t, 1, len(problemDetails.Extensions)) + assert.Equal(t, value, problemDetails.Extensions[key]) + doCommonAssertions(t, problemDetails) +} + func doCommonAssertions(t *testing.T, problemDetails *ProblemDetails) { assert.Equal(t, true, strings.Contains(problemDetails.Instance, uuidUrnPrefix)) From 8be0c2fe3ac18608b9389cb89174d525c2adfb24 Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:05:22 -0400 Subject: [PATCH 17/21] - Renamed MimeType to MIMETypeJSON. - Added example of output in README.md --- README.md | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 179cdee..6dc1a3a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/Evernorth/http-problemdetails-go.svg)](https://pkg.go.dev/github.com/Evernorth/http-problemdetails-go) [![Go Report Card](https://goreportcard.com/badge/github.com/Evernorth/http-problemdetails-go)](https://goreportcard.com/report/github.com/Evernorth/http-problemdetails-go) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![Release](https://img.shields.io/github/v/release/Evernorth/http-problemdetails-go)](https://gtihub.com/Evernorth/http-problemdetails-go/releases) ## Description An implementation of [IETF RFC 9457 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html) which is a specification for a standard error structure for HTTP APIs. @@ -29,7 +28,7 @@ An implementation of [IETF RFC 9457 Problem Details for HTTP APIs](https://www.r | 504 Gateway Timeout | [`NewGatewayTimeout()`](problemdetails/problemdetails.go) | ->Note: MIME type must be set in the response header to comply with the RFC 9457 specification. The MimeType constant is provided for this purpose. +>Note: MIME type must be set in the response header to comply with the RFC 9457 specification. The `MIMETypeJSON` constant is provided for this purpose. ## Usage ### Creating a simple HTTP ProblemDetails @@ -52,7 +51,7 @@ func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { // If the request is not a GET, return a 405 Method Not Allowed if httpReq.Method != http.MethodGet { - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSONJSON) httpRespWriter.WriteHeader(http.StatusMethodNotAllowed) render.JSON(httpRespWriter, httpReq, problemdetails.NewMethodNotAllowed()) return @@ -65,14 +64,14 @@ func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { if exampleType == "basic" { // Return a 500 Internal Server Error, using the NewInternalServerError function. - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSON) httpRespWriter.WriteHeader(http.StatusInternalServerError) render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError()) } else if exampleType == "advanced" { // Return a 500 Internal Server Error, using the NewInternalServerError and With* functions. - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSON) httpRespWriter.WriteHeader(http.StatusInternalServerError) render.JSON(httpRespWriter, httpReq, problemdetails.NewInternalServerError(). WithTitle("KABOOM!!!"). @@ -83,7 +82,7 @@ func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { } else { // Return a 400 Bad Request, using the NewBadRequest function. - httpRespWriter.Header().Set("Content-Type", problemdetails.MimeType) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSON) httpRespWriter.WriteHeader(http.StatusBadRequest) render.JSON(httpRespWriter, httpReq, problemdetails.NewBadRequest(). WithDetail("example-type must be basic or advanced.")) @@ -102,6 +101,23 @@ func main() { } ``` +Here is an example of the output from the above code: +``` +HTTP/1.1 400 Bad Request +Content-Type: application/problem+json +Date: Tue, 15 Oct 2024 16:01:11 GMT +Content-Length: 263 + +{ + "type": "urn:problems:bad-request", + "status": 400, + "title": "Request could not be processed because it is invalid.", + "detail": "example-type must be basic or advanced.", + "instance": "urn:uuid:d9c65ded-ec53-464c-8b3a-be3e8c25bf67", + "created": "2024-10-15T16:01:11.601188Z" +} +``` + ## Dependencies See the [go.mod](go.mod) file. From d352bf44f2973ad877cdb9d7a3a7203c63f8b65f Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:50:21 -0400 Subject: [PATCH 18/21] - Renamed ProblemDetail Struct to Problem - Updated date on CHANGELOG.md --- CHANGELOG.md | 2 +- problemdetails/problemdetails.go | 68 +++++++++++++-------------- problemdetails/problemdetails_test.go | 2 +- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e30c9..7a57596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. We follow the [Semantic Versioning 2.0.0](http://semver.org/) format. -## 1.0.0-2024-10-09 +## 1.0.0-2024-10-15 Initial Release diff --git a/problemdetails/problemdetails.go b/problemdetails/problemdetails.go index 3017871..9b2f237 100644 --- a/problemdetails/problemdetails.go +++ b/problemdetails/problemdetails.go @@ -10,8 +10,8 @@ const ( MIMETypeJSON = "application/problem+json" ) -// ProblemDetails represents a HTTP Problem Details structure from https://www.rfc-editor.org/rfc/rfc9457.html -type ProblemDetails struct { +// Problem represents a HTTP Problem Details structure from https://www.rfc-editor.org/rfc/rfc9457.html +type Problem struct { Type string `json:"type"` Status int `json:"status"` Title string `json:"title,omitempty"` @@ -21,23 +21,23 @@ type ProblemDetails struct { Extensions map[string]any `json:"extensions,omitempty"` } -// WithTitle sets a custom Title value on the ProblemDetails object. The ProblemDetails pointer is +// WithTitle sets a custom Title value on the Problem object. The Problem pointer is // returned only for developer convenience. -func (p *ProblemDetails) WithTitle(title string) *ProblemDetails { +func (p *Problem) WithTitle(title string) *Problem { p.Title = title return p } -// WithDetail sets a custom Detail value on the ProblemDetails object. The ProblemDetails pointer is +// WithDetail sets a custom Detail value on the Problem object. The Problem pointer is // returned only for developer convenience. -func (p *ProblemDetails) WithDetail(detail string) *ProblemDetails { +func (p *Problem) WithDetail(detail string) *Problem { p.Detail = detail return p } -// WithExtension adds a custom Extension value on the ProblemDetails object. The ProblemDetails pointer is +// WithExtension adds a custom Extension value on the Problem object. The Problem pointer is // returned only for developer convenience. -func (p *ProblemDetails) WithExtension(name string, value any) *ProblemDetails { +func (p *Problem) WithExtension(name string, value any) *Problem { if p.Extensions == nil { p.Extensions = make(map[string]any) } @@ -45,10 +45,10 @@ func (p *ProblemDetails) WithExtension(name string, value any) *ProblemDetails { return p //&p recreates pointer? } -// New creates a new ProblemDetails instance. Do not use this function unless you are creating a -// custom ProblemDetails instance. Prefer using the functions that create standard ProblemDetails instances. -func New(typeUri string, status int) *ProblemDetails { - return &ProblemDetails{ +// New creates a new Problem instance. Do not use this function unless you are creating a +// custom Problem instance. Prefer using the functions that create standard Problem instances. +func New(typeUri string, status int) *Problem { + return &Problem{ Type: typeUri, Status: status, Instance: uuidUrnPrefix + uuid.NewString(), @@ -56,57 +56,57 @@ func New(typeUri string, status int) *ProblemDetails { } } -// NewBadRequest creates a new ProblemDetails instance that represents an HTTP 400 Bad Request. -func NewBadRequest() *ProblemDetails { +// NewBadRequest creates a new Problem instance that represents an HTTP 400 Bad Request. +func NewBadRequest() *Problem { return New(badRequest.urn, badRequest.code).WithTitle(badRequest.title) } -// NewUnauthorized creates a new ProblemDetails instance that represents an HTTP 401 Unauthorized. -func NewUnauthorized() *ProblemDetails { +// NewUnauthorized creates a new Problem instance that represents an HTTP 401 Unauthorized. +func NewUnauthorized() *Problem { return New(unauthorized.urn, unauthorized.code).WithTitle(unauthorized.title) } -// NewForbidden creates a new ProblemDetails instance that represents an HTTP 403 Forbidden. -func NewForbidden() *ProblemDetails { +// NewForbidden creates a new Problem instance that represents an HTTP 403 Forbidden. +func NewForbidden() *Problem { return New(forbidden.urn, forbidden.code).WithTitle(forbidden.title) } -// NewNotFound creates a new ProblemDetails instance that represents an HTTP 404 Not Found. -func NewNotFound() *ProblemDetails { +// NewNotFound creates a new Problem instance that represents an HTTP 404 Not Found. +func NewNotFound() *Problem { return New(notFound.urn, notFound.code).WithTitle(notFound.title) } -// NewMethodNotAllowed creates a new ProblemDetails instance that represents an HTTP 405 Method Not Allowed. -func NewMethodNotAllowed() *ProblemDetails { +// NewMethodNotAllowed creates a new Problem instance that represents an HTTP 405 Method Not Allowed. +func NewMethodNotAllowed() *Problem { return New(methodNotAllowed.urn, methodNotAllowed.code).WithTitle(methodNotAllowed.title) } -// NewConflict creates a new ProblemDetails instance that represents an HTTP 409 Conflict. -func NewConflict() *ProblemDetails { +// NewConflict creates a new Problem instance that represents an HTTP 409 Conflict. +func NewConflict() *Problem { return New(conflict.urn, conflict.code).WithTitle(conflict.title) } -// NewTooManyRequests creates a new ProblemDetails instance that represents an HTTP 429 Too Many Requests. -func NewTooManyRequests() *ProblemDetails { +// NewTooManyRequests creates a new Problem instance that represents an HTTP 429 Too Many Requests. +func NewTooManyRequests() *Problem { return New(tooManyRequests.urn, tooManyRequests.code).WithTitle(tooManyRequests.title) } -// NewInternalServerError creates a new ProblemDetails instance that represents an HTTP 500 Internal Server Error. -func NewInternalServerError() *ProblemDetails { +// NewInternalServerError creates a new Problem instance that represents an HTTP 500 Internal Server Error. +func NewInternalServerError() *Problem { return New(internalServerError.urn, internalServerError.code).WithTitle(internalServerError.title) } -// NewBadGateway creates a new ProblemDetails instance that represents an HTTP 502 Bad Gateway. -func NewBadGateway() *ProblemDetails { +// NewBadGateway creates a new Problem instance that represents an HTTP 502 Bad Gateway. +func NewBadGateway() *Problem { return New(badGateway.urn, badGateway.code).WithTitle(badGateway.title) } -// NewServiceUnavailable creates a new ProblemDetails instance that represents an HTTP 503 Service Unavailable. -func NewServiceUnavailable() *ProblemDetails { +// NewServiceUnavailable creates a new Problem instance that represents an HTTP 503 Service Unavailable. +func NewServiceUnavailable() *Problem { return New(serviceUnavailable.urn, serviceUnavailable.code).WithTitle(serviceUnavailable.title) } -// NewGatewayTimeout creates a new ProblemDetails instance that represents an HTTP 504 Gateway Timeout. -func NewGatewayTimeout() *ProblemDetails { +// NewGatewayTimeout creates a new Problem instance that represents an HTTP 504 Gateway Timeout. +func NewGatewayTimeout() *Problem { return New(gatewayTimeout.urn, gatewayTimeout.code).WithTitle(gatewayTimeout.title) } diff --git a/problemdetails/problemdetails_test.go b/problemdetails/problemdetails_test.go index 0535718..a65b1f4 100644 --- a/problemdetails/problemdetails_test.go +++ b/problemdetails/problemdetails_test.go @@ -139,7 +139,7 @@ func TestNewWithExtension(t *testing.T) { doCommonAssertions(t, problemDetails) } -func doCommonAssertions(t *testing.T, problemDetails *ProblemDetails) { +func doCommonAssertions(t *testing.T, problemDetails *Problem) { assert.Equal(t, true, strings.Contains(problemDetails.Instance, uuidUrnPrefix)) var createdTime time.Time From da5cafdaf9275e99308eab4049efdd63906479a3 Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:24:26 -0400 Subject: [PATCH 19/21] - Renamed change New() to NewProblem() in problemdetails.go - Updated README.md to remove 'JSONJSON' --- README.md | 2 +- problemdetails/problemdetails.go | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6dc1a3a..927b324 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ func handler(httpRespWriter http.ResponseWriter, httpReq *http.Request) { // If the request is not a GET, return a 405 Method Not Allowed if httpReq.Method != http.MethodGet { - httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSONJSON) + httpRespWriter.Header().Set("Content-Type", problemdetails.MIMETypeJSON) httpRespWriter.WriteHeader(http.StatusMethodNotAllowed) render.JSON(httpRespWriter, httpReq, problemdetails.NewMethodNotAllowed()) return diff --git a/problemdetails/problemdetails.go b/problemdetails/problemdetails.go index 9b2f237..8f52636 100644 --- a/problemdetails/problemdetails.go +++ b/problemdetails/problemdetails.go @@ -45,9 +45,9 @@ func (p *Problem) WithExtension(name string, value any) *Problem { return p //&p recreates pointer? } -// New creates a new Problem instance. Do not use this function unless you are creating a +// NewProblem creates a new Problem instance. Do not use this function unless you are creating a // custom Problem instance. Prefer using the functions that create standard Problem instances. -func New(typeUri string, status int) *Problem { +func NewProblem(typeUri string, status int) *Problem { return &Problem{ Type: typeUri, Status: status, @@ -58,55 +58,55 @@ func New(typeUri string, status int) *Problem { // NewBadRequest creates a new Problem instance that represents an HTTP 400 Bad Request. func NewBadRequest() *Problem { - return New(badRequest.urn, badRequest.code).WithTitle(badRequest.title) + return NewProblem(badRequest.urn, badRequest.code).WithTitle(badRequest.title) } // NewUnauthorized creates a new Problem instance that represents an HTTP 401 Unauthorized. func NewUnauthorized() *Problem { - return New(unauthorized.urn, unauthorized.code).WithTitle(unauthorized.title) + return NewProblem(unauthorized.urn, unauthorized.code).WithTitle(unauthorized.title) } // NewForbidden creates a new Problem instance that represents an HTTP 403 Forbidden. func NewForbidden() *Problem { - return New(forbidden.urn, forbidden.code).WithTitle(forbidden.title) + return NewProblem(forbidden.urn, forbidden.code).WithTitle(forbidden.title) } // NewNotFound creates a new Problem instance that represents an HTTP 404 Not Found. func NewNotFound() *Problem { - return New(notFound.urn, notFound.code).WithTitle(notFound.title) + return NewProblem(notFound.urn, notFound.code).WithTitle(notFound.title) } // NewMethodNotAllowed creates a new Problem instance that represents an HTTP 405 Method Not Allowed. func NewMethodNotAllowed() *Problem { - return New(methodNotAllowed.urn, methodNotAllowed.code).WithTitle(methodNotAllowed.title) + return NewProblem(methodNotAllowed.urn, methodNotAllowed.code).WithTitle(methodNotAllowed.title) } // NewConflict creates a new Problem instance that represents an HTTP 409 Conflict. func NewConflict() *Problem { - return New(conflict.urn, conflict.code).WithTitle(conflict.title) + return NewProblem(conflict.urn, conflict.code).WithTitle(conflict.title) } // NewTooManyRequests creates a new Problem instance that represents an HTTP 429 Too Many Requests. func NewTooManyRequests() *Problem { - return New(tooManyRequests.urn, tooManyRequests.code).WithTitle(tooManyRequests.title) + return NewProblem(tooManyRequests.urn, tooManyRequests.code).WithTitle(tooManyRequests.title) } // NewInternalServerError creates a new Problem instance that represents an HTTP 500 Internal Server Error. func NewInternalServerError() *Problem { - return New(internalServerError.urn, internalServerError.code).WithTitle(internalServerError.title) + return NewProblem(internalServerError.urn, internalServerError.code).WithTitle(internalServerError.title) } // NewBadGateway creates a new Problem instance that represents an HTTP 502 Bad Gateway. func NewBadGateway() *Problem { - return New(badGateway.urn, badGateway.code).WithTitle(badGateway.title) + return NewProblem(badGateway.urn, badGateway.code).WithTitle(badGateway.title) } // NewServiceUnavailable creates a new Problem instance that represents an HTTP 503 Service Unavailable. func NewServiceUnavailable() *Problem { - return New(serviceUnavailable.urn, serviceUnavailable.code).WithTitle(serviceUnavailable.title) + return NewProblem(serviceUnavailable.urn, serviceUnavailable.code).WithTitle(serviceUnavailable.title) } // NewGatewayTimeout creates a new Problem instance that represents an HTTP 504 Gateway Timeout. func NewGatewayTimeout() *Problem { - return New(gatewayTimeout.urn, gatewayTimeout.code).WithTitle(gatewayTimeout.title) + return NewProblem(gatewayTimeout.urn, gatewayTimeout.code).WithTitle(gatewayTimeout.title) } From 299e507bd65073ac47d78bc7df97f73cc4932322 Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:26:44 -0400 Subject: [PATCH 20/21] - Renamed change New() to NewProblem() in problemdetails.go - Updated README.md to remove 'JSONJSON' --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 927b324..ba60caa 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/Evernorth/http-problemdetails-go.svg)](https://pkg.go.dev/github.com/Evernorth/http-problemdetails-go) [![Go Report Card](https://goreportcard.com/badge/github.com/Evernorth/http-problemdetails-go)](https://goreportcard.com/report/github.com/Evernorth/http-problemdetails-go) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Release](https://img.shields.io/github/v/release/Evernorth/http-problemdetails-go)](https://gtihub.com/Evernorth/http-problemdetails-go/releases) ## Description An implementation of [IETF RFC 9457 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html) which is a specification for a standard error structure for HTTP APIs. @@ -32,11 +33,11 @@ An implementation of [IETF RFC 9457 Problem Details for HTTP APIs](https://www.r ## Usage ### Creating a simple HTTP ProblemDetails -The example code below demonstrates how to create simple ProblemDetails instances as well as how to add information to them using the With* functions. +The example code below demonstrates how to create simple Problem instances as well as how to add information to them using the With* functions. -You can create a ProblemDetails instance by simply calling functions like `problemdetails.NewInternalServerError()`, `problemdetails.NewNotFound()` and `problemdetails.NewBadRequest()`. +You can create a Problem instance by simply calling functions like `problemdetails.NewInternalServerError()`, `problemdetails.NewNotFound()` and `problemdetails.NewBadRequest()`. -You can easily override the default ProblemDetails title and detail fields and provide custom extension fields by using the `WithTitle`, `WithDetail` and `WithExtension` functions. +You can easily override the default Problem title and detail fields and provide custom extension fields by using the `WithTitle`, `WithDetail` and `WithExtension` functions. ``` package main @@ -130,3 +131,4 @@ http-problemdetails-go is Open Source software released under the [Apache 2.0 li ## Original Contributors - Steve Sefton, Evernorth - Shellee Stewart, Evernorth +- Cindy Chen, Evernorth From d82c9095016f43dc8f38b100af2ebd3238aaf9a0 Mon Sep 17 00:00:00 2001 From: Shellee Stewart <10214065+shelleeboo@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:40:47 -0400 Subject: [PATCH 21/21] - Update CHANGELOG.md with version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a57596..b309eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. We follow the [Semantic Versioning 2.0.0](http://semver.org/) format. -## 1.0.0-2024-10-15 +## v1.0.0 2024-10-15 Initial Release