diff --git a/.gitignore b/.gitignore index d5bebc6..1f1befd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/go-form +/public +/.hugo_build.lock diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..c04fa41 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,32 @@ +steps: + Build: + image: gitnet.fr/deblan/hugo + pull: true + environments: + HUGO_ENVIRONMENT: production + commands: + - hugo build + + "Commit pages": + image: alpine/git + commands: + - mv public /tmp/ + - git config --global user.email ci@gitnet.fr + - git config --global user.name CI + - git fetch --no-tags origin +refs/heads/pages + - git switch pages + - rm * -fr + - mv /tmp/public/* . + - git add . + - git commit -m "Build doc ${CI_BUILD_NUMBER}" + + "Push pages": + image: appleboy/drone-git-push + commands: + settings: + branch: pages + remote: git@gitnet.fr:deblan/go-form.git + force: false + commit: false + ssh_key: + from_secret: ssh_priv_key diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index ba56fe5..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,101 +0,0 @@ -## [Unreleased] - -## v1.5.0 - -### Added - -- feat: add collection widget -- feat: refactoring and improvement of example - -## v1.4.0 - -### Added - -- feat: add json configuration - -### Fixed - -- fix: reset `GlobalFields` in `End()` - -## v1.3.0 - -### Added - -- feat: allow to handle request using json body" -- feat: add `WithJsonRequest` on form -- feat: add `MapToUrlValues` transformer - -## v1.2.0 - -### Added - -- feat: add tag `field` to specify the naming strategy (case) -- feat: add `ErrorsTree` methods on form and field - -## v1.1.6 - -### Fixed - -- fix(input/choice): add specific validation func - -## v1.1.5 - -### Fixed - -- fix(input/choice): add specific validation func - -## v1.1.4 - -### Fixed - -- fix(theme/bootstrap5): fix button class -- fix(theme/html5): remove div wrapper on form content - -## v1.1.3 - -### Fixed - -- fix(form): replace existing option in WithOptions - -## v1.1.2 - -### Added - -- chore(example): remove boostrap classes - -### Fixed - -- fix(theme): checkbox is check on nil value - -## v1.1.1 - -### Fixed - -- feat(form): add default options -- fix(choice): fix Match when field data is nil - -## v1.1.0 - -### Added - -- feat: add go template functions to render a form, a field, etc. -- feat: add constraints (isodd, iseven) -- refactor: refactor constraint (not blank check) - -### Changed - -- feat: replace templates with gomponents - -## v1.0.1 - -### Added - -- doc: add documentation - -## v1.0.0 - -### Added - -- feat: form builder -- feat: form validation -- feat: form renderer diff --git a/LICENCE b/LICENCE deleted file mode 100644 index be3f7b2..0000000 --- a/LICENCE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d8be964 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +all: build + +serve: + hugo server --buildDrafts --disableFastRender + +build: + hugo build diff --git a/README.md b/README.md deleted file mode 100644 index 3754083..0000000 --- a/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# go-form - -Creating and processing HTML forms is hard and repetitive. You need to deal with rendering HTML form fields, validating submitted data, mapping the form data into objects and a lot more. [`go-form`][go-form] includes a powerful form feature that provides all these features. - -[`go-form`][go-form] is heavily influenced by [Symfony Form](https://symfony.com/doc/current/forms.html). It includes: - -* A form builder based on fields declarations and independent of structs -* Validation based on constraints -* Data mounting to populate a form from a struct instance -* Data binding to populate a struct instance from a submitted form -* Form renderer with customizable themes - -## Documentation - -* [Official documentation][doc] -* [Demo][demo] - -[go-form]: https://gitnet.fr/deblan/go-form -[demo]: https://gitnet.fr/deblan/go-form-demo -[doc]: https://deblan.gitnet.page/go-form/ diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..0d5eebd --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,5 @@ +--- +date: '{{ .Date }}' +draft: true +title: '{{ replace .File.ContentBaseName "-" " " | title }}' +--- diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..202c701 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,60 @@ +.hidden { + display: none; +} + +.hugo-goplay-summary { + cursor: pointer; +} + +.hugo-goplay-textarea { + padding: 1rem; + width: 100%; + border-radius: 4px; + margin: 10px 0; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 27) / 0.1); + font-family: monospace; +} + +.hugo-goplay-result pre { + padding: 1rem; +} + +.hugo-goplay-result code { + background: none !important; + border: 0 !important; +} + +.hugo-goplay-result .system { + color: green; +} + +.hugo-goplay-result .stderr { + color: red; +} + +.hugo-goplay-toolbox { + display: flex; + justify-content: flex-start; + margin-bottom: 2rem; +} + +.hugo-goplay-toolbox .hugo-goplay-button { + font-size: 13px; + font-weight: bold; + border: 1px solid var(--primary); + padding: 0.15rem 0.75rem; + border-radius: 4px; + margin-left: 0.5rem; + color: var(--primary); + background-color: var(--theme); +} + +.hugo-goplay-toolbox .hugo-goplay-button:hover { + border: 1px solid var(--theme); + color: var(--theme); + background-color: var(--primary); +} + +.hugo-goplay-toolbox .hugo-goplay-button:first-child { + margin-left: 0; +} diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..c96bcf4 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,58 @@ +--- +layout: hextra-home +title: "Welcome!" +--- + +{{< hextra/hero-badge >}} +
+ Free, open source + {{< icon name="arrow-circle-right" attributes="height=14" >}} +{{< /hextra/hero-badge >}} + +
+{{< hextra/hero-headline >}} + Create and process forms with Golang! +{{< /hextra/hero-headline >}} +
+ +
+{{< hextra/hero-subtitle >}} + Fast, batteries-included Hugo theme 
for creating beautiful static websites +{{< /hextra/hero-subtitle >}} +
+
+ +
+{{< hextra/hero-button text="Get Started" link="docs" >}} +
+
+
+ +
+ +{{< hextra/feature-grid >}} + {{< hextra/feature-card + title="Fast and Full-featured" + subtitle="Simple and easy to use, yet powerful and feature-rich." + class="hx:aspect-auto hx:md:aspect-[1.1/1] hx:max-md:min-h-[340px]" + image="images/hextra-doc.webp" + imageClass="hx:top-[40%] hx:left-[24px] hx:w-[180%] hx:sm:w-[110%] hx:dark:opacity-80" + style="background: radial-gradient(ellipse at 50% 80%,rgba(194,97,254,0.15),hsla(0,0%,100%,0));" + >}} + {{< hextra/feature-card + title="Form builder and validation" + subtitle="Compose with just Markdown. Enrich with Shortcode components." + class="hx:aspect-auto hx:md:aspect-[1.1/1] hx:max-lg:min-h-[340px]" + image="images/hextra-markdown.webp" + imageClass="hx:top-[40%] hx:left-[36px] hx:w-[180%] hx:sm:w-[110%] hx:dark:opacity-80" + style="background: radial-gradient(ellipse at 50% 80%,rgba(142,53,74,0.15),hsla(0,0%,100%,0));" + >}} + {{< hextra/feature-card + title="Mount and bind any struct" + subtitle="Built-in full text search with FlexSearch, no extra setup required." + class="hx:aspect-auto hx:md:aspect-[1.1/1] hx:max-md:min-h-[340px]" + image="images/hextra-search.webp" + imageClass="hx:top-[40%] hx:left-[36px] hx:w-[110%] hx:sm:w-[110%] hx:dark:opacity-80" + style="background: radial-gradient(ellipse at 50% 80%,rgba(221,210,59,0.15),hsla(0,0%,100%,0));" + >}} +{{< /hextra/feature-grid >}} diff --git a/content/docs/Installation.md b/content/docs/Installation.md new file mode 100644 index 0000000..cb641b4 --- /dev/null +++ b/content/docs/Installation.md @@ -0,0 +1,9 @@ +--- +linkTitle: Installation +title: Installation +weight: 2 +--- + +```golang +go get gitnet.fr/deblan/go-form +``` diff --git a/content/docs/_index.md b/content/docs/_index.md new file mode 100644 index 0000000..4a7b3d7 --- /dev/null +++ b/content/docs/_index.md @@ -0,0 +1,14 @@ +--- +linkTitle: "Documentation" +title: Introduction +--- + +Creating and processing HTML forms is hard and repetitive. You need to deal with rendering HTML form fields, validating submitted data, mapping the form data into objects and a lot more. **go-form** includes a powerful form feature that provides all these features. + +**go-form** is heavily influenced by [Symfony Form](https://symfony.com/doc/current/forms.html). It includes: + +* A form builder based on fields declarations and independent of structs +* Validation based on constraints +* Data mounting to populate a form from a struct instance +* Data binding to populate a struct instance from a submitted form +* Form renderer with customizable themes diff --git a/content/docs/constraints/_index.md b/content/docs/constraints/_index.md new file mode 100644 index 0000000..d874e08 --- /dev/null +++ b/content/docs/constraints/_index.md @@ -0,0 +1,153 @@ +--- +linkTitle: Constraints +title: Constraints +weight: 5 +--- + +The validation is designed to validate data against constraints. + +## Import + +```golang +import ( + "gitnet.fr/deblan/go-form/validation" +) +``` + +## Constraints + +### Length + +Validate the length of an array, a slice or a string + +```golang +c := validation.NewLength() + +// Define minimum +c.WithMin(1) + +// Define minimum +c.WithMax(100) + +// Define min and max +// Equivalent to c.WithMin(50).WithMax(50) +c.WithExact(50) +``` + +### Mail + +```golang +validation.NewMail() +``` + +### Not blank + +```golang +validation.NewNotBlank() +``` + +### Range + +Validate a number + +```golang +c := validation.NewRange() + +// Define minimum +c.WithMin(1) + +// Define minimum +c.WithMax(100) + +// Define min and max +// Equivalent to c.WithMin(1).WithMax(100) +c.WithRange(1, 100) +``` + +### Regex + +Validate a string with a regex + +```golang +c := validation.NewRegex(`expression`) + +// The value must match +c.MustMatch() + +// The value must not match +c.MustNotMatch() +``` + +### Is even + +Validate that a number is even. + +```golang +validation.NewIsEven() +``` + +### Is odd + +Validate that a number is odd. + +```golang +validation.NewIsOdd() +``` + + +## Custom constraint + +Use case: you want to validate that the data equals "example" + +```golang +package validation + +import ( + "reflect" + + v "gitnet.fr/deblan/go-form/validation" +) + +// Define a struct +type IsExample struct { + Message string + TypeErrorMessage string +} + +// Create a factory +func NewIsExample() IsEven { + return IsEven{ + Message: "This value does not equal \"example\".", + TypeErrorMessage: "This value can not be processed.", + } +} + +// Implement the validation +func (c IsExample) Validate(data any) []Error { + errors := []Error{} + + // Should not validate blank data + if len(v.NewNotBlank().Validate(data)) != 0 { + return []Error{} + } + + t := reflect.TypeOf(data) + + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + switch t.Kind() { + case reflect.String: + if data.(string) != "example" { + errors = append(errors, Error(c.Message)) + } + + default: + errors = append(errors, Error(c.TypeErrorMessage)) + } + + return errors +} +``` + diff --git a/content/docs/fields/_index.md b/content/docs/fields/_index.md new file mode 100644 index 0000000..63fb9be --- /dev/null +++ b/content/docs/fields/_index.md @@ -0,0 +1,384 @@ +--- +linkTitle: Fields +title: Fields +weight: 4 +--- + +A field represents a field in a form. + +### Checkbox + +```golang +func NewFieldCheckbox(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=checkbox] + +### Choice + +```golang +func NewFieldChoice(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + + +Generates inputs (checkbox or radio) or selects + +### Csrf + +```golang +func NewFieldCsrf(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +### Date + +```golang +func NewFieldDate(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=date] with default transformers + +### Datetime + +```golang +func NewFieldDatetime(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=datetime] with default transformers + +### DatetimeLocal + +```golang +func NewFieldDatetimeLocal(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=datetime-local] with default transformers + +### Hidden + +```golang +func NewFieldHidden(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=hidden] + +### Mail + +```golang +func NewFieldMail(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=email] + +### Number + +```golang +func NewFieldNumber(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=number] with default transformers + +### Password + +```golang +func NewFieldPassword(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=password] + +### Range + +```golang +func NewFieldRange(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=range] + +### Sub Form + +```golang +func NewFieldSubForm(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Alias: + +```golang +func NewSubForm(name string) *Field +``` + +Generates a sub form + +### Text + +```golang +func NewFieldText(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=text] + +### Textarea + +```golang +func NewFieldTextarea(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates a textarea + +### Time + +```golang +func NewFieldTime(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=time] with default transformers + +### Submit + +```golang +func NewSubmit(name string) *Field +``` + +{{% goplay-field %}} + +{{% /goplay-field %}} + +Generates an input[type=submit] + +## Methods + +### Add + +```golang +func (f *Field) Add(children ...*Field) *Field +``` + +Appends children + +### Bind + +```golang +func (f *Field) Bind(data map[string]any, key *string) error +``` + +Bind the data into the given map + +### GetChild + +```golang +func (f *Field) GetChild(name string) *Field +``` + +Returns a child using its name + +### GetId + +```golang +func (f *Field) GetId() string +``` + +Computes the id of the field + +### GetName + +```golang +func (f *Field) GetName() string +``` + +Computes the name of the field + +### GetOption + +```golang +func (f *Field) GetOption(name string) *Option +``` + +Returns an option using its name + +### HasChild + +```golang +func (f *Field) HasChild(name string) bool +``` + +Checks if the field contains a child using its name + +### HasOption + +```golang +func (f *Field) HasOption(name string) bool +``` + +Checks if the field contains an option using its name + +### Mount + +```golang +func (f *Field) Mount(data any) error +``` + +Populates the field with data + +### ResetErrors + +```golang +func (f *Field) ResetErrors() *Field +``` + +Resets the field errors + +### WithBeforeBind + +```golang +func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field +``` + +Sets a transformer applied to the data of a field before defining it in a structure + +### WithBeforeMount + +```golang +func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field +``` + +Sets a transformer applied to the structure data before displaying it in a field + +### WithConstraints + +```golang +func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field +``` + +Appends constraints + +### WithData + +```golang +func (f *Field) WithData(data any) *Field +``` + +Sets data the field + +### WithFixedName + +```golang +func (f *Field) WithFixedName() *Field +``` + +Sets that the name of the field is not computed + +### WithOptions + +```golang +func (f *Field) WithOptions(options ...*Option) *Field +``` + +#### Common options + +| Name | type | description | Info | +| ---- | ---- | ---- | ---- | +| `required` | `bool` | Add `required="true"` | Does not apply a constraint | +| `attr` | `form.Attrs` | List of extra attributes of the field | | +| `row_attr` | `form.Attrs` | List of extra attributes of the field's top container | | +| `label` | `string` | The label of the field | Usually show before the field | +| `label_attr` | `form.Attrs` | List of extra attributes of the label | | +| `help` | `string` | Helper of the field | | +| `help_attr` | `form.Attrs` | List of extra attributes of the help | | + +Appends options to the field + +### WithSlice + +```golang +func (f *Field) WithSlice() *Field +``` + +Sets that the field represents a data slice diff --git a/content/docs/form/_index.md b/content/docs/form/_index.md new file mode 100644 index 0000000..60466be --- /dev/null +++ b/content/docs/form/_index.md @@ -0,0 +1,243 @@ +--- +linkTitle: Form +title: Form +weight: 3 +--- + +## Example + +### Prerequisites + +```golang +import ( + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/validation" +) + +type Person struct { + Name string + Age int +} +``` + +### Creating a form + +```golang +myForm := form.NewForm( + form.NewFieldText("Name"). + WithConstraints( + validation.NewNotBlank(), + ), + form.NewFieldNumber("Age"). + WithConstraints( + validation.NewNotBlank(), + validation.NewRange().WithMin(18), + ), +).End() +``` + +### Validating a struct + +```golang +data := Person{} + +myForm.Mount(data) +myForm.IsValid() // false + +data = Person{ + Name: "Alice", + Age: 42, +} + +myForm.Mount(data) +myForm.IsValid() // true +``` + +### Validating a request + +```golang +import ( + "net/http" +) + +myForm.WithMethod(http.MethodPost) + +// req *http.Request +if req.Method == myForm.Method { + myForm.HandleRequest(req) + + if myForm.IsSubmitted() && myForm.IsValid() { + myForm.Bind(&data) + } +} +``` + +## Struct + +```golang +type Form struct { + Fields []*Field + GlobalFields []*Field + Errors []validation.Error + Method string + Action string + Name string + Options []*Option + RequestData *url.Values +} +``` + +## Methods + +### NewForm + +```golang +func NewForm(fields ...*Field) *Form +``` + +Generates a new form with default properties + +### Add + +```golang +func (f *Form) Add(fields ...*Field) +``` + +Appends children + +### AddGlobalField + +```golang +func (f *Form) AddGlobalField(field *Field) +``` + +Configures its children deeply + +### Bind + +```golang +func (f *Form) Bind(data any) error +``` + +Copies datas from the form to a struct + +### End + +```golang +func (f *Form) End() *Form +``` + +Configures its children deeply This function must be called after adding all + +fields + +### GetField + +```golang +func (f *Form) GetField(name string) *Field +``` + +Returns a child using its name + +### GetOption + +```golang +func (f *Form) GetOption(name string) *Option +``` + +Returns an option using its name + +### HandleRequest + +```golang +func (f *Form) HandleRequest(req *http.Request) +``` + +Processes a request + +### HasField + +```golang +func (f *Form) HasField(name string) bool +``` + +Checks if the form contains a child using its name + +### HasOption + +```golang +func (f *Form) HasOption(name string) bool +``` + +Checks if the form contains an option using its name + +### IsSubmitted + +```golang +func (f *Form) IsSubmitted() bool +``` + +Checks if the form is submitted + +### IsValid + +```golang +func (f *Form) IsValid() bool +``` + +Checks the a form is valid + +### Mount + +```golang +func (f *Form) Mount(data any) error +``` + +Copies datas from a struct to the form + +### ResetErrors + +```golang +func (f *Form) ResetErrors() *Form +``` + +Resets the form errors + +### WithAction + +```golang +func (f *Form) WithAction(v string) *Form +``` + +Sets the action of the form (eg: "/") + +### WithMethod + +```golang +func (f *Form) WithMethod(v string) *Form +``` + +Sets the method of the format (http.MethodPost, http.MethodGet, ...) + +### WithName + +```golang +func (f *Form) WithName(v string) *Form +``` + +Sets the name of the form (used to compute name of fields) + +### WithOptions + +```golang +func (f *Form) WithOptions(options ...*Option) *Form +``` + +Appends options to the form + +#### Options + + | Name | Type | Description | + | ---- | ---- | ---- | + | `attr` | `map[string]string` | List of extra attributes | + | `help` | `string` | Helper | diff --git a/content/docs/rendering/_index.md b/content/docs/rendering/_index.md new file mode 100644 index 0000000..4bc00d3 --- /dev/null +++ b/content/docs/rendering/_index.md @@ -0,0 +1,67 @@ +--- +linkTitle: Rendering +title: Rendering +weight: 6 +--- + +**go-form** allows you to render a form using Go's built-in template engine. +Here is a simple example that displays a form: + +```golang +myForm := form.NewForm(...) + +render := theme.NewRenderer(theme.Bootstrap5) + +tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` + + + My form + + + {{ form .Form }} + + +`) + +b := new(strings.Builder) + +tpl.Execute(w, map[string]any{ + "Form": myForm, +}) + +fmt.Println(b.String()) +``` + +{{% goplay-auto-import-main %}} + +{{% /goplay-auto-import-main %}} + +Other helper functions are available to render specific parts of the form: + +- `form_errors`: displays the form's global errors +- `form_row` : renders the label, errors, and widget of a field +- `form_label`: renders only the label of a field +- `form_widget`: renders only the widget of a field +- `form_widget_errors`: renders only the errors of a specific field diff --git a/content/docs/rendering/theming.md b/content/docs/rendering/theming.md new file mode 100644 index 0000000..252e940 --- /dev/null +++ b/content/docs/rendering/theming.md @@ -0,0 +1,11 @@ +--- +linkTitle: Theming +title: Theming +--- + +**go-form** provides 2 themes: + +- `theme.Html5`: a basic view without classes +- `theme.Bootstrap5`: a theme for [Bootstrap 5](https://getbootstrap.com/) + +You can add a custom theme. Learn by reading the [Bootstrap5](https://gitnet.fr/deblan/go-form/src/branch/main/theme/bootstrap5.go) theme. diff --git a/content/docs/workflow/_index.md b/content/docs/workflow/_index.md new file mode 100644 index 0000000..6f2b485 --- /dev/null +++ b/content/docs/workflow/_index.md @@ -0,0 +1,120 @@ +--- +linkTitle: Workflow +title: Workflow +weight: 2 +--- + +{{% steps %}} + +### Import + +```golang +import ( + "html/template" + "net/http" + + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/theme" +) +``` + +### Create a form + +```golang +// Let's create a new form +// You can pass *form.Field as arguments +myForm := form.NewForm(field1, field2, ...) + +// Add somes fields +myForm.Add(field3, field4, ...) + +// Set the method +//
+myForm.WithMethod(http.MethodPost) + +// Define the action +// +myForm.WithAction("/") + +// Set a name +myForm.WithName("myForm") + +// Add options +myForm.WithOptions(option1, option2, ...) + +// When all fields are added, call End() +myForm.End() +``` + +#### Attributes + +Some options are natively supported in go-form themes. + +```golang +myForm.WithOptions( + form.NewOption("help", "A help for the form"), + // - - - - Demo of go-form with Bootstrap - - - - -
- {{ form_widget (.styleForm.GetField "value") }} -
- -

Demo of go-form with Bootstrap

- -
- Debug view -
- Submitted: - {{ .isSubmitted }} -
-
- Valid: - {{ .isValid }} -
- -
- Dump of data -
{{ .dump }}
-
- -
- Form as JSON -
{{ .json }}
-
-
- - {{if .isValid}} -
The form is valid!
- {{else}} -
The form is invalid!
- {{end}} - - {{ form .form }} -
- - - - diff --git a/example/view/html5.html b/example/view/html5.html deleted file mode 100644 index 88db2d8..0000000 --- a/example/view/html5.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - Demo of go-form (pure HTML5 and Pico) - - - - - - - - - {{ form_widget (.styleForm.GetField "value") }} -
- -

Demo of go-form (pure HTML5 and Pico)

- -
- Debug view - -
- Submitted: - {{ .isSubmitted }} -
-
- Valid: - {{ .isValid }} -
- -
- Dump of data -
{{ .dump }}
-
- -
- Form as JSON -
{{ .json }}
-
- -
- - {{if .isValid}} -

The form is valid!

- {{else}} -

The form is invalid!

- {{end}} - - {{ form .form }} - - - - - diff --git a/form/field.go b/form/field.go deleted file mode 100644 index fe63326..0000000 --- a/form/field.go +++ /dev/null @@ -1,423 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "fmt" - "maps" - "slices" - "strings" - - "gitnet.fr/deblan/go-form/util" - "gitnet.fr/deblan/go-form/validation" -) - -// Generic function for field.Validation -func DefaultFieldValidation(f *Field) bool { - if len(f.Children) > 0 { - isValid := true - - for _, c := range f.Children { - c.ResetErrors() - isChildValid, errs := validation.Validate(c.Data, c.Constraints) - - if len(errs) > 0 { - c.Errors = errs - } - - isValid = isChildValid && isValid - - for _, sc := range c.Children { - isValid = DefaultFieldValidation(sc) && isValid - } - } - - return isValid - } else { - f.ResetErrors() - isValid, errs := validation.Validate(f.Data, f.Constraints) - - if len(errs) > 0 { - f.Errors = errs - } - - return isValid - } -} - -// Field represents a field in a form -type Field struct { - Name string `json:"name"` - NamePrefix string `json:"name_prefix"` - Widget string `json:"widget"` - Data any `json:"-"` - Options []*Option `json:"options"` - Children []*Field `json:"children"` - Constraints []validation.Constraint `json:"-"` - Errors []validation.Error `json:"-"` - BeforeMount func(data any) (any, error) `json:"-"` - BeforeBind func(data any) (any, error) `json:"-"` - Validate func(f *Field) bool `json:"-"` - IsSlice bool `json:"is_slice"` - IsCollection bool `json:"is_collection"` - IsFixedName bool `json:"is_fixed_name"` - Form *Form `json:"-"` - Parent *Field `json:"-"` -} - -// Generates a new field with default properties -// It should not be used directly but inside function like in form.NewFieldText -func NewField(name, widget string) *Field { - f := &Field{ - Name: name, - IsFixedName: false, - Widget: widget, - Data: nil, - } - - f.BeforeMount = func(data any) (any, error) { - return data, nil - } - - f.BeforeBind = func(data any) (any, error) { - return data, nil - } - - f.WithOptions( - NewOption("attr", Attrs{}), - NewOption("row_attr", Attrs{}), - NewOption("label_attr", Attrs{}), - NewOption("help_attr", Attrs{}), - ) - - f.Validate = DefaultFieldValidation - - return f -} - -func (f *Field) Copy() *Field { - return &Field{ - Name: f.Name, - Form: f.Form, - Widget: f.Widget, - Options: f.Options, - Constraints: f.Constraints, - BeforeMount: f.BeforeMount, - BeforeBind: f.BeforeBind, - Validate: f.Validate, - IsSlice: f.IsSlice, - IsFixedName: f.IsFixedName, - } -} - -// Checks if the field contains an option using its name -func (f *Field) HasOption(name string) bool { - for _, option := range f.Options { - if option.Name == name { - return true - } - } - - return false -} - -// Returns an option using its name -func (f *Field) GetOption(name string) *Option { - for _, option := range f.Options { - if option.Name == name { - return option - } - } - - return nil -} - -// Appends options to the field -func (f *Field) WithOptions(options ...*Option) *Field { - for _, option := range options { - if f.HasOption(option.Name) { - f.GetOption(option.Name).Value = option.Value - } else { - f.Options = append(f.Options, option) - } - } - - return f -} - -// Remove an option if exists -func (f *Field) RemoveOption(name string) *Field { - var options []*Option - - for _, option := range f.Options { - if option.Name != name { - options = append(options, option) - } - } - - f.Options = options - - return f -} - -// Sets data the field -func (f *Field) WithData(data any) *Field { - f.Data = data - - return f -} - -// Resets the field errors -func (f *Field) ResetErrors() *Field { - f.Errors = []validation.Error{} - - return f -} - -// Sets that the field represents a data slice -func (f *Field) WithSlice() *Field { - f.IsSlice = true - - return f -} - -// Sets that the field represents a collection -func (f *Field) WithCollection() *Field { - f.IsCollection = true - - return f -} - -// Sets that the name of the field is not computed -func (f *Field) WithFixedName() *Field { - f.IsFixedName = true - - return f -} - -// Appends constraints -func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field { - for _, constraint := range constraints { - f.Constraints = append(f.Constraints, constraint) - } - - return f -} - -// Sets a transformer applied to the structure data before displaying it in a field -func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field { - f.BeforeMount = callback - - return f -} - -// Sets a transformer applied to the data of a field before defining it in a structure -func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field { - f.BeforeBind = callback - - return f -} - -// Appends children -func (f *Field) Add(children ...*Field) *Field { - for _, child := range children { - child.Parent = f - f.Children = append(f.Children, child) - } - - return f -} - -// Checks if the field contains a child using its name -func (f *Field) HasChild(name string) bool { - for _, child := range f.Children { - if name == child.Name { - return true - } - } - - return false -} - -// Returns a child using its name -func (f *Field) GetChild(name string) *Field { - var result *Field - - for _, child := range f.Children { - if name == child.Name { - result = child - - break - } - } - - return result -} - -// Computes the name of the field -func (f *Field) GetName() string { - var name string - - if f.IsFixedName { - return f.Name - } - - if f.Form != nil && f.Form.Name != "" { - name = fmt.Sprintf("%s%s[%s]", f.Form.Name, f.NamePrefix, f.Name) - } else if f.Parent != nil { - name = fmt.Sprintf("%s%s[%s]", f.Parent.GetName(), f.NamePrefix, f.Name) - } else { - name = f.Name - } - - return name -} - -// Computes the id of the field -func (f *Field) GetId() string { - name := f.GetName() - name = strings.ReplaceAll(name, "[", "-") - name = strings.ReplaceAll(name, "]", "") - name = strings.ToLower(name) - - return name -} - -// Populates the field with data -func (f *Field) Mount(data any) error { - data, err := f.BeforeMount(data) - - if err != nil { - return err - } - - if len(f.Children) == 0 { - f.Data = data - - return nil - } - - props, err := util.InspectStruct(data) - - if err != nil { - return err - } - - for key, value := range props { - if f.HasChild(key) { - err = f.GetChild(key).Mount(value) - - if err != nil { - return err - } - } - } - - return nil -} - -// Bind the data into the given map -func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error { - if len(f.Children) == 0 { - v, err := f.BeforeBind(f.Data) - - if err != nil { - return err - } - - if key != nil { - data[*key] = v - } else { - data[f.Name] = v - } - - return nil - } - - data[f.Name] = make(map[string]any) - - for _, child := range f.Children { - child.Bind(data[f.Name].(map[string]any), key, f.IsCollection) - } - - if f.IsCollection { - var nextData []any - values := data[f.Name].(map[string]any) - var keys []string - - for key, _ := range values { - keys = append(keys, key) - } - - slices.Sort(keys) - - for _, key := range keys { - for valueKey, value := range values { - if valueKey == key { - nextData = append(nextData, value) - } - } - } - - data[f.Name] = nextData - } - - return nil -} - -// Generates a tree of errors -func (f *Field) ErrorsTree(tree map[string]any, key *string) { - var index string - - if key != nil { - index = *key - } else { - index = f.Name - } - - if len(f.Children) == 0 { - if len(f.Errors) > 0 { - tree[index] = map[string]any{ - "meta": map[string]any{ - "id": f.GetId(), - "name": f.Name, - "formName": f.GetName(), - }, - "errors": f.Errors, - } - } - } else { - errors := make(map[string]any) - - for _, child := range f.Children { - if len(child.Errors) > 0 { - child.ErrorsTree(errors, &child.Name) - } - } - - if len(errors) > 0 { - tree[index] = map[string]any{ - "meta": map[string]any{ - "id": f.GetId(), - "name": f.Name, - "formName": f.GetName(), - }, - "errors": []validation.Error{}, - "children": slices.Collect(maps.Values(errors)), - } - } - } -} diff --git a/form/field_checkbox.go b/form/field_checkbox.go deleted file mode 100644 index b4b903c..0000000 --- a/form/field_checkbox.go +++ /dev/null @@ -1,48 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "github.com/spf13/cast" -) - -// Generates an input[type=checkbox] -func NewFieldCheckbox(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "checkbox")). - WithBeforeMount(func(data any) (any, error) { - switch data.(type) { - case string: - data = data == "1" - case bool: - return data, nil - } - - return cast.ToInt(data), nil - }). - WithBeforeBind(func(data any) (any, error) { - switch data.(type) { - case string: - return data == "1", nil - case bool: - return data, nil - } - - return cast.ToBool(data), nil - }) - - return f -} diff --git a/form/field_choice.go b/form/field_choice.go deleted file mode 100644 index 16c06fc..0000000 --- a/form/field_choice.go +++ /dev/null @@ -1,200 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "encoding/json" - "reflect" - - "github.com/spf13/cast" - "gitnet.fr/deblan/go-form/validation" -) - -type Choice struct { - Value string - Label string - Data any -} - -func (c Choice) Match(value string) bool { - return c.Value == value -} - -type Choices struct { - Data any `json:"data"` - ValueBuilder func(key int, item any) string `json:"-"` - LabelBuilder func(key int, item any) string `json:"-"` -} - -func (c *Choices) Match(f *Field, value string) bool { - if f.Data == nil { - return false - } - - if f.IsSlice { - v := reflect.ValueOf(f.Data) - - for key, _ := range c.GetChoices() { - for i := 0; i < v.Len(); i++ { - item := v.Index(i).Interface() - - switch item.(type) { - case string: - if item == value { - return true - } - default: - if c.ValueBuilder(key, item) == value { - return true - } - } - } - } - - return false - } - - return f.Data == value -} - -func (c *Choices) WithValueBuilder(builder func(key int, item any) string) *Choices { - c.ValueBuilder = builder - - return c -} - -func (c *Choices) WithLabelBuilder(builder func(key int, item any) string) *Choices { - c.LabelBuilder = builder - - return c -} - -func (c *Choices) GetChoices() []Choice { - choices := []Choice{} - - v := reflect.ValueOf(c.Data) - - switch v.Kind() { - case reflect.Slice, reflect.Array, reflect.String, reflect.Map: - for i := 0; i < v.Len(); i++ { - choices = append(choices, Choice{ - Value: c.ValueBuilder(i, v.Index(i).Interface()), - Label: c.LabelBuilder(i, v.Index(i).Interface()), - Data: v.Index(i).Interface(), - }) - } - } - - return choices -} - -func (c Choices) MarshalJSON() ([]byte, error) { - var choices []map[string]string - - v := reflect.ValueOf(c.Data) - - switch v.Kind() { - case reflect.Slice, reflect.Array, reflect.String, reflect.Map: - for i := 0; i < v.Len(); i++ { - choices = append(choices, map[string]string{ - "value": c.ValueBuilder(i, v.Index(i).Interface()), - "label": c.LabelBuilder(i, v.Index(i).Interface()), - }) - } - } - - return json.Marshal(choices) -} - -// Generates an instance of Choices -func NewChoices(items any) *Choices { - builder := func(key int, item any) string { - return cast.ToString(key) - } - - choices := Choices{ - ValueBuilder: builder, - LabelBuilder: builder, - Data: items, - } - - return &choices -} - -// Generates inputs (checkbox or radio) or selects -func NewFieldChoice(name string) *Field { - f := NewField(name, "choice"). - WithOptions( - NewOption("choices", &Choices{}), - NewOption("expanded", false), - NewOption("multiple", false), - NewOption("empty_choice_label", "None"), - ) - - f.Validate = func(field *Field) bool { - isValid := field.Validate(field) - - if len(validation.NewNotBlank().Validate(field.Data)) == 0 { - choices := field.GetOption("choices").Value.(*Choices) - isValidChoice := false - - for _, choice := range choices.GetChoices() { - if choices.Match(field, choice.Value) { - isValidChoice = true - } - } - - if !isValidChoice { - field.Errors = append(field.Errors, validation.Error("This value is not valid.")) - isValid = false - } - } - - return isValid - } - - f.WithBeforeBind(func(data any) (any, error) { - choices := f.GetOption("choices").Value.(*Choices) - - switch data.(type) { - case string: - v := data.(string) - for _, c := range choices.GetChoices() { - if c.Match(v) { - return c.Data, nil - } - } - case []string: - v := reflect.ValueOf(data) - var res []interface{} - - for _, choice := range choices.GetChoices() { - for i := 0; i < v.Len(); i++ { - item := v.Index(i).Interface().(string) - if choice.Match(item) { - res = append(res, choice.Data) - } - } - } - - return res, nil - } - - return data, nil - }) - - return f -} diff --git a/form/field_collection.go b/form/field_collection.go deleted file mode 100644 index 3f3ece1..0000000 --- a/form/field_collection.go +++ /dev/null @@ -1,80 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "fmt" - "reflect" -) - -// Generates a sub form -func NewFieldCollection(name string) *Field { - f := NewField(name, "collection"). - WithOptions( - NewOption("allow_add", true), - NewOption("allow_delete", true), - NewOption("form", nil), - ). - WithCollection() - - f.WithBeforeMount(func(data any) (any, error) { - if opt := f.GetOption("form"); opt != nil { - if src, ok := opt.Value.(*Form); ok { - src.Name = f.GetName() - t := reflect.TypeOf(data) - - switch t.Kind() { - case reflect.Slice: - slice := reflect.ValueOf(data) - - for i := 0; i < slice.Len(); i++ { - name := fmt.Sprintf("%d", i) - value := slice.Index(i).Interface() - - if f.HasChild(name) { - f.GetChild(name).Mount(value) - } else { - form := src.Copy() - form.Mount(value) - - field := f.Copy() - field.Widget = "sub_form" - field.Name = name - field.Add(form.Fields...) - field. - RemoveOption("form"). - RemoveOption("label") - - for _, c := range field.Children { - c.NamePrefix = fmt.Sprintf("[%d]", i) - } - - f.Add(field) - } - } - } - } - } - - return data, nil - }) - - return f -} - -func NewCollection(name string) *Field { - return NewFieldCollection(name) -} diff --git a/form/field_input.go b/form/field_input.go deleted file mode 100644 index ca42873..0000000 --- a/form/field_input.go +++ /dev/null @@ -1,84 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "github.com/spf13/cast" -) - -// Generates an input[type=text] -func NewFieldText(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "text")) - - return f -} - -// Generates an input[type=number] with default transformers -func NewFieldNumber(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "number")). - WithBeforeBind(func(data any) (any, error) { - return cast.ToFloat64(data), nil - }) - - return f -} - -// Generates an input[type=email] -func NewFieldMail(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "email")) - - return f -} - -// Generates an input[type=range] -func NewFieldRange(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "range")). - WithBeforeBind(func(data any) (any, error) { - return cast.ToFloat64(data), nil - }) - - return f -} - -// Generates an input[type=password] -func NewFieldPassword(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "password")) - - return f -} - -// Generates an input[type=hidden] -func NewFieldHidden(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "hidden")) - - return f -} - -// Generates an input[type=submit] -func NewSubmit(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "submit")) - - f.Data = "Submit" - - return f -} diff --git a/form/field_input_csrf.go b/form/field_input_csrf.go deleted file mode 100644 index fc3bc3a..0000000 --- a/form/field_input_csrf.go +++ /dev/null @@ -1,23 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -func NewFieldCsrf(name string) *Field { - f := NewFieldHidden(name). - WithFixedName() - - return f -} diff --git a/form/field_input_date.go b/form/field_input_date.go deleted file mode 100644 index 893c59d..0000000 --- a/form/field_input_date.go +++ /dev/null @@ -1,109 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "fmt" - "time" -) - -func DateBeforeMount(data any, format string) (any, error) { - if data == nil { - return nil, nil - } - - switch data.(type) { - case string: - return data, nil - case time.Time: - return data.(time.Time).Format(format), nil - case *time.Time: - v := data.(*time.Time) - if v != nil { - return v.Format(format), nil - } - } - - return nil, nil -} - -// Generates an input[type=date] with default transformers -func NewFieldDate(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "date")). - WithBeforeMount(func(data any) (any, error) { - return DateBeforeMount(data, "2006-01-02") - }). - WithBeforeBind(func(data any) (any, error) { - return time.Parse(time.DateOnly, data.(string)) - }) - - return f -} - -// Generates an input[type=datetime] with default transformers -func NewFieldDatetime(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "datetime")). - WithBeforeMount(func(data any) (any, error) { - return DateBeforeMount(data, "2006-01-02 15:04") - }). - WithBeforeBind(func(data any) (any, error) { - return time.Parse("2006-01-02T15:04", data.(string)) - }) - - return f -} - -// Generates an input[type=datetime-local] with default transformers -func NewFieldDatetimeLocal(name string) *Field { - f := NewField(name, "input"). - WithOptions( - NewOption("type", "datetime-local"), - ). - WithBeforeMount(func(data any) (any, error) { - return DateBeforeMount(data, "2006-01-02 15:04") - }). - WithBeforeBind(func(data any) (any, error) { - a, b := time.Parse("2006-01-02T15:04", data.(string)) - - return a, b - }) - - return f -} - -// Generates an input[type=time] with default transformers -func NewFieldTime(name string) *Field { - f := NewField(name, "input"). - WithOptions(NewOption("type", "time")). - WithBeforeMount(func(data any) (any, error) { - return DateBeforeMount(data, "15:04") - }). - WithBeforeBind(func(data any) (any, error) { - if data != nil { - v := data.(string) - - if len(v) > 0 { - return time.Parse(time.TimeOnly, fmt.Sprintf("%s:00", v)) - } - } - - return nil, nil - }) - - return f -} diff --git a/form/field_subform.go b/form/field_subform.go deleted file mode 100644 index 6762ef6..0000000 --- a/form/field_subform.go +++ /dev/null @@ -1,27 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// Generates a sub form -func NewFieldSubForm(name string) *Field { - f := NewField(name, "sub_form") - - return f -} - -func NewSubForm(name string) *Field { - return NewFieldSubForm(name) -} diff --git a/form/field_textarea.go b/form/field_textarea.go deleted file mode 100644 index b9045bb..0000000 --- a/form/field_textarea.go +++ /dev/null @@ -1,21 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// Generates a textarea -func NewFieldTextarea(name string) *Field { - return NewField(name, "textarea") -} diff --git a/form/form.go b/form/form.go deleted file mode 100644 index 04016d3..0000000 --- a/form/form.go +++ /dev/null @@ -1,321 +0,0 @@ -package form - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "encoding/json" - "io/ioutil" - "maps" - "net/http" - "net/url" - "slices" - "strings" - - "github.com/mitchellh/mapstructure" - "gitnet.fr/deblan/go-form/util" - "gitnet.fr/deblan/go-form/validation" -) - -// Field represents a form -type Form struct { - Fields []*Field `json:"children"` - GlobalFields []*Field `json:"-"` - Errors []validation.Error `json:"-"` - Method string `json:"method"` - JsonRequest bool `json:"json_request"` - Action string `json:"action"` - Name string `json:"name"` - Options []*Option `json:"options"` - RequestData *url.Values `json:"-"` -} - -// Generates a new form with default properties -func NewForm(fields ...*Field) *Form { - f := new(Form) - f.Method = "POST" - f.Name = "form" - f.Add(fields...) - f.WithOptions( - NewOption("attr", Attrs{}), - NewOption("help_attr", Attrs{}), - ) - - return f -} - -// Checks if the form contains an option using its name -func (f *Form) HasOption(name string) bool { - for _, option := range f.Options { - if option.Name == name { - return true - } - } - - return false -} - -// Returns an option using its name -func (f *Form) GetOption(name string) *Option { - for _, option := range f.Options { - if option.Name == name { - return option - } - } - - return nil -} - -// Resets the form errors -func (f *Form) ResetErrors() *Form { - f.Errors = []validation.Error{} - - return f -} - -// Appends children -func (f *Form) Add(fields ...*Field) { - for _, field := range fields { - field.Form = f - f.Fields = append(f.Fields, field) - } -} - -// Configures its children deeply -// This function must be called after adding all fields -func (f *Form) End() *Form { - f.GlobalFields = []*Field{} - - for _, c := range f.Fields { - f.AddGlobalField(c) - } - - return f -} - -// Configures its children deeply -func (f *Form) AddGlobalField(field *Field) { - f.GlobalFields = append(f.GlobalFields, field) - - for _, c := range field.Children { - f.AddGlobalField(c) - } -} - -// Checks if the form contains a child using its name -func (f *Form) HasField(name string) bool { - for _, field := range f.Fields { - if name == field.Name { - return true - } - } - - return false -} - -// Returns a child using its name -func (f *Form) GetField(name string) *Field { - var result *Field - - for _, field := range f.Fields { - if name == field.Name { - result = field - break - } - } - - return result -} - -// Sets the method of the format (http.MethodPost, http.MethodGet, ...) -func (f *Form) WithMethod(v string) *Form { - f.Method = v - - return f -} - -// Sets the name of the form (used to compute name of fields) -func (f *Form) WithName(v string) *Form { - f.Name = v - - return f -} - -// Sets the action of the form (eg: "/") -func (f *Form) WithAction(v string) *Form { - f.Action = v - - return f -} - -// Appends options to the form -func (f *Form) WithOptions(options ...*Option) *Form { - for _, option := range options { - if f.HasOption(option.Name) { - f.GetOption(option.Name).Value = option.Value - } else { - f.Options = append(f.Options, option) - } - } - - return f -} - -// Checks the a form is valid -func (f *Form) IsValid() bool { - isValid := true - f.ResetErrors() - - for _, field := range f.Fields { - fieldIsValid := field.Validate(field) - isValid = isValid && fieldIsValid - } - - return isValid -} - -// Copies datas from a struct to the form -func (f *Form) Mount(data any) error { - props, err := util.InspectStruct(data) - - if err != nil { - return err - } - - for key, value := range props { - if f.HasField(key) { - err = f.GetField(key).Mount(value) - - if err != nil { - return err - } - } - } - - return nil -} - -func (f *Form) WithJsonRequest() *Form { - f.JsonRequest = true - - return f -} - -// Copies datas from the form to a struct -func (f *Form) Bind(data any) error { - toBind := make(map[string]any) - - for _, field := range f.Fields { - field.Bind(toBind, nil, false) - } - - return mapstructure.Decode(toBind, data) -} - -// Processes a request -func (f *Form) HandleRequest(req *http.Request) { - var data url.Values - - if f.JsonRequest { - body, err := ioutil.ReadAll(req.Body) - - if err != nil { - return - } - - mapping := make(map[string]any) - err = json.Unmarshal(body, &mapping) - - if err != nil { - return - } - - data = url.Values{} - util.MapToUrlValues(&data, f.Name, mapping) - } else { - switch f.Method { - case "GET": - data = req.URL.Query() - default: - req.ParseForm() - data = req.Form - } - } - - isSubmitted := false - - type collectionData map[string]any - - for _, c := range f.GlobalFields { - if c.IsCollection { - collection := util.NewCollection() - - for key, _ := range data { - if strings.HasPrefix(key, c.GetName()) { - root := strings.Replace(key, c.GetName(), "", 1) - indexes := util.ExtractDataIndexes(root) - - collection.Add(indexes, data.Get(key)) - } - } - - c.Mount(collection.Slice()) - } else if data.Has(c.GetName()) { - isSubmitted = true - - if c.IsSlice { - c.Mount(data[c.GetName()]) - } else { - c.Mount(data.Get(c.GetName())) - } - } - } - - if isSubmitted { - f.RequestData = &data - } -} - -// Checks if the form is submitted -func (f *Form) IsSubmitted() bool { - return f.RequestData != nil -} - -// Generates a tree of errors -func (f *Form) ErrorsTree() map[string]any { - errors := make(map[string]any) - - for _, field := range f.Fields { - field.ErrorsTree(errors, nil) - } - - return map[string]any{ - "errors": f.Errors, - "children": slices.Collect(maps.Values(errors)), - } -} - -func (f *Form) Copy() *Form { - var fields []*Field - - for _, i := range f.Fields { - f := *i - fields = append(fields, &f) - } - - return &Form{ - Fields: fields, - } -} diff --git a/form/option.go b/form/option.go deleted file mode 100644 index 95eee67..0000000 --- a/form/option.go +++ /dev/null @@ -1,78 +0,0 @@ -package form - -import "strings" - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -type Option struct { - Name string `json:"name"` - Value any `json:"value"` -} - -func NewOption(name string, value any) *Option { - return &Option{ - Name: name, - Value: value, - } -} - -func (o *Option) AsBool() bool { - return o.Value.(bool) -} - -func (o *Option) AsString() string { - return o.Value.(string) -} - -func (o *Option) AsAttrs() Attrs { - return o.Value.(Attrs) -} - -type Attrs map[string]string - -func (a Attrs) Append(name, value string) { - v, ok := a[name] - - if !ok { - v = value - } else { - v = value + " " + v - } - - a[name] = v -} - -func (a Attrs) Prepend(name, value string) { - v, ok := a[name] - - if !ok { - v = value - } else { - v += " " + value - } - - a[name] = v -} - -func (a Attrs) Remove(name, value string) { - v, ok := a[name] - - if !ok { - v = strings.ReplaceAll(v, value, "") - } - - a[name] = v -} diff --git a/go.mod b/go.mod index 20dda1e..0d1a3b6 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,5 @@ -module gitnet.fr/deblan/go-form +module gitnet.fr/deblan/go-form-doc go 1.23.0 -require ( - github.com/iancoleman/strcase v0.3.0 - github.com/mitchellh/mapstructure v1.5.0 - github.com/spf13/cast v1.9.2 - github.com/yassinebenaid/godump v0.11.1 - maragu.dev/gomponents v1.1.0 -) +require github.com/imfing/hextra v0.9.7 // indirect diff --git a/go.sum b/go.sum index c6c8a71..8501da6 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,2 @@ -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= -github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= -github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI= -github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44= -maragu.dev/gomponents v1.1.0 h1:iCybZZChHr1eSlvkWp/JP3CrZGzctLudQ/JI3sBcO4U= -maragu.dev/gomponents v1.1.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM= +github.com/imfing/hextra v0.9.7 h1:Zg5n24us36Bn/S/5mEUPkRW6uwE6vHHEqWSgN0bPXaM= +github.com/imfing/hextra v0.9.7/go.mod h1:cEfel3lU/bSx7lTE/+uuR4GJaphyOyiwNR3PTqFTXpI= diff --git a/hugo.yaml b/hugo.yaml new file mode 100644 index 0000000..6211413 --- /dev/null +++ b/hugo.yaml @@ -0,0 +1,104 @@ +baseURL: https://deblan.gitnet.page/go-form/ +languageCode: en-us +title: deblan/go-form + +outputs: + home: [HTML] + page: [HTML] + section: [HTML, RSS] + +enableRobotsTXT: true +hasCJKLanguage: true +enableInlineShortcodes: true + +menu: + main: + - identifier: documentation + name: Documentation + pageRef: /docs + weight: 1 + - name: Search + weight: 6 + params: + type: search + - name: GitHub + weight: 7 + url: "https://gitnet.fr/deblan/go-form" + params: + icon: github + + sidebar: [] + # - identifier: installation + # name: Installation + # weight: 2 + + +params: + description: Creating and processing HTML forms in golang + navbar: + displayTitle: true + displayLogo: true + logo: + path: images/logo.svg + dark: images/logo-dark.svg + # width: 40 + # height: 20 + # link: / + width: full + + page: + width: full + + theme: + # light | dark | system + default: system + displayToggle: true + + footer: + enable: true + displayCopyright: false + displayPoweredBy: false + width: normal + + # Display the last modification date + displayUpdatedDate: true + dateFormat: "January 2, 2006" + + search: + enable: true + type: flexsearch + + flexsearch: + # index page by: content | summary | heading | title + index: content + # full | forward | reverse | strict + # https://github.com/nextapps-de/flexsearch/#tokenizer-prefix-search + tokenize: forward + + toc: + displayTags: true + + highlight: + copy: + enable: true + # hover | always + display: hover + +markup: + highlight: + noClasses: false + goldmark: + renderer: + unsafe: true + extensions: + passthrough: + delimiters: + block: [['\[', '\]'], ['$$', '$$']] + inline: [['\(', '\)']] + enable: true + +enableInlineShortcodes: true + +module: + imports: + - path: github.com/imfing/hextra diff --git a/layouts/partials/custom/footer.html b/layouts/partials/custom/footer.html new file mode 100644 index 0000000..092814c --- /dev/null +++ b/layouts/partials/custom/footer.html @@ -0,0 +1 @@ + diff --git a/layouts/shortcodes/goplay-auto-import-main.html b/layouts/shortcodes/goplay-auto-import-main.html new file mode 100644 index 0000000..adc4f0b --- /dev/null +++ b/layouts/shortcodes/goplay-auto-import-main.html @@ -0,0 +1,28 @@ +{{ $id := md5 .Inner }} {{ .Inner }} + +
+ Test me + +
+
+ + + +
+
+ + diff --git a/layouts/shortcodes/goplay-field.html b/layouts/shortcodes/goplay-field.html new file mode 100644 index 0000000..177a83b --- /dev/null +++ b/layouts/shortcodes/goplay-field.html @@ -0,0 +1,28 @@ +{{ $id := md5 .Inner }} {{ .Inner }} + +
+ Test me + +
+
+ + + +
+
+ + diff --git a/static/js/custom.js b/static/js/custom.js new file mode 100644 index 0000000..e8d886e --- /dev/null +++ b/static/js/custom.js @@ -0,0 +1,126 @@ +function normalizeCode(code) { + return code + .trim() + .replace(/^```go/, '') + .replace(/^```/, '') + .replace(/```$/, '') + .replace(/<([^>]+)>/g, '') + .trim() +} + +function updatePlaygroundView(goplay, id, code) { + const textarea = document.getElementById(`textarea-${id}`) + + textarea.value = code + textarea.style.height = textarea.scrollHeight + 'px'; + textarea.style.overflowY = 'hidden'; + + textarea.addEventListener('input', function() { + this.style.height = 'auto'; + this.style.height = this.scrollHeight + 'px'; + }) + + const toolRun = document.getElementById(`hugo-goplay-tool-${id}-run`) + const toolTry = document.getElementById(`hugo-goplay-tool-${id}-try`) + const toolShare = document.getElementById(`hugo-goplay-tool-${id}-share`) + + toolRun.addEventListener('click', () => { + const parent = document.getElementById(id) + const pre = document.createElement('pre') + const container = document.createElement('code') + + container.classList.add('text') + pre.appendChild(container) + parent.replaceChildren(pre) + + goplay.renderCompile(container, textarea.value); + }); + + [toolTry, toolShare].forEach((v) => { + v.addEventListener('click', async () => { + const shareUrl = await goplay.share(code) + window.open(shareUrl, '_blank').focus(); + }) + }); +} + +function createFieldPlayground(goplay, id, code) { + code = createFieldPlaygroundCode(code) + + updatePlaygroundView(goplay, id, code) +} + +function createPlaygroundWithAutoImportMail(goplay, id, code) { + let lines = normalizeCode(code).split("\n") + let results = ["package main", ""] + let imports = [] + let body = [] + + lines.forEach((line) => { + if (line.substr(0, 7) === '@import') { + imports.push(' ' + line.replace('@import', '').trim()) + } else { + body.push(" " + line) + } + }) + + if (imports.length > 0) { + results.push("import (") + + imports.forEach((v) => { + results.push(v) + }) + + results.push(")", "") + } + + results.push("func main() {") + + body.forEach((v, i) => { + if (i == 0 && v.trim() == "") { + return + } + + results.push(v) + }) + + results.push("}", "") + + updatePlaygroundView(goplay, id, results.join("\n")) +} + +function createFieldPlaygroundCode(code) { + code = normalizeCode(code) + let lines = code.split("\n"); + + for (let i in lines) { + lines[i] = [' ', lines[i]].join('') + } + + code = lines.join("\n"); + + return `package main + +import ( + "fmt" + "html/template" + "strings" + + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/theme" +) + +func main() { +${code} + + r(form.NewForm(field)) +} + +func r(f *form.Form) { + render := theme.NewRenderer(theme.Html5) + tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse(\`{{ form_widget (.Form.GetField "Foo") }}\`) + b := new(strings.Builder) + tpl.Execute(b, map[string]any{"Form": f}) + fmt.Println(b.String()) +}` +} diff --git a/theme/bootstrap5.go b/theme/bootstrap5.go deleted file mode 100644 index 525bdb8..0000000 --- a/theme/bootstrap5.go +++ /dev/null @@ -1,134 +0,0 @@ -package theme - -import ( - "gitnet.fr/deblan/go-form/form" - "gitnet.fr/deblan/go-form/validation" - . "maragu.dev/gomponents" - . "maragu.dev/gomponents/html" -) - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -var Bootstrap5 = ExtendTheme(Html5, func() map[string]RenderFunc { - theme := make(map[string]RenderFunc) - - theme["form_help"] = func(parent map[string]RenderFunc, args ...any) Node { - form := args[0].(*form.Form) - - form.GetOption("help_attr").AsAttrs().Append("class", "form-text") - - return parent["base_form_help"](parent, form) - } - - theme["form_widget_help"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - field.GetOption("help_attr").AsAttrs().Append("class", "form-text") - - return parent["base_form_widget_help"](parent, field) - } - - theme["input"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - fieldType := field.GetOption("type").AsString() - - var class string - - if fieldType == "checkbox" || fieldType == "radio" { - class = "form-check-input" - } else if fieldType == "range" { - class = "form-range" - } else if fieldType == "button" || fieldType == "submit" || fieldType == "reset" { - class = "btn" - } else { - class = "form-control" - } - - field.GetOption("attr").AsAttrs().Append("class", class) - - return parent["base_input"](parent, field) - } - - theme["form_label"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - var class string - - if field.Widget == "choice" && field.HasOption("expanded") && field.GetOption("expanded").AsBool() { - class = "form-check-label" - } else { - class = "form-label" - } - - field.GetOption("label_attr").AsAttrs().Append("class", class) - - return parent["base_form_label"](parent, field) - } - - theme["choice"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - if !field.HasOption("expanded") || !field.GetOption("expanded").AsBool() { - field.GetOption("attr").AsAttrs().Append("class", "form-control") - } - - return parent["base_choice"](parent, field) - } - - theme["choice_expanded_item"] = func(parent map[string]RenderFunc, args ...any) Node { - return Div(Class("form-check"), args[0].(Node)) - } - - theme["textarea"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - field.GetOption("attr").AsAttrs().Append("class", "form-control") - - return parent["base_textarea"](parent, field) - } - - theme["form_row"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - if field.HasOption("type") { - fieldType := field.GetOption("type").AsString() - - if fieldType == "checkbox" || fieldType == "radio" { - field.GetOption("row_attr").AsAttrs().Append("class", "form-check") - } - } - - return parent["base_form_row"](parent, field) - } - - theme["errors"] = func(parent map[string]RenderFunc, args ...any) Node { - errors := args[0].([]validation.Error) - - var result []Node - - for _, v := range errors { - result = append(result, Text(string(v))) - result = append(result, Br()) - } - - return Div( - Class("invalid-feedback d-block"), - Group(result), - ) - } - - return theme -}) diff --git a/theme/html5.go b/theme/html5.go deleted file mode 100644 index 2d7889e..0000000 --- a/theme/html5.go +++ /dev/null @@ -1,466 +0,0 @@ -package theme - -import ( - "bytes" - "fmt" - - "github.com/spf13/cast" - "gitnet.fr/deblan/go-form/form" - "gitnet.fr/deblan/go-form/validation" - . "maragu.dev/gomponents" - . "maragu.dev/gomponents/html" -) - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -var Html5 = CreateTheme(func() map[string]RenderFunc { - theme := make(map[string]RenderFunc) - - theme["attributes"] = func(parent map[string]RenderFunc, args ...any) Node { - var result []Node - - for i, v := range args[0].(form.Attrs) { - result = append(result, Attr(i, v)) - } - - return Group(result) - } - - theme["form_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { - form := args[0].(*form.Form) - - if !form.HasOption("attr") { - return Raw("") - } - - return parent["attributes"](parent, form.GetOption("attr").AsAttrs()) - } - - theme["errors"] = func(parent map[string]RenderFunc, args ...any) Node { - errors := args[0].([]validation.Error) - - var result []Node - - for _, v := range errors { - result = append(result, Li(Text(string(v)))) - } - - return Ul( - Class("gf-errors"), - Group(result), - ) - } - - theme["form_errors"] = func(parent map[string]RenderFunc, args ...any) Node { - form := args[0].(*form.Form) - - return If( - len(form.Errors) > 0, - parent["errors"](parent, form.Errors), - ) - } - - theme["form_widget_errors"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - return If( - len(field.Errors) > 0, - parent["errors"](parent, field.Errors), - ) - } - - theme["help"] = func(parent map[string]RenderFunc, args ...any) Node { - help := args[0].(string) - var extra Node - - if len(help) == 0 { - return Raw("") - } - - if len(args) == 2 { - extra = args[1].(Node) - } - - return Div( - Class("gf-help"), - Text(help), - extra, - ) - } - - theme["form_help"] = func(parent map[string]RenderFunc, args ...any) Node { - form := args[0].(*form.Form) - - if !form.HasOption("help") { - return Raw("") - } - - return parent["help"]( - parent, - form.GetOption("help").AsString(), - parent["attributes"](parent, form.GetOption("help_attr").AsAttrs()), - ) - } - - theme["form_widget_help"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - if !field.HasOption("help") { - return Raw("") - } - - return parent["help"]( - parent, - field.GetOption("help").AsString(), - parent["attributes"](parent, field.GetOption("help_attr").AsAttrs()), - ) - } - - theme["label_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - return parent["attributes"](parent, field.GetOption("label_attr").AsAttrs()) - } - - theme["form_label"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - if !field.HasOption("label") { - return Raw("") - } - - label := field.GetOption("label").AsString() - - return If(len(label) > 0, Label( - For(field.GetId()), - parent["label_attributes"](parent, field), - Text(label), - )) - } - - theme["field_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - return parent["attributes"](parent, field.GetOption("attr").AsAttrs()) - } - - theme["textarea_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { - return parent["field_attributes"](parent, args...) - } - - theme["input_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { - return parent["field_attributes"](parent, args...) - } - - theme["sub_form_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { - return parent["field_attributes"](parent, args...) - } - - theme["input"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - fieldType := "text" - - if field.HasOption("type") { - fieldType = field.GetOption("type").AsString() - } - - value := cast.ToString(field.Data) - - if fieldType == "checkbox" { - value = "1" - } - - return Input( - Name(field.GetName()), - ID(field.GetId()), - Type(fieldType), - Value(value), - If(fieldType == "checkbox" && field.Data != nil && field.Data != false, Checked()), - If(field.HasOption("required") && field.GetOption("required").AsBool(), Required()), - parent["input_attributes"](parent, field), - ) - } - - theme["choice_options"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - choices := field.GetOption("choices").Value.(*form.Choices) - - isRequired := field.HasOption("required") && field.GetOption("required").AsBool() - isMultiple := field.GetOption("multiple").AsBool() - - var options []Node - - if !isMultiple && !isRequired { - options = append(options, Option( - Text(field.GetOption("empty_choice_label").AsString()), - )) - } - - for _, choice := range choices.GetChoices() { - options = append(options, Option( - Value(choice.Value), - Text(choice.Label), - If(choices.Match(field, choice.Value), Selected()), - )) - } - - return Group(options) - } - - theme["choice_expanded_item"] = func(parent map[string]RenderFunc, args ...any) Node { - return args[0].(Node) - } - - theme["choice_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { - return parent["field_attributes"](parent, args...) - } - - theme["choice_expanded"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - choices := field.GetOption("choices").Value.(*form.Choices) - - isRequired := field.HasOption("required") && field.GetOption("required").AsBool() - isMultiple := field.GetOption("multiple").AsBool() - noneLabel := field.GetOption("empty_choice_label").AsString() - - var items []Node - - if !isMultiple && !isRequired { - id := fmt.Sprintf("%s-%s", field.GetId(), "none") - - items = append(items, parent["choice_expanded_item"](parent, Group([]Node{ - Input( - Name(field.GetName()), - ID(id), - Value(""), - Type("radio"), - parent["choice_attributes"](parent, field), - If(cast.ToString(field.Data) == "", Checked()), - ), - Label( - For(id), - Text(noneLabel), - parent["label_attributes"](parent, field), - ), - }))) - } - - for key, choice := range choices.GetChoices() { - id := fmt.Sprintf("%s-%d", field.GetId(), key) - - items = append(items, parent["choice_expanded_item"](parent, Group([]Node{ - Input( - Name(field.GetName()), - ID(id), - Value(choice.Value), - If(isMultiple, Type("checkbox")), - If(!isMultiple, Type("radio")), - parent["choice_attributes"](parent, field), - If(choices.Match(field, choice.Value), Checked()), - ), - Label( - For(id), - Text(choice.Label), - parent["label_attributes"](parent, field), - ), - }))) - } - - return Group(items) - } - - theme["choice"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - isRequired := field.HasOption("required") && field.GetOption("required").AsBool() - isExpanded := field.GetOption("expanded").AsBool() - isMultiple := field.GetOption("multiple").AsBool() - noneLabel := field.GetOption("empty_choice_label").AsString() - - _ = noneLabel - - if isExpanded { - return parent["choice_expanded"](parent, field) - } else { - return Select( - ID(field.GetId()), - If(isRequired, Required()), - If(isMultiple, Multiple()), - Name(field.GetName()), - parent["choice_attributes"](parent, field), - parent["choice_options"](parent, field), - ) - } - } - - theme["textarea"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - return Textarea( - Name(field.GetName()), - ID(field.GetId()), - If(field.HasOption("required") && field.GetOption("required").AsBool(), Required()), - parent["textarea_attributes"](parent, field), - Text(cast.ToString(field.Data)), - ) - } - - theme["sub_form_label"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - if !field.HasOption("label") { - return Raw("") - } - - label := field.GetOption("label").AsString() - - return If(len(label) > 0, Legend( - parent["label_attributes"](parent, field), - Text(label), - )) - - } - - theme["sub_form_content"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - return parent["form_fields"](parent, field.Children) - } - - theme["sub_form"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - return FieldSet( - ID(field.GetId()), - parent["sub_form_label"](parent, field), - parent["sub_form_attributes"](parent, field), - parent["sub_form_content"](parent, field), - ) - } - - theme["collection"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - var prototype string - - if opt := field.GetOption("form"); opt != nil { - if val, ok := opt.Value.(*form.Form); ok { - var buffer bytes.Buffer - dest := form.NewFieldSubForm(field.Name) - - for _, c := range val.Fields { - child := c.Copy() - child.NamePrefix = "[__name__]" - dest.Add(child) - } - - fieldPrototype := parent["form_row"](parent, dest) - fieldPrototype.Render(&buffer) - - prototype = buffer.String() - } - } - - field.WithOptions(form.NewOption("prototype", prototype)) - field.Widget = "collection_build" - - return parent["form_widget"](parent, field) - } - - theme["collection_build"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - prototype := field.GetOption("prototype").AsString() - var items []Node - - for _, child := range field.Children { - items = append(items, parent["form_row"](parent, child)) - } - - return Div( - Attr("data-prototype", prototype), - Group(items), - ) - } - - theme["form_widget"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - tpl, ok := parent[field.Widget] - - if !ok { - return Raw("Invalid field widget: " + field.Widget) - } - - return tpl(parent, field) - } - - theme["form_row"] = func(parent map[string]RenderFunc, args ...any) Node { - field := args[0].(*form.Field) - - isCheckbox := field.HasOption("type") && field.GetOption("type").AsString() == "checkbox" - hasChildren := len(field.Children) > 0 - labelAfter := isCheckbox && !hasChildren - label := parent["form_label"](parent, field) - attrs := Raw("") - - if field.HasOption("row_attr") { - attrs = parent["attributes"](parent, field.GetOption("row_attr").AsAttrs()) - } - - return Div( - attrs, - If(!labelAfter, label), - parent["form_widget_errors"](parent, field), - parent["form_widget"](parent, field), - If(labelAfter, label), - parent["form_widget_help"](parent, field), - ) - } - - theme["form_fields"] = func(parent map[string]RenderFunc, args ...any) Node { - var items []Node - - for _, item := range args[0].([]*form.Field) { - items = append(items, parent["form_row"](parent, item)) - } - - return Group(items) - } - - theme["form_content"] = func(parent map[string]RenderFunc, args ...any) Node { - form := args[0].(*form.Form) - - return Group([]Node{ - parent["form_errors"](parent, form), - parent["form_help"](parent, form), - parent["form_fields"](parent, form.Fields), - }) - } - - theme["form"] = func(parent map[string]RenderFunc, args ...any) Node { - form := args[0].(*form.Form) - - return Form( - Class("gf-form"), - Action(form.Action), - Method(form.Method), - parent["form_attributes"](parent, form), - parent["form_content"](parent, form), - ) - } - - return theme -}) diff --git a/theme/renderer.go b/theme/renderer.go deleted file mode 100644 index d90b7b7..0000000 --- a/theme/renderer.go +++ /dev/null @@ -1,84 +0,0 @@ -package theme - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "bytes" - "html/template" - - "gitnet.fr/deblan/go-form/form" - "maragu.dev/gomponents" -) - -type RenderFunc func(parent map[string]RenderFunc, args ...any) gomponents.Node - -type Renderer struct { - Theme map[string]RenderFunc -} - -func NewRenderer(theme map[string]RenderFunc) *Renderer { - r := new(Renderer) - r.Theme = theme - - return r -} - -func toTemplateHtml(n gomponents.Node) template.HTML { - var buf bytes.Buffer - - n.Render(&buf) - - return template.HTML(buf.String()) -} - -func (r *Renderer) RenderForm(form *form.Form) template.HTML { - return toTemplateHtml(r.Theme["form"](r.Theme, form)) -} - -func (r *Renderer) FuncMap() template.FuncMap { - funcs := template.FuncMap{} - - for _, name := range []string{"form", "form_errors"} { - funcs[name] = func(form *form.Form) template.HTML { - return toTemplateHtml(r.Theme[name](r.Theme, form)) - } - } - - for _, name := range []string{"form_row", "form_widget", "form_label", "form_widget_errors"} { - funcs[name] = func(field *form.Field) template.HTML { - return toTemplateHtml(r.Theme[name](r.Theme, field)) - } - } - - return funcs -} - -func (r *Renderer) Render(name, tpl string, args any) template.HTML { - t, err := template.New(name).Funcs(r.FuncMap()).Parse(tpl) - - if err != nil { - return template.HTML(err.Error()) - } - - var buf bytes.Buffer - err = t.Execute(&buf, args) - - if err != nil { - return template.HTML(err.Error()) - } - - return template.HTML(buf.String()) -} diff --git a/theme/theme.go b/theme/theme.go deleted file mode 100644 index 9416346..0000000 --- a/theme/theme.go +++ /dev/null @@ -1,21 +0,0 @@ -package theme - -func CreateTheme(generator func() map[string]RenderFunc) map[string]RenderFunc { - return generator() -} - -func ExtendTheme(base map[string]RenderFunc, generator func() map[string]RenderFunc) map[string]RenderFunc { - extended := CreateTheme(generator) - - for i, v := range base { - _, ok := extended[i] - - if ok { - extended["base_"+i] = v - } else { - extended[i] = v - } - } - - return extended -} diff --git a/util/collection.go b/util/collection.go deleted file mode 100644 index f0aa649..0000000 --- a/util/collection.go +++ /dev/null @@ -1,111 +0,0 @@ -package util - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "regexp" - "strings" - - "github.com/spf13/cast" -) - -type CollectionValue struct { - Name string - Value string - Children map[string]*CollectionValue -} - -type Collection struct { - Children map[int]*CollectionValue -} - -func NewCollection() *Collection { - return &Collection{ - Children: make(map[int]*CollectionValue), - } -} - -func NewCollectionValue(name string) *CollectionValue { - return &CollectionValue{ - Name: name, - Children: make(map[string]*CollectionValue), - } -} - -func (c *Collection) Add(indexes []string, value string) { - firstIndex := cast.ToInt(indexes[0]) - size := len(indexes) - child := c.Children[firstIndex] - - if child == nil { - child = NewCollectionValue(indexes[0]) - c.Children[firstIndex] = child - } - - child.Add(indexes[1:size], value, nil) -} - -func (c *Collection) Slice() []any { - var result []any - - for _, child := range c.Children { - result = append(result, child.Map()) - } - - return result -} - -func (c *CollectionValue) Map() any { - if len(c.Children) == 0 { - return c.Value - } - - results := make(map[string]any) - - for _, child := range c.Children { - results[child.Name] = child.Map() - } - - return results -} - -func (c *CollectionValue) Add(indexes []string, value string, lastChild *CollectionValue) { - size := len(indexes) - - if size > 0 { - firstIndex := indexes[0] - child := c.Children[firstIndex] - - child = NewCollectionValue(indexes[0]) - c.Children[firstIndex] = child - - child.Add(indexes[1:size], value, child) - } else { - lastChild.Value = value - } -} - -func ExtractDataIndexes(value string) []string { - re := regexp.MustCompile(`\[[^\]]+\]`) - items := re.FindAll([]byte(value), -1) - var results []string - - for _, i := range items { - results = append(results, strings.Trim(string(i), "[]")) - } - - return results -} diff --git a/util/inspect.go b/util/inspect.go deleted file mode 100644 index 22fa4f4..0000000 --- a/util/inspect.go +++ /dev/null @@ -1,59 +0,0 @@ -package util - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "errors" - "reflect" - - "github.com/iancoleman/strcase" -) - -func InspectStruct(input interface{}) (map[string]interface{}, error) { - val := reflect.ValueOf(input) - - if val.Kind() == reflect.Ptr { - val = val.Elem() - } - - if val.Kind() == reflect.Map { - return input.(map[string]interface{}), nil - } - - if val.Kind() != reflect.Struct { - return nil, errors.New("Invalid type") - } - - result := make(map[string]interface{}) - - typ := val.Type() - for i := 0; i < val.NumField(); i++ { - field := typ.Field(i) - value := val.Field(i) - tags := typ.Field(i).Tag - name := field.Name - - fieldTag := tags.Get("field") - - if fieldTag == "lowerCamel" { - name = strcase.ToLowerCamel(name) - } - - result[name] = value.Interface() - } - - return result, nil -} diff --git a/util/transformer.go b/util/transformer.go deleted file mode 100644 index a942fa1..0000000 --- a/util/transformer.go +++ /dev/null @@ -1,57 +0,0 @@ -package util - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "fmt" - "net/url" -) - -func MapToUrlValues(values *url.Values, prefix string, data map[string]any) { - keyFormater := "%s" - - if prefix != "" { - keyFormater = prefix + "[%s]" - } - - for key, value := range data { - keyValue := fmt.Sprintf(keyFormater, key) - - switch v := value.(type) { - case string: - values.Add(keyValue, v) - case []string: - case []int: - case []int32: - case []int64: - case []any: - for _, s := range v { - values.Add(keyValue, fmt.Sprintf("%v", s)) - } - case bool: - if v { - values.Add(keyValue, "1") - } else { - values.Add(keyValue, "0") - } - case int, int64, float64: - values.Add(keyValue, fmt.Sprintf("%v", v)) - case map[string]any: - MapToUrlValues(values, keyValue, v) - default: - } - } -} diff --git a/validation/constraint.go b/validation/constraint.go deleted file mode 100644 index 8854921..0000000 --- a/validation/constraint.go +++ /dev/null @@ -1,20 +0,0 @@ -package validation - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -type Constraint interface { - Validate(data any) []Error -} diff --git a/validation/error.go b/validation/error.go deleted file mode 100644 index f73d341..0000000 --- a/validation/error.go +++ /dev/null @@ -1,18 +0,0 @@ -package validation - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -type Error string diff --git a/validation/iseven.go b/validation/iseven.go deleted file mode 100644 index 306cae2..0000000 --- a/validation/iseven.go +++ /dev/null @@ -1,34 +0,0 @@ -package validation - -import ( - "strconv" -) - -type IsEven struct { - Message string - TypeErrorMessage string -} - -func NewIsEven() IsEven { - return IsEven{ - Message: "This value is not an even number.", - TypeErrorMessage: "This value can not be processed.", - } -} - -func (c IsEven) Validate(data any) []Error { - errors := []Error{} - - // The constraint should not validate an empty data - if len(NewNotBlank().Validate(data)) == 0 { - i, err := strconv.Atoi(data.(string)) - - if err != nil { - errors = append(errors, Error(c.TypeErrorMessage)) - } else if i%2 != 0 { - errors = append(errors, Error(c.Message)) - } - } - - return errors -} diff --git a/validation/isodd.go b/validation/isodd.go deleted file mode 100644 index d7e3b53..0000000 --- a/validation/isodd.go +++ /dev/null @@ -1,34 +0,0 @@ -package validation - -import ( - "strconv" -) - -type IsOdd struct { - Message string - TypeErrorMessage string -} - -func NewIsOdd() IsOdd { - return IsOdd{ - Message: "This value is not a odd number.", - TypeErrorMessage: "This value can not be processed.", - } -} - -func (c IsOdd) Validate(data any) []Error { - errors := []Error{} - - // The constraint should not validate an empty data - if len(NewNotBlank().Validate(data)) == 0 { - i, err := strconv.Atoi(data.(string)) - - if err != nil { - errors = append(errors, Error(c.TypeErrorMessage)) - } else if i%2 != 1 { - errors = append(errors, Error(c.Message)) - } - } - - return errors -} diff --git a/validation/length.go b/validation/length.go deleted file mode 100644 index 66d5a1f..0000000 --- a/validation/length.go +++ /dev/null @@ -1,110 +0,0 @@ -package validation - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "reflect" - "strings" - - "github.com/spf13/cast" -) - -type Length struct { - Min *int - Max *int - MinMessage string - MaxMessage string - ExactMessage string - TypeErrorMessage string -} - -func NewLength() Length { - return Length{ - MinMessage: "This value is too short (min: {{ min }}).", - MaxMessage: "This value is too long (max: {{ max }}).", - ExactMessage: "This value is not valid (expected: {{ min }}).", - TypeErrorMessage: "This value can not be processed.", - } -} - -func (c Length) WithMin(v int) Length { - c.Min = &v - - return c -} - -func (c Length) WithMax(v int) Length { - c.Max = &v - - return c -} - -func (c Length) WithExact(v int) Length { - c.Min = &v - c.Max = &v - - return c -} - -func (c Length) Validate(data any) []Error { - if (c.Min == nil && c.Max == nil) || len(NewNotBlank().Validate(data)) != 0 { - return []Error{} - } - - errors := []Error{} - - t := reflect.TypeOf(data) - - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - var size *int - - switch t.Kind() { - case reflect.Array: - case reflect.Slice: - s := reflect.ValueOf(data).Len() - size = &s - case reflect.String: - s := len(data.(string)) - size = &s - - default: - errors = append(errors, Error(c.TypeErrorMessage)) - } - - if size != nil { - if c.Max != nil && c.Min != nil { - if *c.Max == *c.Min && *size != *c.Max { - errors = append(errors, Error(c.BuildMessage(c.ExactMessage))) - } - } else if c.Min != nil && *size < *c.Min { - errors = append(errors, Error(c.BuildMessage(c.MinMessage))) - } else if c.Max != nil && *size > *c.Max { - errors = append(errors, Error(c.BuildMessage(c.MaxMessage))) - } - } - - return errors -} - -func (c *Length) BuildMessage(message string) string { - message = strings.ReplaceAll(message, "{{ min }}", cast.ToString(c.Min)) - message = strings.ReplaceAll(message, "{{ max }}", cast.ToString(c.Max)) - - return message -} diff --git a/validation/mail.go b/validation/mail.go deleted file mode 100644 index 136ee6d..0000000 --- a/validation/mail.go +++ /dev/null @@ -1,42 +0,0 @@ -package validation - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import "net/mail" - -type Mail struct { - Message string -} - -func NewMail() Mail { - return Mail{ - Message: "This value is not a valid email address.", - } -} - -func (c Mail) Validate(data any) []Error { - errors := []Error{} - - if len(NewNotBlank().Validate(data)) == 0 { - _, err := mail.ParseAddress(data.(string)) - - if err != nil { - errors = append(errors, Error(c.Message)) - } - } - - return errors -} diff --git a/validation/notblank.go b/validation/notblank.go deleted file mode 100644 index 1962cb2..0000000 --- a/validation/notblank.go +++ /dev/null @@ -1,84 +0,0 @@ -package validation - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "reflect" - - "github.com/spf13/cast" -) - -type NotBlank struct { - Message string - TypeErrorMessage string -} - -func NewNotBlank() NotBlank { - return NotBlank{ - Message: "This value should not be blank.", - TypeErrorMessage: "This value can not be processed.", - } -} - -func (c NotBlank) Validate(data any) []Error { - isValid := true - errors := []Error{} - - v := reflect.ValueOf(data) - - if data == nil || v.IsZero() { - errors = append(errors, Error(c.Message)) - - return errors - } - - t := reflect.TypeOf(data) - - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - switch t.Kind() { - case reflect.Bool: - isValid = data == false - case reflect.Array: - case reflect.Slice: - isValid = reflect.ValueOf(data).Len() > 0 - case reflect.String: - isValid = len(data.(string)) > 0 - case reflect.Float32: - case reflect.Float64: - case reflect.Int: - case reflect.Int16: - case reflect.Int32: - case reflect.Int64: - case reflect.Int8: - case reflect.Uint: - case reflect.Uint16: - case reflect.Uint32: - case reflect.Uint64: - case reflect.Uint8: - isValid = cast.ToFloat64(data.(string)) == float64(0) - default: - errors = append(errors, Error(c.TypeErrorMessage)) - } - - if !isValid { - errors = append(errors, Error(c.Message)) - } - - return errors -} diff --git a/validation/range.go b/validation/range.go deleted file mode 100644 index ccb95b0..0000000 --- a/validation/range.go +++ /dev/null @@ -1,117 +0,0 @@ -package validation - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "reflect" - "strings" - - "github.com/spf13/cast" -) - -type Range struct { - Min *float64 - Max *float64 - MinMessage string - MaxMessage string - RangeMessage string - TypeErrorMessage string -} - -func NewRange() Range { - return Range{ - MinMessage: "This value must be greater than or equal to {{ min }}.", - MaxMessage: "This value must be less than or equal to {{ max }}.", - RangeMessage: "This value should be between {{ min }} and {{ max }}.", - TypeErrorMessage: "This value can not be processed.", - } -} - -func (c Range) WithMin(v float64) Range { - c.Min = &v - - return c -} - -func (c Range) WithMax(v float64) Range { - c.Max = &v - - return c -} - -func (c Range) WithRange(vMin, vMax float64) Range { - c.Min = &vMin - c.Max = &vMax - - return c -} - -func (c Range) Validate(data any) []Error { - if c.Min == nil && c.Max == nil || len(NewNotBlank().Validate(data)) != 0 { - return []Error{} - } - - errors := []Error{} - - t := reflect.TypeOf(data) - - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - switch t.Kind() { - case reflect.Float32: - case reflect.Float64: - case reflect.Int: - case reflect.Int16: - case reflect.Int32: - case reflect.Int64: - case reflect.Int8: - case reflect.Uint: - case reflect.Uint16: - case reflect.Uint32: - case reflect.Uint64: - case reflect.Uint8: - case reflect.String: - isValidMin := c.Min == nil || *c.Min <= cast.ToFloat64(data.(string)) - isValidMax := c.Max == nil || *c.Max >= cast.ToFloat64(data.(string)) - - if !isValidMin || !isValidMax { - errors = append(errors, Error(c.BuildMessage())) - } - default: - errors = append(errors, Error(c.TypeErrorMessage)) - } - - return errors -} - -func (c *Range) BuildMessage() string { - var message string - - if c.Min != nil && c.Max == nil { - message = c.MinMessage - } else if c.Max != nil && c.Min == nil { - message = c.MaxMessage - } else { - message = c.RangeMessage - } - - message = strings.ReplaceAll(message, "{{ min }}", cast.ToString(c.Min)) - message = strings.ReplaceAll(message, "{{ max }}", cast.ToString(c.Max)) - - return message -} diff --git a/validation/regex.go b/validation/regex.go deleted file mode 100644 index 28815b1..0000000 --- a/validation/regex.go +++ /dev/null @@ -1,75 +0,0 @@ -package validation - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import ( - "reflect" - "regexp" -) - -type Regex struct { - Message string - TypeErrorMessage string - Match bool - Expression string -} - -func NewRegex(expr string) Regex { - return Regex{ - Message: "This value is not valid.", - TypeErrorMessage: "This value can not be processed.", - Match: true, - Expression: expr, - } -} - -func (c Regex) MustMatch() Regex { - c.Match = true - - return c -} - -func (c Regex) MustNotMatch() Regex { - c.Match = false - - return c -} - -func (c Regex) Validate(data any) []Error { - errors := []Error{} - - if len(NewNotBlank().Validate(data)) == 0 { - t := reflect.TypeOf(data) - - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - switch t.Kind() { - case reflect.String: - matched, _ := regexp.MatchString(c.Expression, data.(string)) - - if !matched && c.Match || matched && !c.Match { - errors = append(errors, Error(c.Message)) - } - - default: - errors = append(errors, Error(c.TypeErrorMessage)) - } - } - - return errors -} diff --git a/validation/validation.go b/validation/validation.go deleted file mode 100644 index 97dfd17..0000000 --- a/validation/validation.go +++ /dev/null @@ -1,28 +0,0 @@ -package validation - -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -func Validate(data any, constraints []Constraint) (bool, []Error) { - errs := []Error{} - - for _, constraint := range constraints { - for _, e := range constraint.Validate(data) { - errs = append(errs, e) - } - } - - return len(errs) == 0, errs -}