diff --git a/.envrc b/.envrc new file mode 100755 index 0000000..6f6af1f --- /dev/null +++ b/.envrc @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# ^ added for shellcheck and file-type detection + +# This installs nix-direnv for users that don't already have it. +if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" +fi + +if [[ $(type -t use_flake) != function ]]; then + echo "ERROR: use_flake function missing." + echo "Please update direnv to v2.30.0 or later." + exit 1 +fi + +# Pull in a generic elixir flake template +use flake "github:the-nix-way/dev-templates?dir=elixir" + +export PORT=4000 + +# Add our own customisations on top +use flake + + +# Local Variables: +# mode: envrc-file +# End: diff --git a/.gitignore b/.gitignore index f300b35..8d17467 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /db /deps /*.ez +/result # Generated on crash by the VM erl_crash.dump @@ -17,3 +18,4 @@ erl_crash.dump priv/static/docsets/*.tgz priv/static/docsets/*/* priv/static/docsets/*.tar.gz +/.direnv/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fa379a6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,45 @@ +# Code of Conduct + +## Why have a Code of Conduct? + +As contributors and maintainers of this project, we are committed to providing a friendly, safe and welcoming environment for all, regardless of age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. + +The goal of the Code of Conduct is to specify a baseline standard of behavior so that people with different social values and communication styles can talk about Hexdocs docset API effectively, productively, and respectfully, even in face of disagreements. The Code of Conduct also provides a mechanism for resolving conflicts in the community when they arise. + +## Our Values + +These are the values Hexdocs docset API developers should aspire to: + + * Be friendly and welcoming + * Be kind + * Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) + * Interpret the arguments of others in good faith, do not seek to disagree. + * When we do disagree, try to understand why. + * Be thoughtful + * Productive communication requires effort. Think about how your words will be interpreted. + * Remember that sometimes it is best to refrain entirely from commenting. + * Be respectful + * In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively. + * Be constructive + * Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation. + * Avoid unconstructive criticism: don't merely decry the current state of affairs; offer — or at least solicit — suggestions as to how things may be improved. + * Avoid harsh words and stern tone: we are all aligned towards the well-being of the community and the progress of the ecosystem. Harsh words exclude, demotivate, and lead to unnecessary conflict. + * Avoid snarking (pithy, unproductive, sniping comments). + * Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults towards a project, person or group). + * Be responsible + * What you say and do matters. Take responsibility for your words and actions, including their consequences, whether intended or otherwise. + +The following actions are explicitly forbidden: + + * Insulting, demeaning, hateful, or threatening remarks. + * Discrimination based on age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. + * Bullying or systematic harassment. + * Unwelcome sexual advances. + * Incitement to any of these. + + +## Acknowledgements + +This document was based on the Code of Conduct from the Elixir project. + +https://github.com/elixir-lang/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2122244 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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. + + Hexdocs docset API + Copyright (C) 2022 hissssst and johnhamelink + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Hexdocs docset API Copyright (C) 2022 hissssst and johnhamelink + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index d02a90c..872e2fc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@ # DocsetApi +## **DocsetApi HAS MOVED** + +DocsetApi is now being maintained over at https://github.com/hissssst/hexdocs_docset_api/ + +--- + An API that produces docset packages (for https://zealdocs.org & dash.app) from any Elixir app or library with hexdocs documentation. ## Feed API -1. Start the server with `mix phx.start` +1. Start the server with `PORT=8080 mix phx.server` (or whatever port is available on your machine) 2. In Zeal, go to tools -> docsets -> Installed -> Add Feed 3. Insert the url in the format, `http://localhost:8080/feeds/` @@ -25,10 +31,10 @@ Add DocsetApi as a dependecy in `mix.exs`: Put a mix task like this one in your app: ```elixir -defmodule Mix.Tasks.MyApp.GenerateDocsets do +defmodule Mix.Tasks.Hello.GenerateDocsets do use Mix.Task - @usage "mix my_app.generate_docsets PATH" + @usage "mix hello.generate_docsets PATH" @shortdoc "Generate Dash-compatible docsets for the app and dependencies." @moduledoc """ @@ -43,18 +49,38 @@ defmodule Mix.Tasks.MyApp.GenerateDocsets do Mix.Task.run("app.start") # generate a docset for this codebase - DocsetApi.Builder.build("MyApp", "docs/exdoc", path) + DocsetApi.Builder.build("Hello", "doc") # generate docsets for every dependency - configured_deps = Enum.map(MoodleNet.Mixfile.deps(), &dep_process(&1, path)) + results = + Hello.MixProject.project[:deps] + |> IO.inspect(label: "Deps found for our package") + |> Enum.map(&dep_process(&1, path)) - end + Mix.shell().info """ + Conversion completed. Here are the results: - defp dep_process(dep, path) do - lib = elem(dep, 0) + """ + + + Enum.map(results, fn + %{release: %{name: name, version: version}} -> + Mix.shell().info " -> OK: #{name} @ #{version}" + {:error, name, message} -> + Mix.shell().error """ - DocsetApi.Builder.build(Atom.to_string(lib), path) + -> Could not build docset for #{name}: + #{message} + """ + unknown -> + Mix.shell().error """ + + -> Unknown error: + #{inspect unknown} + + """ + end) end def run(_args), @@ -64,12 +90,21 @@ defmodule Mix.Tasks.MyApp.GenerateDocsets do Usage: - #{@usage} + #{@usage} """) + + defp dep_process(dep, path) do + lib = elem(dep, 0) + + case entry = DocsetApi.Builder.build(Atom.to_string(lib)) do + {:error, _pkg, _reason} -> entry + %{base_dir: _ } -> DocsetApi.Builder.copy_docset(entry, Path.expand(path)) + end + end end ``` -Then run the task `mix moodle_net.generate_docsets /home/myuser/.local/share/Zeal/Zeal/docsets/` with your desired output directory. +Then run the task `mix hello.generate_docsets /home/myuser/.local/share/Zeal/Zeal/docsets/` with your desired output directory. Voila! diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..1cc5f65 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.1.0 \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index fbdb7dd..fac9c3c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -3,7 +3,7 @@ # # This configuration file is loaded before any dependency and # is restricted to this project. -use Mix.Config +import Config # Configures the endpoint config :docset_api, DocsetApi.Endpoint, diff --git a/config/dev.exs b/config/dev.exs index 18dcf71..153b0ef 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # For development, we disable any cache and enable # debugging and code reloading. diff --git a/config/prod.exs b/config/prod.exs index aa43cfe..1117d19 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,26 +1,11 @@ -use Mix.Config +import Config -# For production, we configure the host to read the PORT -# from the system environment. Therefore, you will need -# to set PORT=80 before running your server. -# -# You should also configure the url host to something -# meaningful, we use this information when generating URLs. -# -# Finally, we also include the path to a manifest -# containing the digested version of static files. This -# manifest is generated by the mix phoenix.digest task -# which you typically run after static files are built. -config :docset_api, DocsetApi.Endpoint, - http: [port: System.get_env("PORT")], - url: [host: System.get_env("DOMAIN_NAME"), port: 80], - cache_static_manifest: "priv/static/manifest.json" +config :docset_api, DocsetApi.Endpoint, server: true # Do not print debug messages in production config :logger, level: :info -config :docset_api, DocsetApi.Endpoint, - secret_key_base: System.get_env("SECRET_KET_BASE") +config :docset_api, DocsetApi.Endpoint, secret_key_base: System.get_env("SECRET_KET_BASE") config :docset_api, base_url: System.get_env("BASE_URL") diff --git a/config/runtime.exs b/config/runtime.exs new file mode 100644 index 0000000..2129754 --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,7 @@ +import Config + +port = String.to_integer(System.fetch_env!("PORT")) + +config :docset_api, DocsetApi.Endpoint, + http: [port: port], + url: [host: "localhost", port: port] diff --git a/config/test.exs b/config/test.exs index efe211f..18d08fb 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # We don't run a server during test. If one is required, # you can enable the server option below. @@ -7,13 +7,4 @@ config :docset_api, DocsetApi.Endpoint, server: false # Print only warnings and errors during test -config :logger, level: :warn - -# Configure your database -config :docset_api, DocsetApi.Repo, - adapter: Ecto.Adapters.Postgres, - username: "postgres", - password: "postgres", - database: "docset_api_test", - hostname: "localhost", - pool: Ecto.Adapters.SQL.Sandbox +config :logger, level: :warning diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..eb0063b --- /dev/null +++ b/default.nix @@ -0,0 +1,28 @@ +{ pkgs ? }: +let + packages = pkgs.beam.packagesWith pkgs.beam.interpreters.erlang_27; +in +packages.mixRelease rec { + elixir = packages.elixir_1_17; + pname = "hexdocs_docset_api"; + version = builtins.readFile ./VERSION; + + enableDebugInfo = true; + stripDebug = false; + + src = pkgs.fetchFromGitHub { + owner = "johnhamelink"; + rev = "509316f9bf9570c6b2c8e1e25b980b7276e7c4ea"; + repo = "${pname}"; + sha256 = "sha256-ttXZP/v84rK2B7Fwdy3EedhmsyFmjN3dmKNPKNNzNBI="; + }; + + mixNixDeps = with pkgs; import ./mix.nix { inherit lib beamPackages; }; + + postBuild = '' + # for external task you need a workaround for the no deps check flag + # https://github.com/phoenixframework/phoenix/issues/2690 + mix do deps.loadpaths --no-deps-check, phx.digest + mix phx.digest --no-deps-check + ''; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7d3d9a1 --- /dev/null +++ b/flake.lock @@ -0,0 +1,75 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1730504689, + "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "506278e768c2a08bec68eb62932193e341f55c90", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1730272153, + "narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1730504152, + "narHash": "sha256-lXvH/vOfb4aGYyvFmZK/HlsNsr/0CVWlwYvo2rxJk3s=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..43a89fe --- /dev/null +++ b/flake.nix @@ -0,0 +1,50 @@ +{ + description = "Hexdocs Docset API - nix flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + + flake-parts.url = "github:hercules-ci/flake-parts"; + }; + + outputs = + inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ + "aarch64-linux" + "x86_64-linux" + + "aarch64-darwin" + "x86_64-darwin" + ]; + + perSystem = + { pkgs, ... }: + { + packages.default = (pkgs.callPackage ./default.nix { inherit pkgs; }); + + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + # For Nix + mix2nix + nixd + nixfmt-rfc-style + + # For Elixir + elixir + elixir-ls + fswatch # Used to re-run tests + + # For Elixir deps + sqlite + gnutar + ]; + + }; + }; + }; +} diff --git a/lib/builder.ex b/lib/builder.ex deleted file mode 100644 index 391188f..0000000 --- a/lib/builder.ex +++ /dev/null @@ -1,256 +0,0 @@ -defmodule DocsetApi.Builder do - require Logger - alias DocsetApi.FileParser - - @doc """ - Build a docset from a hex library name - """ - def build(name, destination) do - try do - state = - retrieve_release(name, destination) - |> prepare_environment(name) - |> download_and_extract_docs - |> build_plist - |> copy_logo - |> build_index - |> build_tarball(destination) - - Map.fetch!(state, :release) - rescue - e -> - Logger.warn("Could not download and build docset for #{name}") - IO.inspect(e) - :ok - end - end - - @doc """ - Build a docset from a folder - """ - def build(name, from_path, destination) do - try do - state = - %{} - |> Map.put(:name, name) - |> Map.put(:destination, destination) - |> prepare_environment(name, from_path) - |> copy_docs - |> build_plist - |> build_index - |> build_tarball(destination) - - Map.fetch!(state, :release) - rescue - e -> - Logger.warn("Could not copy and build docset for #{name}") - IO.inspect(e) - :ok - end - end - - def retrieve_release(name, destination) do - "https://hex.pm/api/packages/#{name}" - |> HTTPoison.get() - |> case do - {:ok, response} -> get_latest_version(name, destination, response) - {:error, err} -> return_error(err) - end - end - - def get_latest_version(name, destination, %HTTPoison.Response{body: json}) do - json - |> Poison.decode!(as: %{"releases" => [%DocsetApi.Release{}]}) - # |> IO.inspect() - |> Map.fetch!("releases") - |> List.first() - |> Map.fetch!(:url) - |> HTTPoison.get!() - |> Map.fetch!(:body) - |> Poison.decode!(as: %DocsetApi.Release{}) - |> Map.put(:name, name) - |> Map.put(:destination, destination) - |> IO.inspect() - end - - defp prepare_environment(release, name, from_path \\ nil) do - working_dir = "#{Path.dirname(release.destination)}" - base_dir = "#{working_dir}/#{name}.docset" - files_dir = "#{base_dir}/Contents/Resources/Documents" - docs_archive = "#{working_dir}/#{name}_hexdocs.tar.gz" - - Logger.debug("Create working dir #{release.name}.docset") - :ok = File.mkdir_p(working_dir) - {:ok, _} = File.rm_rf(base_dir) - {:ok, _} = File.rm_rf(docs_archive) - :ok = File.mkdir_p(base_dir) - :ok = File.mkdir_p(files_dir) - - IO.inspect(%{ - working_dir: working_dir, - base_dir: base_dir, - files_dir: files_dir, - docs_archive: docs_archive, - from_path: from_path, - release: release - }) - end - - defp download_and_extract_docs( - state = %{ - docs_archive: docs_archive, - release: release, - files_dir: files_dir - } - ) do - url = - release.docs_url || - "https://hex.pm/api/packages/#{release.name}/releases/#{release.version}/docs" - - Logger.debug("download from #{url} to #{docs_archive} and extract to #{files_dir}") - - %HTTPoison.Response{body: doc} = HTTPoison.get!(url, [], follow_redirect: true) - - File.write!(docs_archive, doc) - - :erl_tar.extract( - docs_archive, - [:compressed, {:cwd, files_dir}] - ) - - state - end - - defp copy_docs( - state = %{ - from_path: from_path, - files_dir: files_dir - } - ) do - Logger.debug("copy from #{from_path} to #{files_dir}") - - File.cp_r(from_path, files_dir) - - state - end - - defp build_plist(state = %{base_dir: base_dir, release: release}) do - info_plist = """ - - - - - CFBundleIdentifier - #{release.name |> String.downcase()} - - CFBundleName - #{release.name} - - DocSetPlatformFamily - elixir - - isJavaScriptEnabled - - - isDashDocset - - - DashDocSetPluginKeyword - #{release.name} - - - """ - - File.write!("#{base_dir}/Contents/Info.plist", info_plist) - - version = Map.get(release, :version, 0) - - info_meta = """ - { - "extra": { - "isJavaScriptEnabled": true - }, - "name": "#{release.name}", - "version": "#{version}", - "title": "#{release.name}" - } - """ - - File.write!("#{base_dir}/meta.json", info_meta) - - state - end - - defp copy_logo(state = %{base_dir: base_dir, files_dir: files_dir}) do - files_dir - |> find_logo() - |> File.cp(Path.join(base_dir, "icon.png")) - - state - end - - defp find_logo(files_dir) do - package_logo_file = Path.join([files_dir, "assets", "logo.png"]) - - if File.exists?(package_logo_file) do - package_logo_file - else - Path.join([Application.app_dir(:docset_api), "priv", "static", "images", "hexpm.png"]) - end - end - - def build_index(state = %{base_dir: base_dir, files_dir: files_dir}) do - index_file = "#{base_dir}/Contents/Resources/docSet.dsidx" - - Sqlitex.with_db(String.to_charlist(index_file), fn db -> - # Create SQLite index table - Sqlitex.query(db, """ - CREATE TABLE searchIndex( - id INTEGER PRIMARY KEY, - name TEXT, - type TEXT, - path TEXT - ); - """) - - # Add a unique index for the name, type and path combo - Sqlitex.query( - db, - "CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path)" - ) - - # Create a callback which inserts the record into the DB - index_fn = fn name, type, path -> - {:ok, _} = - Sqlitex.query( - db, - "INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES ('#{name}', '#{type}', '#{ - path - }');" - ) - end - - # Deep-search for files - files = FileExt.ls_r(files_dir) - - # For each file, parse it for the right keywords and run the callback - # against the result. - Enum.each( - files, - &FileParser.parse_file(&1, files_dir, index_fn) - ) - end) - - state - end - - defp build_tarball(state = %{base_dir: base_dir}, destination) do - Logger.debug("Writing tarball to #{destination}") - System.cmd("tar", ["-czvf", destination, "."], cd: base_dir, into: IO.stream(:stdio, :line)) - state - end - - def return_error(%HTTPoison.Error{reason: reason}) do - IO.inspect(reason) - end -end diff --git a/lib/builder_server.ex b/lib/builder_server.ex deleted file mode 100644 index 49cb063..0000000 --- a/lib/builder_server.ex +++ /dev/null @@ -1,68 +0,0 @@ -defmodule DocsetApi.BuilderServer do - use GenServer - - require Logger - alias DocsetApi.Builder - - @name __MODULE__ - @timeout 50000 - - ## External API - - def start_link(_opts) do - Logger.info("Starting BuilderServer") - GenServer.start_link(@name, %{}, name: @name) - end - - def update_package(pkg, destination), - do: GenServer.call(@name, {:build_package, pkg, destination}, @timeout) - - def fetch_package(pkg, destination) do - case GenServer.call(@name, {:get_cached, pkg}, @timeout) do - {:ok, package} -> - package - - :error -> - GenServer.call( - @name, - {:build_package, pkg, destination}, - @timeout - ) - end - end - - ## Internal API - - def init(init) when is_map(init) do - Process.send_after(self(), :update_packages, tomorrow()) - {:ok, init} - end - - defp tomorrow do - 1000 * 60 * 60 * 24 - end - - def handle_call({:build_package, pkg_name, destination}, _from, packages) do - pkg = Builder.build(pkg_name, destination) - {:reply, pkg, Map.put(packages, pkg_name, pkg)} - end - - def handle_call({:get_cached, pkg}, _from, packages) do - {:reply, Map.fetch(packages, pkg), packages} - end - - def handle_info(:update_packages, packages) do - await_timeout_ms = 1000 * 10 - - packages - |> Enum.map(fn {pkg, release} -> - Task.async(fn -> - Builder.build(pkg, release.destination) - end) - end) - |> Enum.each(&Task.await(&1, await_timeout_ms)) - - Process.send_after(self(), :update_packages, tomorrow()) - {:noreply, packages} - end -end diff --git a/lib/docset_api.ex b/lib/docset_api.ex index 4f943b0..070ac9f 100644 --- a/lib/docset_api.ex +++ b/lib/docset_api.ex @@ -26,4 +26,16 @@ defmodule DocsetApi do DocsetApi.Endpoint.config_change(changed, removed) :ok end + + def docset_dir do + case Application.fetch_env(:docset_api, :docset_dir) do + {:ok, dir} -> + dir + + :error -> + tmp = Path.join(System.tmp_dir!(), "hexdocs_docset_api") + Application.put_env(:docset_api, :docset_dir, tmp) + docset_dir() + end + end end diff --git a/lib/docset_api/builder.ex b/lib/docset_api/builder.ex new file mode 100644 index 0000000..e102310 --- /dev/null +++ b/lib/docset_api/builder.ex @@ -0,0 +1,328 @@ +defmodule DocsetApi.Builder do + require Logger + alias DocsetApi.FileParser + alias DocsetApi.Release + + @doc """ + Build a docset from a hex library name + """ + def build(name) do + with {:ok, release} <- retrieve_release(name), + {:ok, state} <- prepare_environment(name, release), + {:ok, state} <- download_and_extract_docs(state), + {:ok, state} <- build_plist(state), + {:ok, state} <- copy_logo(state) do + build_index(state) + else + {:error, msg} -> {:error, name, msg} + end + end + + @doc """ + Build a docset from a folder + """ + def build(name, from_path) do + state = %Release{ + name: name + } + + with {:ok, state} <- prepare_environment(name, state, from_path), + {:ok, state} <- copy_docs(state), + {:ok, state} <- build_plist(state) do + build_index(state) + end + end + + def build_tarball( + %{base_dir: base_dir, release: %{name: name, version: version}} = state, + dest_dir + ) do + filename = Path.join(dest_dir, "#{name}-#{version}.tgz") + Logger.debug("Writing tarball to #{filename}") + System.cmd("tar", ["-czvf", filename, "."], cd: base_dir) + + {:ok, state} + end + + def copy_docset(%{base_dir: base_dir} = state, dest_dir) do + Logger.debug("Copying #{base_dir} to #{dest_dir}") + + {:ok, _} = + File.cp_r( + base_dir, + Path.join([dest_dir, Path.basename(base_dir)]) + ) + + {:ok, state} + end + + defp return_error(%HTTPoison.Error{reason: reason}) do + Logger.error(inspect(reason, pretty: true)) + end + + defp retrieve_release(name) do + "https://hex.pm/api/packages/#{name}" + |> HTTPoison.get() + |> case do + {:ok, response} -> get_latest_version(name, response) + {:error, err} -> return_error(err) + end + end + + defp get_latest_version(name, %HTTPoison.Response{body: json, status_code: 200}) do + release = + json + |> Poison.decode!(as: %{"releases" => [%Release{}]}) + |> Map.fetch!("releases") + |> List.first() + |> Map.fetch!(:url) + |> HTTPoison.get!() + |> Map.fetch!(:body) + |> Poison.decode!(as: %Release{}) + |> Map.replace!(:name, name) + + {:ok, release} + end + + defp get_latest_version(name, %HTTPoison.Response{body: json, status_code: status}) + when status != 200 do + Logger.warning(~s""" + Could not process response from hex.pm for #{name} dependency: + + Received #{status} from server. Skipping. + Message from server: + + #{inspect(Poison.decode!(json))} + """) + + case status do + 404 -> {:error, :hexpm_not_found} + end + end + + defp prepare_environment(name, %Release{} = release, from_path \\ nil) do + working_dir = + Path.join([System.tmp_dir(), "hexdocs_docset", name]) + |> Path.expand() + + base_dir = + Path.join([working_dir, "#{name}.docset"]) + |> Path.expand() + + files_dir = + Path.join([base_dir, "Contents", "Resources", "Documents"]) + |> Path.expand() + + docs_archive = + Path.join([working_dir, "#{name}_hexdocs.tar.gz"]) + |> Path.expand() + + Logger.debug("Create working dir #{base_dir}") + + File.mkdir_p!(working_dir) + File.rm_rf!(base_dir) + File.rm_rf!(docs_archive) + File.mkdir_p!(base_dir) + File.mkdir_p!(files_dir) + + {:ok, + %{ + working_dir: working_dir, + base_dir: base_dir, + files_dir: files_dir, + docs_archive: docs_archive, + from_path: from_path, + release: release + }} + end + + defp download_and_extract_docs( + %{ + docs_archive: docs_archive, + release: %Release{name: name, version: version, docs_url: docs_url}, + files_dir: files_dir + } = state + ) do + url = docs_url || "https://hex.pm/api/packages/#{name}/releases/#{version}/docs" + + Logger.debug("download from #{url} to #{docs_archive} and extract to #{files_dir}") + + %HTTPoison.Response{body: doc} = HTTPoison.get!(url, [], follow_redirect: true) + + File.write!(docs_archive, doc) + + :erl_tar.extract(docs_archive, [:compressed, cwd: files_dir]) + + {:ok, state} + end + + defp copy_docs(%{from_path: from_path, files_dir: files_dir} = state) do + Logger.debug("copy from #{from_path} to #{files_dir}") + File.cp_r(from_path, files_dir) + state + end + + defp build_plist(%{base_dir: base_dir, release: %Release{name: name, version: version}} = state) do + info_plist = """ + + + + + CFBundleIdentifier + #{String.downcase(name)} + + CFBundleName + #{name} + + DocSetPlatformFamily + elixir + + isJavaScriptEnabled + + + isDashDocset + + + DashDocSetPluginKeyword + #{name} + + + """ + + base_dir + |> Path.join("Contents/Info.plist") + |> File.write!(info_plist) + + info_meta = + Poison.encode!(%{ + extra: %{isJavaScriptEnabled: true}, + name: name, + version: version, + title: name + }) + + base_dir + |> Path.join("meta.json") + |> File.write!(info_meta) + + {:ok, state} + end + + defp copy_logo(%{base_dir: base_dir, files_dir: files_dir} = state) do + files_dir + |> find_logo() + |> File.cp(Path.join(base_dir, "icon.png")) + + {:ok, state} + end + + defp find_logo(files_dir) do + package_logo_file = Path.join([files_dir, "assets", "logo.png"]) + + if File.exists?(package_logo_file) do + package_logo_file + else + Path.join([to_string(:code.priv_dir(:docset_api)), "static", "images", "hexpm.png"]) + end + end + + defp build_index(%{base_dir: base_dir, files_dir: files_dir} = state) do + sqlite_file = + [base_dir, "Contents", "Resources", "docSet.dsidx"] + |> Path.join() + |> String.to_charlist() + + {:ok, db} = Exqlite.Basic.open(sqlite_file) + + # Create SQLite index table + {:ok, _query, _result, db} = + Exqlite.Basic.exec(db, """ + CREATE TABLE searchIndex( + id INTEGER PRIMARY KEY, + name TEXT, + type TEXT, + path TEXT + ); + """) + + # Add a unique index for the name, type and path combo + {:ok, _query, _result, db} = + Exqlite.Basic.exec(db, "CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path)") + + # Deep-search for files + files = ls_r(files_dir) + html_files = Enum.filter(files, &String.match?(&1, ~r"\.html")) + + for file <- html_files do + Logger.debug("Parsing #{file} ...") + + html = Floki.parse_document!(File.read!(file)) + relative_path = Path.relative_to(file, files_dir) + + # Set `sidebar-closed` on the body instead of `sidebar-opened`. + # Remove sidebar-button sidebar-toggle + # Remove icon-action display-settings + content = + FileParser.parse(html, relative_path, fn name, type, path -> + query = + "INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES ('#{name}', '#{type}', '#{path}');" + + {:ok, _query, _result, _db} = Exqlite.Basic.exec(db, query) + end) + |> Floki.attr("body", "class", fn + nil -> + "sidebar-closed" + + classes -> + if String.contains?(classes, "sidebar-opened") do + String.replace(classes, "sidebar-opened", "sidebar-closed") + else + "#{classes} sidebar-closed" + end + end) + |> Floki.find_and_update("button", fn button -> + button_classes = Floki.attribute(button, "class") + + if Enum.all?(button_classes, &String.starts_with?(&1, "sidebar-")) or + Enum.member?(button_classes, "display-settings") do + :delete + else + button + end + end) + |> Floki.raw_html() + + File.write( + file, + content + ) + end + + skipped_files = Enum.reject(files, &Enum.member?(html_files, &1)) + + Logger.debug(""" + Skipped the following files: + + #{Enum.each(skipped_files, &" - #{&1}")} + """) + + :ok = Exqlite.Basic.close(db) + + {:ok, state} + end + + defp ls_r(path) do + cond do + File.regular?(path) -> + [path] + + File.dir?(path) -> + path + |> File.ls!() + |> Enum.flat_map(&ls_r(Path.join(path, &1))) + + true -> + [] + end + end +end diff --git a/lib/docset_api/builder_server.ex b/lib/docset_api/builder_server.ex new file mode 100644 index 0000000..524d333 --- /dev/null +++ b/lib/docset_api/builder_server.ex @@ -0,0 +1,77 @@ +defmodule DocsetApi.BuilderServer do + use GenServer + + require Logger + alias DocsetApi.Builder + + @timeout 50_000 + + ## External API + + def start_link(_opts) do + Logger.info("Starting BuilderServer") + GenServer.start_link(__MODULE__, %{}, name: __MODULE__) + end + + defp call(message) do + GenServer.call(__MODULE__, message, @timeout) + end + + def update_package(pkg, destination) do + call({:build_package, pkg, destination}) + end + + def fetch_package(pkg) do + with {:ok, package = %{working_dir: working_dir, release: %{name: name, version: version}}} <- + call({:get_cached, pkg}), + package_path <- Path.join([working_dir, "#{name}-#{version}.tgz"]), + true <- File.exists?(package_path) do + package + else + _cache_miss_state -> call({:build_package, pkg}) + end + end + + ## Internal API + + def init(init) when is_map(init) do + Process.send_after(self(), :update_packages, tomorrow()) + {:ok, init} + end + + defp tomorrow do + :timer.hours(24) + end + + def handle_call({:build_package, pkg_name}, _from, packages) do + with {:ok, docset} <- Builder.build(pkg_name), + pkg <- Builder.build_tarball(docset, docset[:working_dir]) do + {:reply, pkg, Map.put(packages, pkg_name, pkg)} + else + {:error, docset, :hexpm_not_found} = err -> + Logger.error """ + Could not build package "#{docset}": + Hexdocs.pm returned 404 for this libary. Is it spelt + correctly? + """ + {:reply, err, packages} + {:error, docset, unknown} -> + raise "[#{docset}] An unknown error occurred: #{inspect unknown}" + wtf -> + raise "Failed to recognise response from builder: #{inspect wtf}" + end + end + + def handle_call({:get_cached, pkg}, _from, packages) do + {:reply, Map.fetch(packages, pkg), packages} + end + + def handle_info(:update_packages, packages) do + packages + |> Task.async_stream(fn {pkg, _release} -> Builder.build(pkg) end) + |> Stream.run() + + Process.send_after(self(), :update_packages, tomorrow()) + {:noreply, packages} + end +end diff --git a/lib/docset_api/endpoint.ex b/lib/docset_api/endpoint.ex deleted file mode 100644 index 25ff72a..0000000 --- a/lib/docset_api/endpoint.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule DocsetApi.Endpoint do - use Phoenix.Endpoint, otp_app: :docset_api - - socket "/socket", DocsetApi.UserSocket, - websocket: true, - longpoll: false - - # Serve at "/" the static files from "priv/static" directory. - # - # You should set gzip to true if you are running phoenix.digest - # when deploying your static files in production. - plug Plug.Static, - at: "/", from: :docset_api, gzip: false, - only: ~w(docsets css fonts images js favicon.ico robots.txt) - - # Code reloading can be explicitly enabled under the - # :code_reloader configuration of your endpoint. - if code_reloading? do - socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket - plug Phoenix.LiveReloader - plug Phoenix.CodeReloader - end - - plug Plug.RequestId - plug Plug.Logger - - plug Plug.Parsers, - parsers: [:urlencoded, :multipart, :json], - pass: ["*/*"], - json_decoder: Poison - - plug Plug.MethodOverride - plug Plug.Head - - # The session will be stored in the cookie and signed, - # this means its contents can be read but not tampered with. - # Set :encryption_salt if you would also like to encrypt it. - plug Plug.Session, - store: :cookie, - key: "_docset_api_key", - signing_salt: "0fJq0gf5" - - plug DocsetApi.Router -end diff --git a/lib/docset_api/file_parser.ex b/lib/docset_api/file_parser.ex new file mode 100644 index 0000000..9978c1a --- /dev/null +++ b/lib/docset_api/file_parser.ex @@ -0,0 +1,379 @@ +defmodule DocsetApi.FileParser do + require Logger + require IEx + + @banned_files [ + "404.html", + "search.html", + "index.html" + ] + + defp whole_file_finder({:exdoc, %Version{major: 0, minor: 11, patch: _}}, html) do + Floki.find(html, "title") + |> Floki.text() + |> String.trim() + |> String.split(" – ") + |> List.first() + end + + defp whole_file_finder({:exdoc, _}, html) do + Floki.find(html, "title") + |> Floki.text() + |> String.trim() + |> String.split(" — ") + |> List.first() + end + + defp is_type?({:exdoc, %Version{major: 0, minor: min, patch: _}}, html, expected) when min >= 11 and min <= 25 do + body_tag = Floki.find(html, "body") + + module_type = + Floki.find(html, "div.content-inner>h1>small") + |> List.first + |> Floki.text + |> String.trim + + if expected == "module" do + "modules" in Floki.attribute(body_tag, "data-type") and + module_type not in module_subtypes(:downcase) + else + "modules" in Floki.attribute(body_tag, "data-type") and + module_type == expected + end + end + + defp is_type?({:exdoc, _version}, html, expected) do + body_tag = Floki.find(html, "body") + + class_items = + Floki.attribute(body_tag, "class") + |> Enum.flat_map(&String.split(&1, " ")) + + "modules" in Floki.attribute(body_tag, "data-type") and + "page-#{expected}" in class_items + end + + # TODO: It'd also be really cool if this could be extended through a + # plugin system... + def identify_check_for(:exdoc, html, file_path) do + Floki.find(html, "meta[name=\"generator\"]") + |> Floki.attribute("content") + |> Floki.text() + |> String.trim() + |> String.split(" v") + |> case do + ["ExDoc", version] -> {:exdoc, Version.parse!(version)} + [""] -> + # TODO: It'd be nice to drop down to some sort of basic + # behaviour to allow the docset to at least be + # built. Maybe use the Entry type to just store all of + # the pages. + # + Logger.error "Could not detect documentation tooling library for #{file_path}." + nil + end + end + + # Other available types to chose from: + # + # Annotation Attribute Binding Builtin Callback Category Class + # Command Component Constant Constructor Define Delegate Diagram + # Directive Element Entry Enum Environment Error Event Extension + # Field File Filter Framework Global Hook Instance Instruction + # Keyword Library Literal Macro Method Mixin Modifier Namespace + # Notation Object Operator Option Package Parameter Plugin + # Property Provider Provisioner Query Record Resource Sample + # Section Service Setting Shortcut Statement Struct Style + # Subroutine Tag Test Trait Union Value Variable Word + # + # Taken from https://kapeli.com/docsets#supportedentrytypes + def parsers do + + %{ + "Callback" => %{ + type: :inline, + + # The unique identifier for the function we are recording. It + # should contain the entire namespace. + name: fn + {:exdoc, _} -> &("#{&1}." <> String.replace_prefix(&2, "c:", "")) + end, + + # A function which will find function IDs on the html tree + id: fn + {:exdoc, _} -> &Floki.attribute(Floki.find(&1, ".callbacks-list .detail"), "id") + end, + + # The relative path to the content to navigate to + path: fn + {:exdoc, _} -> &"#{&1}##{&2}" + end + }, + + "Exception" => %{ + type: :whole_file, + # Determines whether the file in question is of + # type "exception" or not. + is_type?: fn doc -> &is_type?(doc, &1, "exception") end, + # What is the ID of this thing? + id: fn doc -> &(whole_file_finder(doc, &1)) end, + # Where is the content we wish to keep? + path: fn + {:exdoc, _version} -> &"#{&1}#content" + end + }, + + "Function" => %{ + type: :inline, + + # The unique identifier for the function we are recording. It + # should contain the entire namespace. + name: fn {:exdoc, _} -> &"#{&1}.#{&2}" end, + + # A function which will find function IDs on the html tree (it includes arity) + id: fn {:exdoc, _} -> &Floki.attribute(Floki.find(&1, "#functions .detail"), "id") end, + + # The relative path to the content to navigate to + path: fn {:exdoc, _} -> &"#{&1}##{&2}" end + }, + + "Guide" => %{ + type: :whole_file, + is_type?: fn + {:exdoc, %Version{major: 0, minor: m, patch: _}} when m > 25 -> + fn html -> + body_tag = Floki.find(html, "body") + + class_items = + Floki.attribute(body_tag, "class") + |> Enum.flat_map(&String.split(&1, " ")) + |> MapSet.new() + + class_intersection = + MapSet.new(["page-extra", "page-cheatmd"]) + |> MapSet.intersection(class_items) + + "extras" in Floki.attribute(body_tag, "data-type") and + MapSet.size(class_intersection) > 0 + end + {:exdoc, %Version{major: 0, minor: m, patch: _}} when m <= 25 -> # and m >= 23 -> + fn html -> + # FIXME: Is this specific enough? + body_tag = Floki.find(html, "body") + "extras" in Floki.attribute(body_tag, "data-type") + end + end, + id: fn doc -> &(whole_file_finder(doc, &1)) end, + path: fn + {:exdoc, _} -> &"#{&1}#content" + end + }, + + # Obviously an interface is not a behaviour, but it's close + # enough to continue on, and I can see about having behaviours + # added later. + "Interface" => %{ + type: :whole_file, + is_type?: fn doc -> &is_type?(doc, &1, "behaviour") end, + id: fn doc -> &(whole_file_finder(doc, &1)) end, + path: fn + {:exdoc, _} -> &"#{&1}#content" + end + }, + + "Macro" => %{ + type: :inline, + + # The unique identifier for the function we are recording. It + # should contain the entire namespace. + name: fn + {:exdoc, _} -> &"#{&1}.#{&2}" + end, + + # A function which will find function IDs on the html tree (it includes arity) + id: fn {:exdoc, _} -> &Floki.attribute(Floki.find(&1, "#macros .detail"), "id") end, + + # The relative path to the content to navigate to + path: fn {:exdoc, _} -> &"#{&1}##{&2}" end + }, + + + "Module" => %{ + type: :whole_file, + is_type?: fn doc -> &is_type?(doc, &1, "module") end, + id: fn doc -> &(whole_file_finder(doc, &1)) end, + path: fn + {:exdoc, _} -> &"#{&1}#content" + end + }, + + # FIXME: We'll use Procedure for Tasks since Tasks isn't + # available + "Procedure" => %{ + type: :whole_file, + is_type?: fn doc -> &is_type?(doc, &1, "task") end, + id: fn doc -> &(whole_file_finder(doc, &1)) end, + path: fn + {:exdoc, _} -> &"#{&1}#content" + end + }, + + "Protocol" => %{ + type: :whole_file, + is_type?: fn doc -> &is_type?(doc, &1, "protocol") end, + id: fn doc -> &(whole_file_finder(doc, &1)) end, + path: fn + {:exdoc, _} -> &"#{&1}#content" + end + }, + + "Type" => %{ + type: :inline, + + # The unique identifier for the function we are recording. It + # should contain the entire namespace. + name: fn + {:exdoc, _} -> &("#{&1}." <> String.replace_prefix(&2, "t:", "")) + end, + + # A function which will find function IDs on the html tree + id: fn + {:exdoc, _} -> &Floki.attribute(Floki.find(&1, ".types-list .detail"), "id") + end, + + # The relative path to the content to navigate to + path: fn + {:exdoc, _} -> &"#{&1}##{&2}" + end + } + } + end + + @spec identify_documenting_tool_version(Floki.html_tree(), binary()) :: {atom(), binary()} + def identify_documenting_tool_version(html, file_path) do + # Try various different + Enum.find_value( + [ + fn -> identify_check_for(:exdoc, html, file_path) end + ], + fn x -> x.() end + ) + end + + def module_subtypes(:downcase) do + Enum.map(module_subtypes(), &String.downcase/1) + end + + def module_subtypes(:upcase) do + Enum.map(module_subtypes(), &String.upcase/1) + end + + def module_subtypes() do + parsers() + |> Map.filter(fn + {_key, %{type: :whole_file}} -> true + _ -> false + end) + |> Map.keys + end + + def parsers(type_filter) do + Enum.filter(parsers(), fn + {_key, %{type: ^type_filter}} -> true + _ -> false + end) + end + + def parse_file_type(html, doc, file_path, callback) do + # Determine "whole-file" type, and use that to define the namespace + parsers(:whole_file) + # I wonder - it might be necessary to allow multiple for + # whole_file types per file. Leaving this comment here just to + # remind me where to adjust this if necessary. + |> Enum.find_value(fn {type, parser} -> + Logger.debug("Checking if file is of type #{type}") + if file_path == "js/index.html", do: IEx.pry() + + is_type? = parser.is_type?.(doc) + id = parser.id.(doc) + path = parser.path.(doc) + + if path.(file_path) =~ "Guardsafe" do + require IEx; IEx.pry + end + + if is_type?.(html) do + name = id.(html) + file_path = path.(file_path) + + # Trigger the callback against this matched file type, so that + # it's added to the database. + callback.(name, type, file_path) + + # Return the detection state + {name, type, file_path} + else + # Keep searching + false + end + end) + end + + def parse_inside_file(html, doc, namespace, file_path, callback) do + # Loop through the inline parsers + for {type, parser} <- parsers(:inline) do + Logger.debug("Looking for #{type} types") + # Fetch the module associated with this file + # + # TODO: Cache this, probably, since the module:function + # ratio will be highly skewed + if namespace == nil do + raise """ + Could not find namespace for a #{file_path}. This is probably a + bug? + """ + end + + id = parser.id.(doc) + name = parser.name.(doc) + path = parser.path.(doc) + + for id <- id.(html) do + # IO.inspect(id, label: "ID") + # IO.inspect(namespace, label: "Namespace") + # IEx.pry + callback.( + name.(namespace, id), + type, + path.(file_path, id) + ) + end + + :ok + end + end + + @spec parse(Floki.html_tree(), binary(), fun()) :: Floki.html_tree() + def parse(html, file_path, callback) when is_function(callback) do + doc = identify_documenting_tool_version(html, file_path) + + if file_path not in @banned_files do + case parse_file_type(html, doc, file_path, callback) do + {namespace, _type, _file_path} -> + parse_inside_file(html, doc, namespace, file_path, callback) + + nil -> + Logger.warning( + "Could not categorise #{file_path}. If this is surprising then consider it a bug. Moving on." + ) + + other -> + Logger.warning("Could not categorise response #{inspect(other)}. This is a bug.") + end + else + Logger.warning("Ignoring #{file_path} since it's a banned file.") + end + + html + end +end diff --git a/lib/models/release.ex b/lib/docset_api/release.ex similarity index 76% rename from lib/models/release.ex rename to lib/docset_api/release.ex index 4ef2107..ea6ec63 100644 --- a/lib/models/release.ex +++ b/lib/docset_api/release.ex @@ -1,6 +1,8 @@ defmodule DocsetApi.Release do + # A structure for a Hex release with documentation + defstruct name: nil, - version: nil, + version: 0, url: nil, docs_url: nil, docs_html_url: nil, diff --git a/web/web.ex b/lib/docset_api_web.ex similarity index 89% rename from web/web.ex rename to lib/docset_api_web.ex index fe7f9dd..77ef296 100644 --- a/web/web.ex +++ b/lib/docset_api_web.ex @@ -28,7 +28,7 @@ defmodule DocsetApi.Web do def view do quote do - use Phoenix.View, root: "web/templates" + use Phoenix.View, root: "lib/docset_api_web/templates" # Import convenience functions from controllers import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] @@ -47,14 +47,6 @@ defmodule DocsetApi.Web do end end - def channel do - quote do - use Phoenix.Channel - - alias DocsetApi.Repo - end - end - @doc """ When used, dispatch to the appropriate controller/view/etc. """ diff --git a/lib/docset_api_web/controllers/feed_controller.ex b/lib/docset_api_web/controllers/feed_controller.ex new file mode 100644 index 0000000..3bb8931 --- /dev/null +++ b/lib/docset_api_web/controllers/feed_controller.ex @@ -0,0 +1,30 @@ +defmodule DocsetApi.FeedController do + use DocsetApi.Web, :controller + alias DocsetApi.BuilderServer + + def show(conn, %{"package_name" => package}) do + with {:ok, entry} when is_map(entry) <- BuilderServer.fetch_package(package) do + render(conn, "show.xml", entry: entry) + else + {:error, _docset, :hexpm_not_found} -> + send_resp(conn, 404, "Docset not found") + end + end + + def docset(conn, %{"docset" => docset}) do + + package = String.trim_trailing(docset, ".tgz") + [name, _] = String.split(package, "-") + + filename = + Path.join([System.tmp_dir(), "hexdocs_docset", name, docset]) + |> Path.absname() + |> IO.inspect(label: "candidate filename") + + if File.exists?(filename) do + send_file(conn, 200, filename) + else + send_resp(conn, 404, "Docset not found") + end + end +end diff --git a/lib/docset_api_web/endpoint.ex b/lib/docset_api_web/endpoint.ex new file mode 100644 index 0000000..5f7e28c --- /dev/null +++ b/lib/docset_api_web/endpoint.ex @@ -0,0 +1,11 @@ +defmodule DocsetApi.Endpoint do + use Phoenix.Endpoint, otp_app: :docset_api + + plug Plug.Logger, log: :info + + plug Plug.Parsers, + parsers: [:urlencoded], + pass: ["*/*"] + + plug DocsetApi.Router +end diff --git a/lib/docset_api_web/router.ex b/lib/docset_api_web/router.ex new file mode 100644 index 0000000..4aeb84d --- /dev/null +++ b/lib/docset_api_web/router.ex @@ -0,0 +1,8 @@ +defmodule DocsetApi.Router do + use DocsetApi.Web, :router + + scope "/", DocsetApi do + get "/feeds/:package_name", FeedController, :show + get "/docsets/:docset", FeedController, :docset + end +end diff --git a/lib/docset_api_web/templates/feed/show.xml.eex b/lib/docset_api_web/templates/feed/show.xml.eex new file mode 100644 index 0000000..14e405d --- /dev/null +++ b/lib/docset_api_web/templates/feed/show.xml.eex @@ -0,0 +1,4 @@ + + <%= @entry.release.version %> + <%= base_url() %>/docsets/<%= @entry.release.name %>-<%= @entry.release.version %>.tgz + diff --git a/web/views/error_helpers.ex b/lib/docset_api_web/views/error_helpers.ex similarity index 100% rename from web/views/error_helpers.ex rename to lib/docset_api_web/views/error_helpers.ex diff --git a/web/views/error_view.ex b/lib/docset_api_web/views/error_view.ex similarity index 91% rename from web/views/error_view.ex rename to lib/docset_api_web/views/error_view.ex index a9799d5..610fa3b 100644 --- a/web/views/error_view.ex +++ b/lib/docset_api_web/views/error_view.ex @@ -12,6 +12,6 @@ defmodule DocsetApi.ErrorView do # In case no render clause matches or no # template is found, let's render it as 500 def template_not_found(_template, assigns) do - render "500.html", assigns + render("500.html", assigns) end end diff --git a/web/views/feed_view.ex b/lib/docset_api_web/views/feed_view.ex similarity index 100% rename from web/views/feed_view.ex rename to lib/docset_api_web/views/feed_view.ex diff --git a/lib/file_ext.ex b/lib/file_ext.ex deleted file mode 100644 index 82f1882..0000000 --- a/lib/file_ext.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule FileExt do - def ls_r(path \\ ".") do - cond do - File.regular?(path) -> - [path] - - File.dir?(path) -> - File.ls!(path) - |> Enum.map(&Path.join(path, &1)) - |> Enum.map(&ls_r/1) - |> Enum.concat() - - true -> - [] - end - end -end diff --git a/lib/file_parser.ex b/lib/file_parser.ex deleted file mode 100644 index abc3084..0000000 --- a/lib/file_parser.ex +++ /dev/null @@ -1,74 +0,0 @@ -defmodule DocsetApi.FileParser do - require Logger - - defp parse_module(html) do - html - |> Floki.find("#content>h1") - |> Enum.map(fn {tag, attrs, children} -> - children = - children - |> Enum.reject(fn - {_tag, _attrs, _children} -> true - text when is_bitstring(text) -> false - end) - - {tag, attrs, children} - |> Floki.text() - |> String.trim() - end) - |> Enum.reject(&(&1 =~ " ")) - - # Reject all headers with spaces - # in them - they're not modules - end - - defp parse_functions(html) do - html - |> Floki.find("#functions .detail") - |> Floki.attribute("id") - end - - defp parse_macros(html) do - html - |> Floki.find("#macros .detail") - |> Floki.attribute("id") - end - - def parse_file(file, files_dir, callback) when is_function(callback) do - case Path.extname(file) do - ".html" -> - Logger.info("parse #{file}") - - html = Floki.parse_document!(File.read!(file)) - relative_file = Path.relative_to(file, files_dir) - - module = parse_module(html) - - unless module == [] do - functions = parse_functions(html) - macros = parse_macros(html) - - callback.(module, "Module", "#{relative_file}#content") - - for function <- functions do - callback.( - "#{module}.#{function}", - "Function", - "#{relative_file}##{function}" - ) - end - - for macro <- macros do - callback.( - "#{module}.#{macro}", - "Macro", - "#{relative_file}##{macro}" - ) - end - end - - _ -> - Logger.info("skip #{file}") - end - end -end diff --git a/lib/mix/tasks/install_docsets.ex b/lib/mix/tasks/install_docsets.ex new file mode 100644 index 0000000..91dd3ad --- /dev/null +++ b/lib/mix/tasks/install_docsets.ex @@ -0,0 +1,81 @@ +defmodule Mix.Tasks.HexdocsDocset.InstallDocsets do + use Mix.Task + + @usage "mix hexdocs_docset.install_docsets package_name [version] [output_path]" + + @shortdoc "Generate Dash-compatible docsets" + @moduledoc """ + + Usage: + + $ #{@usage} + """ + + def run([package | _] = args) when is_binary(package) do + Mix.Task.run("app.start") + + # generate a docset for this codebase + # TODO: Handle version requirements + {:ok, build} = DocsetApi.Builder.build(package) + + # Detect if optional flags are present + copy_to_index = Enum.find_index(args, fn x -> x == "--copy-to" end) + compress? = Enum.member?(args, "--compress") + + build_ok? = fn -> + cond do + is_tuple(build) -> false + true -> true + end + end + + # Wrap this in a function so it's only called at the time that + # `copy_to` is accessed. + copy_to = fn -> + Enum.at(args, copy_to_index + 1) + |> Path.expand() + end + + if build_ok? do + {:ok, build} = + cond do + compress? and copy_to_index -> + DocsetApi.Builder.build_tarball(build, copy_to.()) + + copy_to_index -> + DocsetApi.Builder.copy_docset(build, copy_to.()) + + compress? -> + DocsetApi.Builder.build_tarball(build, File.cwd!()) + + true -> + build + end + + Mix.shell().info(""" + Conversion completed. Here are the results: + + #{inspect(build, pretty: true)} + + """) + else + Mix.shell().info(""" + The conversion failed: + + #{inspect(build, pretty: true)} + + """) + + end + end + + def run(_args), + do: + Mix.shell().error(""" + Invalid parameters. + + Usage: + + #{@usage} + """) +end diff --git a/mix.exs b/mix.exs index 568400f..0f2d8a5 100644 --- a/mix.exs +++ b/mix.exs @@ -4,68 +4,35 @@ defmodule DocsetApi.Mixfile do def project do [ app: :docset_api, - version: "0.1.0", + version: File.read!("VERSION"), elixir: "~> 1.10", elixirc_paths: elixirc_paths(Mix.env()), - compilers: [:phoenix] ++ Mix.compilers(), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, - aliases: aliases(), deps: deps() ] end - # Configuration for the OTP application. - # - # Type `mix help compile.app` for more information. def application do [ - mod: {DocsetApi, []}, - applications: [ - :phoenix, - :phoenix_pubsub, - :phoenix_html, - :cowboy, - :logger, - :httpoison, - :poison, - :sqlitex, - :floki - ] + mod: {DocsetApi, []} ] end - # Specifies which paths to compile per environment. defp elixirc_paths(:test), do: ["lib", "web", "test/support"] defp elixirc_paths(_), do: ["lib", "web"] - # Specifies your project dependencies. - # - # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.5"}, - {:phoenix_html, "~> 2.14"}, - {:plug_cowboy, "~> 2.3"}, - {:httpoison, "~> 1.7"}, - {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0"}, - {:sqlitex, "~> 1.7"}, - {:floki, "~> 0.28"} - ] - end - - # Aliases are shortcuts or tasks specific to the current project. - # For example, to create, migrate and run the seeds file at once: - # - # $ mix ecto.setup - # - # See the documentation for `Mix` for more info on aliases. - defp aliases do - [ - "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], - "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate", "test"], - start: ["phx.server"] + {:phoenix, "~> 1.6"}, + {:phoenix_html, "~> 3.0"}, + {:phoenix_view, "~> 2.0"}, + {:plug_cowboy, "~> 2.7"}, + {:httpoison, "~> 2.2"}, + {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0"}, + {:exqlite, "~> 0.27"}, + {:floki, ">= 0.3.0"}, + {:ex_doc, "~> 0.29.1"} ] end end diff --git a/mix.lock b/mix.lock index 786720e..c115740 100644 --- a/mix.lock +++ b/mix.lock @@ -1,39 +1,41 @@ %{ - "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, - "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, - "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, - "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, - "db_connection": {:hex, :db_connection, "1.0.0-rc.5", "1d9ab6e01387bdf2de7a16c56866971f7c2f75aea7c69cae2a0346e4b537ae0d", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0.0-beta.3", [hex: :sbroker, optional: true]}]}, - "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, - "ecto": {:hex, :ecto, "2.0.5", "7f4c79ac41ffba1a4c032b69d7045489f0069c256de606523c65d9f8188e502d", [:mix], [{:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]}, - "esqlite": {:hex, :esqlite, "0.4.1", "ba5d0bab6b9c8432ffe1bf12fee8e154a50f1c3c40eadc3a9c870c23ca94d961", [:rebar3], [], "hexpm", "3584ca33172f4815ce56e96eed9835f5d8c987a9000fbc8c376c86acef8bf965"}, - "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, - "floki": {:hex, :floki, "0.28.0", "0d0795a17189510ee01323e6990f906309e9fc6e8570219135211f1264d78c7f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "db1549560874ebba5a6367e46c3aec5fedd41f2757ad6efe567efb04b4d4ee55"}, - "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm", "9a00246e8af58cdf465ae7c48fd6fd7ba2e43300413dfcc25447ecd3bf76f0c1"}, - "gettext": {:hex, :gettext, "0.18.1", "89e8499b051c7671fa60782faf24409b5d2306aa71feb43d79648a8bc63d0522", [:mix], [], "hexpm", "e70750c10a5f88cb8dc026fc28fa101529835026dec4a06dba3b614f2a99c7a9"}, - "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, - "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, - "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, - "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, + "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, + "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, + "exqlite": {:hex, :exqlite, "0.27.0", "2ef6021862e74c6253d1fb1f5701bd47e4e779b035d34daf2a13ec83945a05ba", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "b947b9db15bb7aad11da6cd18a0d8b78f7fcce89508a27a5b9be18350fe12c59"}, + "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mochiweb_html": {:hex, :mochiweb_html, "2.15.0", "d7402e967d7f9f2912f8befa813c37be62d5eeeddbbcb6fe986c44e01460d497", [:rebar3], [], "hexpm", "7651a4ef29bd6d69819b37b6aa12c7616c5cf75e67ccd849cfb499e2bbbf0ce6"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "phoenix": {:hex, :phoenix, "1.5.4", "0fca9ce7e960f9498d6315e41fcd0c80bfa6fbeb5fa3255b830c67fdfb7e703f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4e516d131fde87b568abd62e1b14aa07ba7d5edfd230bab4e25cc9dedbb39135"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "3.0.1", "42eb486ef732cf209d0a353e791806721f33ff40beab0a86f02070a5649ed00a", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, - "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.4", "940c0344b1d66a2e46eef02af3a70e0c5bb45a4db0bf47917add271b76cd3914", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "38f9308357dea4cc77f247e216da99fcb0224e05ada1469167520bed4cb8cccd"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, - "plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"}, - "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, - "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, - "postgrex": {:hex, :postgrex, "0.12.1", "2f8b46cb3a44dcd42f42938abedbfffe7e103ba4ce810ccbeee8dcf27ca0fb06", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, - "sqlitex": {:hex, :sqlitex, "1.7.1", "022d477aab2ae999c43ae6fbd1782ff1457e0e95c251c7b5fa6f7b7b102040ff", [:mix], [{:decimal, "~> 1.7", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm", "ef16cda37b151136a47a6c0830dc9eb5e5f8f5f029b649e9f3a58a6eed634b80"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, } diff --git a/mix.nix b/mix.nix new file mode 100644 index 0000000..574f404 --- /dev/null +++ b/mix.nix @@ -0,0 +1,441 @@ +{ lib, beamPackages, overrides ? (x: y: {}) }: + +let + buildRebar3 = lib.makeOverridable beamPackages.buildRebar3; + buildMix = lib.makeOverridable beamPackages.buildMix; + buildErlangMk = lib.makeOverridable beamPackages.buildErlangMk; + + self = packages // (overrides self packages); + + packages = with beamPackages; with self; { + castore = buildMix rec { + name = "castore"; + version = "1.0.9"; + + src = fetchHex { + pkg = "castore"; + version = "${version}"; + sha256 = "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"; + }; + + beamDeps = []; + }; + + cc_precompiler = buildMix rec { + name = "cc_precompiler"; + version = "0.1.10"; + + src = fetchHex { + pkg = "cc_precompiler"; + version = "${version}"; + sha256 = "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"; + }; + + beamDeps = [ elixir_make ]; + }; + + certifi = buildRebar3 rec { + name = "certifi"; + version = "2.12.0"; + + src = fetchHex { + pkg = "certifi"; + version = "${version}"; + sha256 = "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"; + }; + + beamDeps = []; + }; + + cowboy = buildErlangMk rec { + name = "cowboy"; + version = "2.12.0"; + + src = fetchHex { + pkg = "cowboy"; + version = "${version}"; + sha256 = "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"; + }; + + beamDeps = [ cowlib ranch ]; + }; + + cowboy_telemetry = buildRebar3 rec { + name = "cowboy_telemetry"; + version = "0.4.0"; + + src = fetchHex { + pkg = "cowboy_telemetry"; + version = "${version}"; + sha256 = "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"; + }; + + beamDeps = [ cowboy telemetry ]; + }; + + cowlib = buildRebar3 rec { + name = "cowlib"; + version = "2.13.0"; + + src = fetchHex { + pkg = "cowlib"; + version = "${version}"; + sha256 = "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"; + }; + + beamDeps = []; + }; + + db_connection = buildMix rec { + name = "db_connection"; + version = "2.7.0"; + + src = fetchHex { + pkg = "db_connection"; + version = "${version}"; + sha256 = "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"; + }; + + beamDeps = [ telemetry ]; + }; + + decimal = buildMix rec { + name = "decimal"; + version = "1.9.0"; + + src = fetchHex { + pkg = "decimal"; + version = "${version}"; + sha256 = "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"; + }; + + beamDeps = []; + }; + + elixir_make = buildMix rec { + name = "elixir_make"; + version = "0.8.4"; + + src = fetchHex { + pkg = "elixir_make"; + version = "${version}"; + sha256 = "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"; + }; + + beamDeps = [ castore certifi ]; + }; + + exqlite = buildMix rec { + name = "exqlite"; + version = "0.27.0"; + + src = fetchHex { + pkg = "exqlite"; + version = "${version}"; + sha256 = "b947b9db15bb7aad11da6cd18a0d8b78f7fcce89508a27a5b9be18350fe12c59"; + }; + + beamDeps = [ cc_precompiler db_connection elixir_make ]; + }; + + floki = buildMix rec { + name = "floki"; + version = "0.36.3"; + + src = fetchHex { + pkg = "floki"; + version = "${version}"; + sha256 = "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"; + }; + + beamDeps = []; + }; + + hackney = buildRebar3 rec { + name = "hackney"; + version = "1.20.1"; + + src = fetchHex { + pkg = "hackney"; + version = "${version}"; + sha256 = "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"; + }; + + beamDeps = [ certifi idna metrics mimerl parse_trans ssl_verify_fun unicode_util_compat ]; + }; + + httpoison = buildMix rec { + name = "httpoison"; + version = "2.2.1"; + + src = fetchHex { + pkg = "httpoison"; + version = "${version}"; + sha256 = "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"; + }; + + beamDeps = [ hackney ]; + }; + + idna = buildRebar3 rec { + name = "idna"; + version = "6.1.1"; + + src = fetchHex { + pkg = "idna"; + version = "${version}"; + sha256 = "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"; + }; + + beamDeps = [ unicode_util_compat ]; + }; + + metrics = buildRebar3 rec { + name = "metrics"; + version = "1.0.1"; + + src = fetchHex { + pkg = "metrics"; + version = "${version}"; + sha256 = "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"; + }; + + beamDeps = []; + }; + + mime = buildMix rec { + name = "mime"; + version = "2.0.6"; + + src = fetchHex { + pkg = "mime"; + version = "${version}"; + sha256 = "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"; + }; + + beamDeps = []; + }; + + mimerl = buildRebar3 rec { + name = "mimerl"; + version = "1.3.0"; + + src = fetchHex { + pkg = "mimerl"; + version = "${version}"; + sha256 = "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"; + }; + + beamDeps = []; + }; + + parse_trans = buildRebar3 rec { + name = "parse_trans"; + version = "3.4.1"; + + src = fetchHex { + pkg = "parse_trans"; + version = "${version}"; + sha256 = "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"; + }; + + beamDeps = []; + }; + + phoenix = buildMix rec { + name = "phoenix"; + version = "1.7.14"; + + src = fetchHex { + pkg = "phoenix"; + version = "${version}"; + sha256 = "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"; + }; + + beamDeps = [ castore phoenix_pubsub phoenix_template phoenix_view plug plug_cowboy plug_crypto telemetry websock_adapter ]; + }; + + phoenix_html = buildMix rec { + name = "phoenix_html"; + version = "3.3.4"; + + src = fetchHex { + pkg = "phoenix_html"; + version = "${version}"; + sha256 = "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"; + }; + + beamDeps = [ plug ]; + }; + + phoenix_pubsub = buildMix rec { + name = "phoenix_pubsub"; + version = "2.1.3"; + + src = fetchHex { + pkg = "phoenix_pubsub"; + version = "${version}"; + sha256 = "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"; + }; + + beamDeps = []; + }; + + phoenix_template = buildMix rec { + name = "phoenix_template"; + version = "1.0.4"; + + src = fetchHex { + pkg = "phoenix_template"; + version = "${version}"; + sha256 = "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"; + }; + + beamDeps = [ phoenix_html ]; + }; + + phoenix_view = buildMix rec { + name = "phoenix_view"; + version = "2.0.4"; + + src = fetchHex { + pkg = "phoenix_view"; + version = "${version}"; + sha256 = "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"; + }; + + beamDeps = [ phoenix_html phoenix_template ]; + }; + + plug = buildMix rec { + name = "plug"; + version = "1.16.1"; + + src = fetchHex { + pkg = "plug"; + version = "${version}"; + sha256 = "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"; + }; + + beamDeps = [ mime plug_crypto telemetry ]; + }; + + plug_cowboy = buildMix rec { + name = "plug_cowboy"; + version = "2.7.2"; + + src = fetchHex { + pkg = "plug_cowboy"; + version = "${version}"; + sha256 = "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"; + }; + + beamDeps = [ cowboy cowboy_telemetry plug ]; + }; + + plug_crypto = buildMix rec { + name = "plug_crypto"; + version = "2.1.0"; + + src = fetchHex { + pkg = "plug_crypto"; + version = "${version}"; + sha256 = "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"; + }; + + beamDeps = []; + }; + + poison = buildMix rec { + name = "poison"; + version = "4.0.1"; + + src = fetchHex { + pkg = "poison"; + version = "${version}"; + sha256 = "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"; + }; + + beamDeps = []; + }; + + ranch = buildRebar3 rec { + name = "ranch"; + version = "1.8.0"; + + src = fetchHex { + pkg = "ranch"; + version = "${version}"; + sha256 = "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"; + }; + + beamDeps = []; + }; + + ssl_verify_fun = buildRebar3 rec { + name = "ssl_verify_fun"; + version = "1.1.7"; + + src = fetchHex { + pkg = "ssl_verify_fun"; + version = "${version}"; + sha256 = "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"; + }; + + beamDeps = []; + }; + + telemetry = buildRebar3 rec { + name = "telemetry"; + version = "1.3.0"; + + src = fetchHex { + pkg = "telemetry"; + version = "${version}"; + sha256 = "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"; + }; + + beamDeps = []; + }; + + unicode_util_compat = buildRebar3 rec { + name = "unicode_util_compat"; + version = "0.7.0"; + + src = fetchHex { + pkg = "unicode_util_compat"; + version = "${version}"; + sha256 = "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"; + }; + + beamDeps = []; + }; + + websock = buildMix rec { + name = "websock"; + version = "0.5.3"; + + src = fetchHex { + pkg = "websock"; + version = "${version}"; + sha256 = "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"; + }; + + beamDeps = []; + }; + + websock_adapter = buildMix rec { + name = "websock_adapter"; + version = "0.5.7"; + + src = fetchHex { + pkg = "websock_adapter"; + version = "${version}"; + sha256 = "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"; + }; + + beamDeps = [ plug plug_cowboy websock ]; + }; + }; +in self + diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs deleted file mode 100644 index 994fd71..0000000 --- a/priv/repo/seeds.exs +++ /dev/null @@ -1,11 +0,0 @@ -# Script for populating the database. You can run it as: -# -# mix run priv/repo/seeds.exs -# -# Inside the script, you can read and write to any of your -# repositories directly: -# -# DocsetApi.Repo.insert!(%DocsetApi.SomeModel{}) -# -# We recommend using the bang functions (`insert!`, `update!` -# and so on) as they will fail if something goes wrong. diff --git a/priv/static/css/app.css b/priv/static/css/app.css deleted file mode 100644 index edcce5e..0000000 --- a/priv/static/css/app.css +++ /dev/null @@ -1,82 +0,0 @@ -/* This file is for your main application css. *//* Includes Bootstrap as well as some default style for the starter - * application. This can be safely deleted to start fresh. - */ - -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.33px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:3;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} - -/* Space out content a bit */ -body, form, ul, table { - margin-top: 20px; - margin-bottom: 20px; -} - -/* Phoenix flash messages */ -.alert:empty { display: none; } - -/* Phoenix inline forms in links and buttons */ -form.link, form.button { - display: inline; -} - -/* Custom page header */ -.header { - border-bottom: 1px solid #e5e5e5; -} -.logo { - width: 519px; - height: 71px; - display: inline-block; - margin-bottom: 1em; - background-image: url("/images/phoenix.png"); - background-size: 519px 71px; -} - -/* Everything but the jumbotron gets side spacing for mobile first views */ -.header, -.marketing { - padding-right: 15px; - padding-left: 15px; -} - -/* Customize container */ -@media (min-width: 768px) { - .container { - max-width: 730px; - } -} -.container-narrow > hr { - margin: 30px 0; -} - -/* Main marketing message */ -.jumbotron { - text-align: center; - border-bottom: 1px solid #e5e5e5; -} - -/* Supporting marketing content */ -.marketing { - margin: 35px 0; -} - -/* Responsive: Portrait tablets and up */ -@media screen and (min-width: 768px) { - /* Remove the padding we set earlier */ - .header, - .marketing { - padding-right: 0; - padding-left: 0; - } - /* Space out the masthead */ - .header { - margin-bottom: 30px; - } - /* Remove the bottom border on the jumbotron for visual effect */ - .jumbotron { - border-bottom: 0; - } -} \ No newline at end of file diff --git a/priv/static/docsets/.keep b/priv/static/docsets/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico deleted file mode 100644 index 73de524..0000000 Binary files a/priv/static/favicon.ico and /dev/null differ diff --git a/priv/static/images/phoenix.png b/priv/static/images/phoenix.png deleted file mode 100644 index 9c81075..0000000 Binary files a/priv/static/images/phoenix.png and /dev/null differ diff --git a/priv/static/js/app.js b/priv/static/js/app.js deleted file mode 100644 index 3130448..0000000 --- a/priv/static/js/app.js +++ /dev/null @@ -1,3 +0,0 @@ -// for phoenix_html support, including form and button helpers -// copy the following scripts into your javascript bundle: -// * https://raw.githubusercontent.com/phoenixframework/phoenix_html/v2.3.0/priv/static/phoenix_html.js \ No newline at end of file diff --git a/priv/static/js/phoenix.js b/priv/static/js/phoenix.js deleted file mode 100644 index 1128901..0000000 --- a/priv/static/js/phoenix.js +++ /dev/null @@ -1,1271 +0,0 @@ -(function(exports){ -"use strict"; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -// Phoenix Channels JavaScript client -// -// ## Socket Connection -// -// A single connection is established to the server and -// channels are multiplexed over the connection. -// Connect to the server using the `Socket` class: -// -// let socket = new Socket("/ws", {params: {userToken: "123"}}) -// socket.connect() -// -// The `Socket` constructor takes the mount point of the socket, -// the authentication params, as well as options that can be found in -// the Socket docs, such as configuring the `LongPoll` transport, and -// heartbeat. -// -// ## Channels -// -// Channels are isolated, concurrent processes on the server that -// subscribe to topics and broker events between the client and server. -// To join a channel, you must provide the topic, and channel params for -// authorization. Here's an example chat room example where `"new_msg"` -// events are listened for, messages are pushed to the server, and -// the channel is joined with ok/error/timeout matches: -// -// let channel = socket.channel("room:123", {token: roomToken}) -// channel.on("new_msg", msg => console.log("Got message", msg) ) -// $input.onEnter( e => { -// channel.push("new_msg", {body: e.target.val}, 10000) -// .receive("ok", (msg) => console.log("created message", msg) ) -// .receive("error", (reasons) => console.log("create failed", reasons) ) -// .receive("timeout", () => console.log("Networking issue...") ) -// }) -// channel.join() -// .receive("ok", ({messages}) => console.log("catching up", messages) ) -// .receive("error", ({reason}) => console.log("failed join", reason) ) -// .receive("timeout", () => console.log("Networking issue. Still waiting...") ) -// -// -// ## Joining -// -// Creating a channel with `socket.channel(topic, params)`, binds the params to -// `channel.params`, which are sent up on `channel.join()`. -// Subsequent rejoins will send up the modified params for -// updating authorization params, or passing up last_message_id information. -// Successful joins receive an "ok" status, while unsuccessful joins -// receive "error". -// -// ## Duplicate Join Subscriptions -// -// While the client may join any number of topics on any number of channels, -// the client may only hold a single subscription for each unique topic at any -// given time. When attempting to create a duplicate subscription, -// the server will close the existing channel, log a warning, and -// spawn a new channel for the topic. The client will have their -// `channel.onClose` callbacks fired for the existing channel, and the new -// channel join will have its receive hooks processed as normal. -// -// ## Pushing Messages -// -// From the previous example, we can see that pushing messages to the server -// can be done with `channel.push(eventName, payload)` and we can optionally -// receive responses from the push. Additionally, we can use -// `receive("timeout", callback)` to abort waiting for our other `receive` hooks -// and take action after some period of waiting. The default timeout is 5000ms. -// -// -// ## Socket Hooks -// -// Lifecycle events of the multiplexed connection can be hooked into via -// `socket.onError()` and `socket.onClose()` events, ie: -// -// socket.onError( () => console.log("there was an error with the connection!") ) -// socket.onClose( () => console.log("the connection dropped") ) -// -// -// ## Channel Hooks -// -// For each joined channel, you can bind to `onError` and `onClose` events -// to monitor the channel lifecycle, ie: -// -// channel.onError( () => console.log("there was an error!") ) -// channel.onClose( () => console.log("the channel has gone away gracefully") ) -// -// ### onError hooks -// -// `onError` hooks are invoked if the socket connection drops, or the channel -// crashes on the server. In either case, a channel rejoin is attempted -// automatically in an exponential backoff manner. -// -// ### onClose hooks -// -// `onClose` hooks are invoked only in two cases. 1) the channel explicitly -// closed on the server, or 2). The client explicitly closed, by calling -// `channel.leave()` -// -// -// ## Presence -// -// The `Presence` object provides features for syncing presence information -// from the server with the client and handling presences joining and leaving. -// -// ### Syncing initial state from the server -// -// `Presence.syncState` is used to sync the list of presences on the server -// with the client's state. An optional `onJoin` and `onLeave` callback can -// be provided to react to changes in the client's local presences across -// disconnects and reconnects with the server. -// -// `Presence.syncDiff` is used to sync a diff of presence join and leave -// events from the server, as they happen. Like `syncState`, `syncDiff` -// accepts optional `onJoin` and `onLeave` callbacks to react to a user -// joining or leaving from a device. -// -// ### Listing Presences -// -// `Presence.list` is used to return a list of presence information -// based on the local state of metadata. By default, all presence -// metadata is returned, but a `listBy` function can be supplied to -// allow the client to select which metadata to use for a given presence. -// For example, you may have a user online from different devices with a -// a metadata status of "online", but they have set themselves to "away" -// on another device. In this case, they app may choose to use the "away" -// status for what appears on the UI. The example below defines a `listBy` -// function which prioritizes the first metadata which was registered for -// each user. This could be the first tab they opened, or the first device -// they came online from: -// -// let state = {} -// state = Presence.syncState(state, stateFromServer) -// let listBy = (id, {metas: [first, ...rest]}) => { -// first.count = rest.length + 1 // count of this user's presences -// first.id = id -// return first -// } -// let onlineUsers = Presence.list(state, listBy) -// -// -// ### Example Usage -// -// // detect if user has joined for the 1st time or from another tab/device -// let onJoin = (id, current, newPres) => { -// if(!current){ -// console.log("user has entered for the first time", newPres) -// } else { -// console.log("user additional presence", newPres) -// } -// } -// // detect if user has left from all tabs/devices, or is still present -// let onLeave = (id, current, leftPres) => { -// if(current.metas.length === 0){ -// console.log("user has left from all devices", leftPres) -// } else { -// console.log("user left from a device", leftPres) -// } -// } -// let presences = {} // client's initial empty presence state -// // receive initial presence data from server, sent after join -// myChannel.on("presences", state => { -// presences = Presence.syncState(presences, state, onJoin, onLeave) -// displayUsers(Presence.list(presences)) -// }) -// // receive "presence_diff" from server, containing join/leave events -// myChannel.on("presence_diff", diff => { -// presences = Presence.syncDiff(presences, diff, onJoin, onLeave) -// this.setState({users: Presence.list(room.presences, listBy)}) -// }) -// -var VSN = "1.0.0"; -var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 }; -var DEFAULT_TIMEOUT = 10000; -var CHANNEL_STATES = { - closed: "closed", - errored: "errored", - joined: "joined", - joining: "joining", - leaving: "leaving" -}; -var CHANNEL_EVENTS = { - close: "phx_close", - error: "phx_error", - join: "phx_join", - reply: "phx_reply", - leave: "phx_leave" -}; -var TRANSPORTS = { - longpoll: "longpoll", - websocket: "websocket" -}; - -var Push = function () { - - // Initializes the Push - // - // channel - The Channel - // event - The event, for example `"phx_join"` - // payload - The payload, for example `{user_id: 123}` - // timeout - The push timeout in milliseconds - // - - function Push(channel, event, payload, timeout) { - _classCallCheck(this, Push); - - this.channel = channel; - this.event = event; - this.payload = payload || {}; - this.receivedResp = null; - this.timeout = timeout; - this.timeoutTimer = null; - this.recHooks = []; - this.sent = false; - } - - _createClass(Push, [{ - key: "resend", - value: function resend(timeout) { - this.timeout = timeout; - this.cancelRefEvent(); - this.ref = null; - this.refEvent = null; - this.receivedResp = null; - this.sent = false; - this.send(); - } - }, { - key: "send", - value: function send() { - if (this.hasReceived("timeout")) { - return; - } - this.startTimeout(); - this.sent = true; - this.channel.socket.push({ - topic: this.channel.topic, - event: this.event, - payload: this.payload, - ref: this.ref - }); - } - }, { - key: "receive", - value: function receive(status, callback) { - if (this.hasReceived(status)) { - callback(this.receivedResp.response); - } - - this.recHooks.push({ status: status, callback: callback }); - return this; - } - - // private - - }, { - key: "matchReceive", - value: function matchReceive(_ref) { - var status = _ref.status; - var response = _ref.response; - var ref = _ref.ref; - - this.recHooks.filter(function (h) { - return h.status === status; - }).forEach(function (h) { - return h.callback(response); - }); - } - }, { - key: "cancelRefEvent", - value: function cancelRefEvent() { - if (!this.refEvent) { - return; - } - this.channel.off(this.refEvent); - } - }, { - key: "cancelTimeout", - value: function cancelTimeout() { - clearTimeout(this.timeoutTimer); - this.timeoutTimer = null; - } - }, { - key: "startTimeout", - value: function startTimeout() { - var _this = this; - - if (this.timeoutTimer) { - return; - } - this.ref = this.channel.socket.makeRef(); - this.refEvent = this.channel.replyEventName(this.ref); - - this.channel.on(this.refEvent, function (payload) { - _this.cancelRefEvent(); - _this.cancelTimeout(); - _this.receivedResp = payload; - _this.matchReceive(payload); - }); - - this.timeoutTimer = setTimeout(function () { - _this.trigger("timeout", {}); - }, this.timeout); - } - }, { - key: "hasReceived", - value: function hasReceived(status) { - return this.receivedResp && this.receivedResp.status === status; - } - }, { - key: "trigger", - value: function trigger(status, response) { - this.channel.trigger(this.refEvent, { status: status, response: response }); - } - }]); - - return Push; -}(); - -var Channel = exports.Channel = function () { - function Channel(topic, params, socket) { - var _this2 = this; - - _classCallCheck(this, Channel); - - this.state = CHANNEL_STATES.closed; - this.topic = topic; - this.params = params || {}; - this.socket = socket; - this.bindings = []; - this.timeout = this.socket.timeout; - this.joinedOnce = false; - this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout); - this.pushBuffer = []; - this.rejoinTimer = new Timer(function () { - return _this2.rejoinUntilConnected(); - }, this.socket.reconnectAfterMs); - this.joinPush.receive("ok", function () { - _this2.state = CHANNEL_STATES.joined; - _this2.rejoinTimer.reset(); - _this2.pushBuffer.forEach(function (pushEvent) { - return pushEvent.send(); - }); - _this2.pushBuffer = []; - }); - this.onClose(function () { - _this2.rejoinTimer.reset(); - _this2.socket.log("channel", "close " + _this2.topic + " " + _this2.joinRef()); - _this2.state = CHANNEL_STATES.closed; - _this2.socket.remove(_this2); - }); - this.onError(function (reason) { - if (_this2.isLeaving() || _this2.isClosed()) { - return; - } - _this2.socket.log("channel", "error " + _this2.topic, reason); - _this2.state = CHANNEL_STATES.errored; - _this2.rejoinTimer.scheduleTimeout(); - }); - this.joinPush.receive("timeout", function () { - if (!_this2.isJoining()) { - return; - } - _this2.socket.log("channel", "timeout " + _this2.topic, _this2.joinPush.timeout); - _this2.state = CHANNEL_STATES.errored; - _this2.rejoinTimer.scheduleTimeout(); - }); - this.on(CHANNEL_EVENTS.reply, function (payload, ref) { - _this2.trigger(_this2.replyEventName(ref), payload); - }); - } - - _createClass(Channel, [{ - key: "rejoinUntilConnected", - value: function rejoinUntilConnected() { - this.rejoinTimer.scheduleTimeout(); - if (this.socket.isConnected()) { - this.rejoin(); - } - } - }, { - key: "join", - value: function join() { - var timeout = arguments.length <= 0 || arguments[0] === undefined ? this.timeout : arguments[0]; - - if (this.joinedOnce) { - throw "tried to join multiple times. 'join' can only be called a single time per channel instance"; - } else { - this.joinedOnce = true; - this.rejoin(timeout); - return this.joinPush; - } - } - }, { - key: "onClose", - value: function onClose(callback) { - this.on(CHANNEL_EVENTS.close, callback); - } - }, { - key: "onError", - value: function onError(callback) { - this.on(CHANNEL_EVENTS.error, function (reason) { - return callback(reason); - }); - } - }, { - key: "on", - value: function on(event, callback) { - this.bindings.push({ event: event, callback: callback }); - } - }, { - key: "off", - value: function off(event) { - this.bindings = this.bindings.filter(function (bind) { - return bind.event !== event; - }); - } - }, { - key: "canPush", - value: function canPush() { - return this.socket.isConnected() && this.isJoined(); - } - }, { - key: "push", - value: function push(event, payload) { - var timeout = arguments.length <= 2 || arguments[2] === undefined ? this.timeout : arguments[2]; - - if (!this.joinedOnce) { - throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use channel.join() before pushing events"; - } - var pushEvent = new Push(this, event, payload, timeout); - if (this.canPush()) { - pushEvent.send(); - } else { - pushEvent.startTimeout(); - this.pushBuffer.push(pushEvent); - } - - return pushEvent; - } - - // Leaves the channel - // - // Unsubscribes from server events, and - // instructs channel to terminate on server - // - // Triggers onClose() hooks - // - // To receive leave acknowledgements, use the a `receive` - // hook to bind to the server ack, ie: - // - // channel.leave().receive("ok", () => alert("left!") ) - // - - }, { - key: "leave", - value: function leave() { - var _this3 = this; - - var timeout = arguments.length <= 0 || arguments[0] === undefined ? this.timeout : arguments[0]; - - this.state = CHANNEL_STATES.leaving; - var onClose = function onClose() { - _this3.socket.log("channel", "leave " + _this3.topic); - _this3.trigger(CHANNEL_EVENTS.close, "leave", _this3.joinRef()); - }; - var leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout); - leavePush.receive("ok", function () { - return onClose(); - }).receive("timeout", function () { - return onClose(); - }); - leavePush.send(); - if (!this.canPush()) { - leavePush.trigger("ok", {}); - } - - return leavePush; - } - - // Overridable message hook - // - // Receives all events for specialized message handling - // before dispatching to the channel callbacks. - // - // Must return the payload, modified or unmodified - - }, { - key: "onMessage", - value: function onMessage(event, payload, ref) { - return payload; - } - - // private - - }, { - key: "isMember", - value: function isMember(topic) { - return this.topic === topic; - } - }, { - key: "joinRef", - value: function joinRef() { - return this.joinPush.ref; - } - }, { - key: "sendJoin", - value: function sendJoin(timeout) { - this.state = CHANNEL_STATES.joining; - this.joinPush.resend(timeout); - } - }, { - key: "rejoin", - value: function rejoin() { - var timeout = arguments.length <= 0 || arguments[0] === undefined ? this.timeout : arguments[0]; - if (this.isLeaving()) { - return; - } - this.sendJoin(timeout); - } - }, { - key: "trigger", - value: function trigger(event, payload, ref) { - var close = CHANNEL_EVENTS.close; - var error = CHANNEL_EVENTS.error; - var leave = CHANNEL_EVENTS.leave; - var join = CHANNEL_EVENTS.join; - - if (ref && [close, error, leave, join].indexOf(event) >= 0 && ref !== this.joinRef()) { - return; - } - var handledPayload = this.onMessage(event, payload, ref); - if (payload && !handledPayload) { - throw "channel onMessage callbacks must return the payload, modified or unmodified"; - } - - this.bindings.filter(function (bind) { - return bind.event === event; - }).map(function (bind) { - return bind.callback(handledPayload, ref); - }); - } - }, { - key: "replyEventName", - value: function replyEventName(ref) { - return "chan_reply_" + ref; - } - }, { - key: "isClosed", - value: function isClosed() { - return this.state === CHANNEL_STATES.closed; - } - }, { - key: "isErrored", - value: function isErrored() { - return this.state === CHANNEL_STATES.errored; - } - }, { - key: "isJoined", - value: function isJoined() { - return this.state === CHANNEL_STATES.joined; - } - }, { - key: "isJoining", - value: function isJoining() { - return this.state === CHANNEL_STATES.joining; - } - }, { - key: "isLeaving", - value: function isLeaving() { - return this.state === CHANNEL_STATES.leaving; - } - }]); - - return Channel; -}(); - -var Socket = exports.Socket = function () { - - // Initializes the Socket - // - // endPoint - The string WebSocket endpoint, ie, "ws://example.com/ws", - // "wss://example.com" - // "/ws" (inherited host & protocol) - // opts - Optional configuration - // transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll. - // Defaults to WebSocket with automatic LongPoll fallback. - // timeout - The default timeout in milliseconds to trigger push timeouts. - // Defaults `DEFAULT_TIMEOUT` - // heartbeatIntervalMs - The millisec interval to send a heartbeat message - // reconnectAfterMs - The optional function that returns the millsec - // reconnect interval. Defaults to stepped backoff of: - // - // function(tries){ - // return [1000, 5000, 10000][tries - 1] || 10000 - // } - // - // logger - The optional function for specialized logging, ie: - // `logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) } - // - // longpollerTimeout - The maximum timeout of a long poll AJAX request. - // Defaults to 20s (double the server long poll timer). - // - // params - The optional params to pass when connecting - // - // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim) - // - - function Socket(endPoint) { - var _this4 = this; - - var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - _classCallCheck(this, Socket); - - this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] }; - this.channels = []; - this.sendBuffer = []; - this.ref = 0; - this.timeout = opts.timeout || DEFAULT_TIMEOUT; - this.transport = opts.transport || window.WebSocket || LongPoll; - this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000; - this.reconnectAfterMs = opts.reconnectAfterMs || function (tries) { - return [1000, 2000, 5000, 10000][tries - 1] || 10000; - }; - this.logger = opts.logger || function () {}; // noop - this.longpollerTimeout = opts.longpollerTimeout || 20000; - this.params = opts.params || {}; - this.endPoint = endPoint + "/" + TRANSPORTS.websocket; - this.reconnectTimer = new Timer(function () { - _this4.disconnect(function () { - return _this4.connect(); - }); - }, this.reconnectAfterMs); - } - - _createClass(Socket, [{ - key: "protocol", - value: function protocol() { - return location.protocol.match(/^https/) ? "wss" : "ws"; - } - }, { - key: "endPointURL", - value: function endPointURL() { - var uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params), { vsn: VSN }); - if (uri.charAt(0) !== "/") { - return uri; - } - if (uri.charAt(1) === "/") { - return this.protocol() + ":" + uri; - } - - return this.protocol() + "://" + location.host + uri; - } - }, { - key: "disconnect", - value: function disconnect(callback, code, reason) { - if (this.conn) { - this.conn.onclose = function () {}; // noop - if (code) { - this.conn.close(code, reason || ""); - } else { - this.conn.close(); - } - this.conn = null; - } - callback && callback(); - } - - // params - The params to send when connecting, for example `{user_id: userToken}` - - }, { - key: "connect", - value: function connect(params) { - var _this5 = this; - - if (params) { - console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"); - this.params = params; - } - if (this.conn) { - return; - } - - this.conn = new this.transport(this.endPointURL()); - this.conn.timeout = this.longpollerTimeout; - this.conn.onopen = function () { - return _this5.onConnOpen(); - }; - this.conn.onerror = function (error) { - return _this5.onConnError(error); - }; - this.conn.onmessage = function (event) { - return _this5.onConnMessage(event); - }; - this.conn.onclose = function (event) { - return _this5.onConnClose(event); - }; - } - - // Logs the message. Override `this.logger` for specialized logging. noops by default - - }, { - key: "log", - value: function log(kind, msg, data) { - this.logger(kind, msg, data); - } - - // Registers callbacks for connection state change events - // - // Examples - // - // socket.onError(function(error){ alert("An error occurred") }) - // - - }, { - key: "onOpen", - value: function onOpen(callback) { - this.stateChangeCallbacks.open.push(callback); - } - }, { - key: "onClose", - value: function onClose(callback) { - this.stateChangeCallbacks.close.push(callback); - } - }, { - key: "onError", - value: function onError(callback) { - this.stateChangeCallbacks.error.push(callback); - } - }, { - key: "onMessage", - value: function onMessage(callback) { - this.stateChangeCallbacks.message.push(callback); - } - }, { - key: "onConnOpen", - value: function onConnOpen() { - var _this6 = this; - - this.log("transport", "connected to " + this.endPointURL(), this.transport.prototype); - this.flushSendBuffer(); - this.reconnectTimer.reset(); - if (!this.conn.skipHeartbeat) { - clearInterval(this.heartbeatTimer); - this.heartbeatTimer = setInterval(function () { - return _this6.sendHeartbeat(); - }, this.heartbeatIntervalMs); - } - this.stateChangeCallbacks.open.forEach(function (callback) { - return callback(); - }); - } - }, { - key: "onConnClose", - value: function onConnClose(event) { - this.log("transport", "close", event); - this.triggerChanError(); - clearInterval(this.heartbeatTimer); - this.reconnectTimer.scheduleTimeout(); - this.stateChangeCallbacks.close.forEach(function (callback) { - return callback(event); - }); - } - }, { - key: "onConnError", - value: function onConnError(error) { - this.log("transport", error); - this.triggerChanError(); - this.stateChangeCallbacks.error.forEach(function (callback) { - return callback(error); - }); - } - }, { - key: "triggerChanError", - value: function triggerChanError() { - this.channels.forEach(function (channel) { - return channel.trigger(CHANNEL_EVENTS.error); - }); - } - }, { - key: "connectionState", - value: function connectionState() { - switch (this.conn && this.conn.readyState) { - case SOCKET_STATES.connecting: - return "connecting"; - case SOCKET_STATES.open: - return "open"; - case SOCKET_STATES.closing: - return "closing"; - default: - return "closed"; - } - } - }, { - key: "isConnected", - value: function isConnected() { - return this.connectionState() === "open"; - } - }, { - key: "remove", - value: function remove(channel) { - this.channels = this.channels.filter(function (c) { - return c.joinRef() !== channel.joinRef(); - }); - } - }, { - key: "channel", - value: function channel(topic) { - var chanParams = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var chan = new Channel(topic, chanParams, this); - this.channels.push(chan); - return chan; - } - }, { - key: "push", - value: function push(data) { - var _this7 = this; - - var topic = data.topic; - var event = data.event; - var payload = data.payload; - var ref = data.ref; - - var callback = function callback() { - return _this7.conn.send(JSON.stringify(data)); - }; - this.log("push", topic + " " + event + " (" + ref + ")", payload); - if (this.isConnected()) { - callback(); - } else { - this.sendBuffer.push(callback); - } - } - - // Return the next message ref, accounting for overflows - - }, { - key: "makeRef", - value: function makeRef() { - var newRef = this.ref + 1; - if (newRef === this.ref) { - this.ref = 0; - } else { - this.ref = newRef; - } - - return this.ref.toString(); - } - }, { - key: "sendHeartbeat", - value: function sendHeartbeat() { - if (!this.isConnected()) { - return; - } - this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.makeRef() }); - } - }, { - key: "flushSendBuffer", - value: function flushSendBuffer() { - if (this.isConnected() && this.sendBuffer.length > 0) { - this.sendBuffer.forEach(function (callback) { - return callback(); - }); - this.sendBuffer = []; - } - } - }, { - key: "onConnMessage", - value: function onConnMessage(rawMessage) { - var msg = JSON.parse(rawMessage.data); - var topic = msg.topic; - var event = msg.event; - var payload = msg.payload; - var ref = msg.ref; - - this.log("receive", (payload.status || "") + " " + topic + " " + event + " " + (ref && "(" + ref + ")" || ""), payload); - this.channels.filter(function (channel) { - return channel.isMember(topic); - }).forEach(function (channel) { - return channel.trigger(event, payload, ref); - }); - this.stateChangeCallbacks.message.forEach(function (callback) { - return callback(msg); - }); - } - }]); - - return Socket; -}(); - -var LongPoll = exports.LongPoll = function () { - function LongPoll(endPoint) { - _classCallCheck(this, LongPoll); - - this.endPoint = null; - this.token = null; - this.skipHeartbeat = true; - this.onopen = function () {}; // noop - this.onerror = function () {}; // noop - this.onmessage = function () {}; // noop - this.onclose = function () {}; // noop - this.pollEndpoint = this.normalizeEndpoint(endPoint); - this.readyState = SOCKET_STATES.connecting; - - this.poll(); - } - - _createClass(LongPoll, [{ - key: "normalizeEndpoint", - value: function normalizeEndpoint(endPoint) { - return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)\/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll); - } - }, { - key: "endpointURL", - value: function endpointURL() { - return Ajax.appendParams(this.pollEndpoint, { token: this.token }); - } - }, { - key: "closeAndRetry", - value: function closeAndRetry() { - this.close(); - this.readyState = SOCKET_STATES.connecting; - } - }, { - key: "ontimeout", - value: function ontimeout() { - this.onerror("timeout"); - this.closeAndRetry(); - } - }, { - key: "poll", - value: function poll() { - var _this8 = this; - - if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) { - return; - } - - Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) { - if (resp) { - var status = resp.status; - var token = resp.token; - var messages = resp.messages; - - _this8.token = token; - } else { - var status = 0; - } - - switch (status) { - case 200: - messages.forEach(function (msg) { - return _this8.onmessage({ data: JSON.stringify(msg) }); - }); - _this8.poll(); - break; - case 204: - _this8.poll(); - break; - case 410: - _this8.readyState = SOCKET_STATES.open; - _this8.onopen(); - _this8.poll(); - break; - case 0: - case 500: - _this8.onerror(); - _this8.closeAndRetry(); - break; - default: - throw "unhandled poll status " + status; - } - }); - } - }, { - key: "send", - value: function send(body) { - var _this9 = this; - - Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) { - if (!resp || resp.status !== 200) { - _this9.onerror(status); - _this9.closeAndRetry(); - } - }); - } - }, { - key: "close", - value: function close(code, reason) { - this.readyState = SOCKET_STATES.closed; - this.onclose(); - } - }]); - - return LongPoll; -}(); - -var Ajax = exports.Ajax = function () { - function Ajax() { - _classCallCheck(this, Ajax); - } - - _createClass(Ajax, null, [{ - key: "request", - value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) { - if (window.XDomainRequest) { - var req = new XDomainRequest(); // IE8, IE9 - this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback); - } else { - var req = window.XMLHttpRequest ? new XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari - new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5 - this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback); - } - } - }, { - key: "xdomainRequest", - value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) { - var _this10 = this; - - req.timeout = timeout; - req.open(method, endPoint); - req.onload = function () { - var response = _this10.parseJSON(req.responseText); - callback && callback(response); - }; - if (ontimeout) { - req.ontimeout = ontimeout; - } - - // Work around bug in IE9 that requires an attached onprogress handler - req.onprogress = function () {}; - - req.send(body); - } - }, { - key: "xhrRequest", - value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) { - var _this11 = this; - - req.timeout = timeout; - req.open(method, endPoint, true); - req.setRequestHeader("Content-Type", accept); - req.onerror = function () { - callback && callback(null); - }; - req.onreadystatechange = function () { - if (req.readyState === _this11.states.complete && callback) { - var response = _this11.parseJSON(req.responseText); - callback(response); - } - }; - if (ontimeout) { - req.ontimeout = ontimeout; - } - - req.send(body); - } - }, { - key: "parseJSON", - value: function parseJSON(resp) { - return resp && resp !== "" ? JSON.parse(resp) : null; - } - }, { - key: "serialize", - value: function serialize(obj, parentKey) { - var queryStr = []; - for (var key in obj) { - if (!obj.hasOwnProperty(key)) { - continue; - } - var paramKey = parentKey ? parentKey + "[" + key + "]" : key; - var paramVal = obj[key]; - if ((typeof paramVal === "undefined" ? "undefined" : _typeof(paramVal)) === "object") { - queryStr.push(this.serialize(paramVal, paramKey)); - } else { - queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal)); - } - } - return queryStr.join("&"); - } - }, { - key: "appendParams", - value: function appendParams(url, params) { - if (Object.keys(params).length === 0) { - return url; - } - - var prefix = url.match(/\?/) ? "&" : "?"; - return "" + url + prefix + this.serialize(params); - } - }]); - - return Ajax; -}(); - -Ajax.states = { complete: 4 }; - -var Presence = exports.Presence = { - syncState: function syncState(currentState, newState, onJoin, onLeave) { - var _this12 = this; - - var state = this.clone(currentState); - var joins = {}; - var leaves = {}; - - this.map(state, function (key, presence) { - if (!newState[key]) { - leaves[key] = presence; - } - }); - this.map(newState, function (key, newPresence) { - var currentPresence = state[key]; - if (currentPresence) { - (function () { - var newRefs = newPresence.metas.map(function (m) { - return m.phx_ref; - }); - var curRefs = currentPresence.metas.map(function (m) { - return m.phx_ref; - }); - var joinedMetas = newPresence.metas.filter(function (m) { - return curRefs.indexOf(m.phx_ref) < 0; - }); - var leftMetas = currentPresence.metas.filter(function (m) { - return newRefs.indexOf(m.phx_ref) < 0; - }); - if (joinedMetas.length > 0) { - joins[key] = newPresence; - joins[key].metas = joinedMetas; - } - if (leftMetas.length > 0) { - leaves[key] = _this12.clone(currentPresence); - leaves[key].metas = leftMetas; - } - })(); - } else { - joins[key] = newPresence; - } - }); - return this.syncDiff(state, { joins: joins, leaves: leaves }, onJoin, onLeave); - }, - syncDiff: function syncDiff(currentState, _ref2, onJoin, onLeave) { - var joins = _ref2.joins; - var leaves = _ref2.leaves; - - var state = this.clone(currentState); - if (!onJoin) { - onJoin = function onJoin() {}; - } - if (!onLeave) { - onLeave = function onLeave() {}; - } - - this.map(joins, function (key, newPresence) { - var currentPresence = state[key]; - state[key] = newPresence; - if (currentPresence) { - var _state$key$metas; - - (_state$key$metas = state[key].metas).unshift.apply(_state$key$metas, _toConsumableArray(currentPresence.metas)); - } - onJoin(key, currentPresence, newPresence); - }); - this.map(leaves, function (key, leftPresence) { - var currentPresence = state[key]; - if (!currentPresence) { - return; - } - var refsToRemove = leftPresence.metas.map(function (m) { - return m.phx_ref; - }); - currentPresence.metas = currentPresence.metas.filter(function (p) { - return refsToRemove.indexOf(p.phx_ref) < 0; - }); - onLeave(key, currentPresence, leftPresence); - if (currentPresence.metas.length === 0) { - delete state[key]; - } - }); - return state; - }, - list: function list(presences, chooser) { - if (!chooser) { - chooser = function chooser(key, pres) { - return pres; - }; - } - - return this.map(presences, function (key, presence) { - return chooser(key, presence); - }); - }, - - // private - - map: function map(obj, func) { - return Object.getOwnPropertyNames(obj).map(function (key) { - return func(key, obj[key]); - }); - }, - clone: function clone(obj) { - return JSON.parse(JSON.stringify(obj)); - } -}; - -// Creates a timer that accepts a `timerCalc` function to perform -// calculated timeout retries, such as exponential backoff. -// -// ## Examples -// -// let reconnectTimer = new Timer(() => this.connect(), function(tries){ -// return [1000, 5000, 10000][tries - 1] || 10000 -// }) -// reconnectTimer.scheduleTimeout() // fires after 1000 -// reconnectTimer.scheduleTimeout() // fires after 5000 -// reconnectTimer.reset() -// reconnectTimer.scheduleTimeout() // fires after 1000 -// - -var Timer = function () { - function Timer(callback, timerCalc) { - _classCallCheck(this, Timer); - - this.callback = callback; - this.timerCalc = timerCalc; - this.timer = null; - this.tries = 0; - } - - _createClass(Timer, [{ - key: "reset", - value: function reset() { - this.tries = 0; - clearTimeout(this.timer); - } - - // Cancels any previous scheduleTimeout and schedules callback - - }, { - key: "scheduleTimeout", - value: function scheduleTimeout() { - var _this13 = this; - - clearTimeout(this.timer); - - this.timer = setTimeout(function () { - _this13.tries = _this13.tries + 1; - _this13.callback(); - }, this.timerCalc(this.tries + 1)); - } - }]); - - return Timer; -}(); - -})(typeof(exports) === "undefined" ? window.Phoenix = window.Phoenix || {} : exports); - diff --git a/priv/static/robots.txt b/priv/static/robots.txt deleted file mode 100644 index 3c9c7c0..0000000 --- a/priv/static/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-agent: * -# Disallow: / diff --git a/rel/env.bat.eex b/rel/env.bat.eex new file mode 100644 index 0000000..0d82afd --- /dev/null +++ b/rel/env.bat.eex @@ -0,0 +1,8 @@ +@echo off +rem Set the release to load code on demand (interactive) instead of preloading (embedded). +rem set RELEASE_MODE=interactive + +rem Set the release to work across nodes. +rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". +rem set RELEASE_DISTRIBUTION=name +rem set RELEASE_NODE=<%= @release.name %> diff --git a/rel/env.sh.eex b/rel/env.sh.eex new file mode 100644 index 0000000..ab7b76a --- /dev/null +++ b/rel/env.sh.eex @@ -0,0 +1,20 @@ +#!/bin/sh + +# # Sets and enables heart (recommended only in daemon mode) +# case $RELEASE_COMMAND in +# daemon*) +# HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" +# export HEART_COMMAND +# export ELIXIR_ERL_OPTIONS="-heart" +# ;; +# *) +# ;; +# esac + +# # Set the release to load code on demand (interactive) instead of preloading (embedded). +# export RELEASE_MODE=interactive + +# # Set the release to work across nodes. +# # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". +# export RELEASE_DISTRIBUTION=name +# export RELEASE_NODE=<%= @release.name %> diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server new file mode 100755 index 0000000..c52ba30 --- /dev/null +++ b/rel/overlays/bin/server @@ -0,0 +1,3 @@ +#!/bin/sh +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./docset_api start diff --git a/rel/overlays/bin/server.bat b/rel/overlays/bin/server.bat new file mode 100755 index 0000000..529ea27 --- /dev/null +++ b/rel/overlays/bin/server.bat @@ -0,0 +1,2 @@ +set PHX_SERVER=true +call "%~dp0\docset_api" start diff --git a/rel/remote.vm.args.eex b/rel/remote.vm.args.eex new file mode 100644 index 0000000..983397a --- /dev/null +++ b/rel/remote.vm.args.eex @@ -0,0 +1,8 @@ +## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html +## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here + +## Increase number of concurrent ports/sockets +##+Q 65536 + +## Tweak GC to run more often +##-env ERL_FULLSWEEP_AFTER 10 diff --git a/rel/vm.args.eex b/rel/vm.args.eex new file mode 100644 index 0000000..983397a --- /dev/null +++ b/rel/vm.args.eex @@ -0,0 +1,8 @@ +## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html +## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here + +## Increase number of concurrent ports/sockets +##+Q 65536 + +## Tweak GC to run more often +##-env ERL_FULLSWEEP_AFTER 10 diff --git a/test/controllers/page_controller_test.exs b/test/controllers/page_controller_test.exs index ac48bfd..225ade3 100644 --- a/test/controllers/page_controller_test.exs +++ b/test/controllers/page_controller_test.exs @@ -1,8 +1,21 @@ -defmodule DocsetApi.PageControllerTest do +defmodule DocsetApi.FeedControllerTest do use DocsetApi.ConnCase + import ExUnit.CaptureLog - test "GET /", %{conn: conn} do - conn = get conn, "/" - assert html_response(conn, 200) =~ "Welcome to Phoenix!" + test "GET /feeds/ecto", %{conn: conn} do + # TODO: Mock Builder so that we don't actually download any + # hexdocs. + conn = get(conn, "/feeds/ecto") + assert response(conn, 200) =~ "/docsets/ecto-" + end + + test "GET /feeds/sdfjnfdkjn", %{conn: conn} do + # TODO: Mock Builder so that we don't actually download any + # hexdocs. + capture_log(fn -> + resp = get(conn, "/feeds/sdfjnfdkjn") + assert resp.status == 404 + assert resp.resp_body =~ "Docset not found" + end) end end diff --git a/test/docset_api/file_parser_test.exs b/test/docset_api/file_parser_test.exs new file mode 100644 index 0000000..7dcdeb7 --- /dev/null +++ b/test/docset_api/file_parser_test.exs @@ -0,0 +1,435 @@ +defmodule DocsetApi.FileParserTest do + @moduledoc """ + The following code dynamically builds unit tests for all of the + example Fixtures in the fixtures directory. The filenames of each + fixture are parsed and then they are sorted by which version of the + documentation library (usually `ExDoc`) is being used. This should + hopefully aid in finding regression patterns. + + Although macros probably could've been used here, I chose to instead + use `ExUnit.Case.register_module_attribute/3` to dynamically build + out a test suite based on a list of fixture filenames. + """ + + alias DocsetApi.FileParser + use ExUnit.Case + import ExUnit.CaptureLog + + # Used to store all the fixture filenames. At runtime this attribute + # is processed and overwritten as a new data structure for use with the tests. + ExUnit.Case.register_module_attribute(__MODULE__, :fixtures, accumulate: false) + + # Used to temporarily store fixture state while it's looped through + ExUnit.Case.register_module_attribute(__MODULE__, :fixture, accumulate: false) + + # This is where the magic happens: we define a map with the keys + # representing fixture files, and the values representing the + # various types of expectations we have on those files. + @specs %{ + # This is a behaviour, not an interface, but it's the closest + # that the docset spec provides. + # + # TODO: See about proposing behaviours to be added to the spec. + "exdoc-0.34.2-ecto-3.12.4--Ecto.Adapter.html" => %{ + callbacks: [ + {"Ecto.Adapter", "Interface", "Ecto.Adapter.html#content"}, + {"Ecto.Adapter.lookup_meta/1", "Function", "Ecto.Adapter.html#lookup_meta/1"}, + {"Ecto.Adapter.adapter_meta/0", "Type", "Ecto.Adapter.html#t:adapter_meta/0"}, + {"Ecto.Adapter.t/0", "Type", "Ecto.Adapter.html#t:t/0"}, + {"Ecto.Adapter.__before_compile__/1", "Callback", "Ecto.Adapter.html#c:__before_compile__/1"}, + {"Ecto.Adapter.checked_out?/1", "Callback", "Ecto.Adapter.html#c:checked_out?/1"}, + {"Ecto.Adapter.checkout/3", "Callback", "Ecto.Adapter.html#c:checkout/3"}, + {"Ecto.Adapter.dumpers/2", "Callback", "Ecto.Adapter.html#c:dumpers/2"}, + {"Ecto.Adapter.ensure_all_started/2", "Callback", "Ecto.Adapter.html#c:ensure_all_started/2"}, + {"Ecto.Adapter.init/1", "Callback", "Ecto.Adapter.html#c:init/1"}, + {"Ecto.Adapter.loaders/2", "Callback", "Ecto.Adapter.html#c:loaders/2"} + ] + }, + "exdoc-0.34.2-ecto-3.12.4--Ecto.Query.API.html" => %{ + callbacks: [ + {"Ecto.Query.API", "Module", "Ecto.Query.API.html#content"}, + {"Ecto.Query.API.avg/1", "Function", "Ecto.Query.API.html#avg/1"}, + {"Ecto.Query.API.*/2", "Function", "Ecto.Query.API.html#*/2"}, + {"Ecto.Query.API.+/2", "Function", "Ecto.Query.API.html#+/2"}, + {"Ecto.Query.API.-/2", "Function", "Ecto.Query.API.html#-/2"}, + {"Ecto.Query.API.//2", "Function", "Ecto.Query.API.html#//2"}, + {"Ecto.Query.API.!=/2", "Function", "Ecto.Query.API.html#!=/2"}, + {"Ecto.Query.API.%3C/2", "Function", "Ecto.Query.API.html#%3C/2"}, + {"Ecto.Query.API.%3C=/2", "Function", "Ecto.Query.API.html#%3C=/2"}, + {"Ecto.Query.API.==/2", "Function", "Ecto.Query.API.html#==/2"}, + {"Ecto.Query.API.%3E/2", "Function", "Ecto.Query.API.html#%3E/2"}, + {"Ecto.Query.API.%3E=/2", "Function", "Ecto.Query.API.html#%3E=/2"}, + {"Ecto.Query.API.ago/2", "Function", "Ecto.Query.API.html#ago/2"}, + {"Ecto.Query.API.all/1", "Function", "Ecto.Query.API.html#all/1"}, + {"Ecto.Query.API.and/2", "Function", "Ecto.Query.API.html#and/2"}, + {"Ecto.Query.API.any/1", "Function", "Ecto.Query.API.html#any/1"}, + {"Ecto.Query.API.as/1", "Function", "Ecto.Query.API.html#as/1"}, + {"Ecto.Query.API.coalesce/2", "Function", "Ecto.Query.API.html#coalesce/2"}, + {"Ecto.Query.API.count/0", "Function", "Ecto.Query.API.html#count/0"}, + {"Ecto.Query.API.count/1", "Function", "Ecto.Query.API.html#count/1"}, + {"Ecto.Query.API.count/2", "Function", "Ecto.Query.API.html#count/2"}, + {"Ecto.Query.API.date_add/3", "Function", "Ecto.Query.API.html#date_add/3"}, + {"Ecto.Query.API.datetime_add/3", "Function", "Ecto.Query.API.html#datetime_add/3"}, + {"Ecto.Query.API.exists/1", "Function", "Ecto.Query.API.html#exists/1"}, + {"Ecto.Query.API.field/2", "Function", "Ecto.Query.API.html#field/2"}, + {"Ecto.Query.API.filter/2", "Function", "Ecto.Query.API.html#filter/2"}, + {"Ecto.Query.API.fragment/1", "Function", "Ecto.Query.API.html#fragment/1"}, + {"Ecto.Query.API.from_now/2", "Function", "Ecto.Query.API.html#from_now/2"}, + {"Ecto.Query.API.ilike/2", "Function", "Ecto.Query.API.html#ilike/2"}, + {"Ecto.Query.API.in/2", "Function", "Ecto.Query.API.html#in/2"}, + {"Ecto.Query.API.is_nil/1", "Function", "Ecto.Query.API.html#is_nil/1"}, + {"Ecto.Query.API.json_extract_path/2", "Function", + "Ecto.Query.API.html#json_extract_path/2"}, + {"Ecto.Query.API.like/2", "Function", "Ecto.Query.API.html#like/2"}, + {"Ecto.Query.API.literal/1", "Function", "Ecto.Query.API.html#literal/1"}, + {"Ecto.Query.API.map/2", "Function", "Ecto.Query.API.html#map/2"}, + {"Ecto.Query.API.max/1", "Function", "Ecto.Query.API.html#max/1"}, + {"Ecto.Query.API.merge/2", "Function", "Ecto.Query.API.html#merge/2"}, + {"Ecto.Query.API.min/1", "Function", "Ecto.Query.API.html#min/1"}, + {"Ecto.Query.API.not/1", "Function", "Ecto.Query.API.html#not/1"}, + {"Ecto.Query.API.or/2", "Function", "Ecto.Query.API.html#or/2"}, + {"Ecto.Query.API.parent_as/1", "Function", "Ecto.Query.API.html#parent_as/1"}, + {"Ecto.Query.API.selected_as/1", "Function", "Ecto.Query.API.html#selected_as/1"}, + {"Ecto.Query.API.selected_as/2", "Function", "Ecto.Query.API.html#selected_as/2"}, + {"Ecto.Query.API.splice/1", "Function", "Ecto.Query.API.html#splice/1"}, + {"Ecto.Query.API.struct/2", "Function", "Ecto.Query.API.html#struct/2"}, + {"Ecto.Query.API.sum/1", "Function", "Ecto.Query.API.html#sum/1"}, + {"Ecto.Query.API.type/2", "Function", "Ecto.Query.API.html#type/2"}, + {"Ecto.Query.API.values/2", "Function", "Ecto.Query.API.html#values/2"} + ] + }, + "exdoc-0.34.2-ecto-3.12.4--getting-started.html" => %{ + callbacks: [{"Getting Started", "Guide", "getting-started.html#content"}] + }, + "exdoc-0.34.2-ecto-3.12.4--Ecto.QueryError.html" => %{ + callbacks: [{"Ecto.QueryError", "Exception", "Ecto.QueryError.html#content"}] + }, + # This is not a guide, but a "cheatsheet", which we put in the guide category. + "exdoc-0.34.2-ecto-3.12.4--crud.html" => %{ + callbacks: [{"Basic CRUD", "Guide", "crud.html#content"}] + }, + "exdoc-0.34.0-phoenix-1.7.14--Phoenix.Param.html" => %{ + callbacks: [ + {"Phoenix.Param", "Protocol", "Phoenix.Param.html#content"}, + {"Phoenix.Param.to_param/1", "Function", "Phoenix.Param.html#to_param/1"}, + {"Phoenix.Param.t/0", "Type", "Phoenix.Param.html#t:t/0"} + ] + }, + "exdoc-0.34.0-phoenix-1.7.14--Phoenix.Socket.Transport.html" => %{ + callbacks: [ + {"Phoenix.Socket.Transport", "Interface", "Phoenix.Socket.Transport.html#content"}, + {"Phoenix.Socket.Transport.check_origin/5", "Function", + "Phoenix.Socket.Transport.html#check_origin/5"}, + {"Phoenix.Socket.Transport.check_subprotocols/2", "Function", + "Phoenix.Socket.Transport.html#check_subprotocols/2"}, + {"Phoenix.Socket.Transport.code_reload/3", "Function", + "Phoenix.Socket.Transport.html#code_reload/3"}, + {"Phoenix.Socket.Transport.connect_info/3", "Function", + "Phoenix.Socket.Transport.html#connect_info/3"}, + {"Phoenix.Socket.Transport.transport_log/2", "Function", + "Phoenix.Socket.Transport.html#transport_log/2"}, + {"Phoenix.Socket.Transport.state/0", "Type", "Phoenix.Socket.Transport.html#t:state/0"}, + {"Phoenix.Socket.Transport.child_spec/1", "Callback", "Phoenix.Socket.Transport.html#c:child_spec/1"}, + {"Phoenix.Socket.Transport.connect/1", "Callback", "Phoenix.Socket.Transport.html#c:connect/1"}, + {"Phoenix.Socket.Transport.drainer_spec/1", "Callback", "Phoenix.Socket.Transport.html#c:drainer_spec/1"}, + {"Phoenix.Socket.Transport.handle_control/2", "Callback", "Phoenix.Socket.Transport.html#c:handle_control/2"}, + {"Phoenix.Socket.Transport.handle_in/2", "Callback", "Phoenix.Socket.Transport.html#c:handle_in/2"}, + {"Phoenix.Socket.Transport.handle_info/2", "Callback", "Phoenix.Socket.Transport.html#c:handle_info/2"}, + {"Phoenix.Socket.Transport.init/1", "Callback", "Phoenix.Socket.Transport.html#c:init/1"}, + {"Phoenix.Socket.Transport.terminate/2", "Callback", "Phoenix.Socket.Transport.html#c:terminate/2"} + ] + }, + "exdoc-0.28.6-jason-1.5.0.alpha2--Jason.EncodeError.html" => %{ + callbacks: [ + {"Jason.EncodeError", "Exception", "Jason.EncodeError.html#content"}, + {"Jason.EncodeError.new/1", "Function", "Jason.EncodeError.html#new/1"}, + {"Jason.EncodeError.t/0", "Type", "Jason.EncodeError.html#t:t/0"} + ] + }, + "exdoc-0.28.6-jason-1.5.0.alpha2--Jason.OrderedObject.html" => %{ + callbacks: [ + {"Jason.OrderedObject", "Module", "Jason.OrderedObject.html#content"}, + {"Jason.OrderedObject.__struct__/0", "Function", "Jason.OrderedObject.html#__struct__/0"}, + {"Jason.OrderedObject.new/1", "Function", "Jason.OrderedObject.html#new/1"}, + {"Jason.OrderedObject.t/0", "Type", "Jason.OrderedObject.html#t:t/0"} + ] + }, + "exdoc-0.28.6-jason-1.5.0.alpha2--Jason.Encoder.html" => %{ + callbacks: [ + {"Jason.Encoder", "Protocol", "Jason.Encoder.html#content"}, + {"Jason.Encoder.encode/2", "Function", "Jason.Encoder.html#encode/2"}, + {"Jason.Encoder.opts/0", "Type", "Jason.Encoder.html#t:opts/0"}, + {"Jason.Encoder.t/0", "Type", "Jason.Encoder.html#t:t/0"} + ] + }, + "exdoc-0.25.5-httpoison-2.2.1--HTTPoison.AsyncChunk.html" => %{ + callbacks: [ + {"HTTPoison.AsyncChunk", "Module", "HTTPoison.AsyncChunk.html#content"}, + {"HTTPoison.AsyncChunk.t/0", "Type", "HTTPoison.AsyncChunk.html#t:t/0"} + ] + }, + "exdoc-0.25.5-httpoison-2.2.1--HTTPoison.Base.html" => %{ + callbacks: [ + {"HTTPoison.Base", "Interface", "HTTPoison.Base.html#content"}, + {"HTTPoison.Base.maybe_process_form/1", "Function", + "HTTPoison.Base.html#maybe_process_form/1"}, + {"HTTPoison.Base.body/0", "Type", "HTTPoison.Base.html#t:body/0"}, + {"HTTPoison.Base.headers/0", "Type", "HTTPoison.Base.html#t:headers/0"}, + {"HTTPoison.Base.method/0", "Type", "HTTPoison.Base.html#t:method/0"}, + {"HTTPoison.Base.options/0", "Type", "HTTPoison.Base.html#t:options/0"}, + {"HTTPoison.Base.params/0", "Type", "HTTPoison.Base.html#t:params/0"}, + {"HTTPoison.Base.request/0", "Type", "HTTPoison.Base.html#t:request/0"}, + {"HTTPoison.Base.response/0", "Type", "HTTPoison.Base.html#t:response/0"}, + {"HTTPoison.Base.url/0", "Type", "HTTPoison.Base.html#t:url/0"}, + {"HTTPoison.Base.delete/1", "Callback", "HTTPoison.Base.html#c:delete/1"}, + {"HTTPoison.Base.delete/2", "Callback", "HTTPoison.Base.html#c:delete/2"}, + {"HTTPoison.Base.delete/3", "Callback", "HTTPoison.Base.html#c:delete/3"}, + {"HTTPoison.Base.delete!/1", "Callback", "HTTPoison.Base.html#c:delete!/1"}, + {"HTTPoison.Base.delete!/2", "Callback", "HTTPoison.Base.html#c:delete!/2"}, + {"HTTPoison.Base.delete!/3", "Callback", "HTTPoison.Base.html#c:delete!/3"}, + {"HTTPoison.Base.get/1", "Callback", "HTTPoison.Base.html#c:get/1"}, + {"HTTPoison.Base.get/2", "Callback", "HTTPoison.Base.html#c:get/2"}, + {"HTTPoison.Base.get/3", "Callback", "HTTPoison.Base.html#c:get/3"}, + {"HTTPoison.Base.get!/1", "Callback", "HTTPoison.Base.html#c:get!/1"}, + {"HTTPoison.Base.get!/2", "Callback", "HTTPoison.Base.html#c:get!/2"}, + {"HTTPoison.Base.get!/3", "Callback", "HTTPoison.Base.html#c:get!/3"}, + {"HTTPoison.Base.head/1", "Callback", "HTTPoison.Base.html#c:head/1"}, + {"HTTPoison.Base.head/2", "Callback", "HTTPoison.Base.html#c:head/2"}, + {"HTTPoison.Base.head/3", "Callback", "HTTPoison.Base.html#c:head/3"}, + {"HTTPoison.Base.head!/1", "Callback", "HTTPoison.Base.html#c:head!/1"}, + {"HTTPoison.Base.head!/2", "Callback", "HTTPoison.Base.html#c:head!/2"}, + {"HTTPoison.Base.head!/3", "Callback", "HTTPoison.Base.html#c:head!/3"}, + {"HTTPoison.Base.options/1", "Callback", "HTTPoison.Base.html#c:options/1"}, + {"HTTPoison.Base.options/2", "Callback", "HTTPoison.Base.html#c:options/2"}, + {"HTTPoison.Base.options/3", "Callback", "HTTPoison.Base.html#c:options/3"}, + {"HTTPoison.Base.options!/1", "Callback", "HTTPoison.Base.html#c:options!/1"}, + {"HTTPoison.Base.options!/2", "Callback", "HTTPoison.Base.html#c:options!/2"}, + {"HTTPoison.Base.options!/3", "Callback", "HTTPoison.Base.html#c:options!/3"}, + {"HTTPoison.Base.patch/2", "Callback", "HTTPoison.Base.html#c:patch/2"}, + {"HTTPoison.Base.patch/3", "Callback", "HTTPoison.Base.html#c:patch/3"}, + {"HTTPoison.Base.patch/4", "Callback", "HTTPoison.Base.html#c:patch/4"}, + {"HTTPoison.Base.patch!/2", "Callback", "HTTPoison.Base.html#c:patch!/2"}, + {"HTTPoison.Base.patch!/3", "Callback", "HTTPoison.Base.html#c:patch!/3"}, + {"HTTPoison.Base.patch!/4", "Callback", "HTTPoison.Base.html#c:patch!/4"}, + {"HTTPoison.Base.post/2", "Callback", "HTTPoison.Base.html#c:post/2"}, + {"HTTPoison.Base.post/3", "Callback", "HTTPoison.Base.html#c:post/3"}, + {"HTTPoison.Base.post/4", "Callback", "HTTPoison.Base.html#c:post/4"}, + {"HTTPoison.Base.post!/2", "Callback", "HTTPoison.Base.html#c:post!/2"}, + {"HTTPoison.Base.post!/3", "Callback", "HTTPoison.Base.html#c:post!/3"}, + {"HTTPoison.Base.post!/4", "Callback", "HTTPoison.Base.html#c:post!/4"}, + {"HTTPoison.Base.process_headers/1", "Callback", "HTTPoison.Base.html#c:process_headers/1"}, + {"HTTPoison.Base.process_request_body/1", "Callback", "HTTPoison.Base.html#c:process_request_body/1"}, + {"HTTPoison.Base.process_request_headers/1", "Callback", "HTTPoison.Base.html#c:process_request_headers/1"}, + {"HTTPoison.Base.process_request_options/1", "Callback", "HTTPoison.Base.html#c:process_request_options/1"}, + {"HTTPoison.Base.process_request_params/1", "Callback", "HTTPoison.Base.html#c:process_request_params/1"}, + {"HTTPoison.Base.process_request_url/1", "Callback", "HTTPoison.Base.html#c:process_request_url/1"}, + {"HTTPoison.Base.process_response/1", "Callback", "HTTPoison.Base.html#c:process_response/1"}, + {"HTTPoison.Base.process_response_body/1", "Callback", "HTTPoison.Base.html#c:process_response_body/1"}, + {"HTTPoison.Base.process_response_chunk/1", "Callback", "HTTPoison.Base.html#c:process_response_chunk/1"}, + {"HTTPoison.Base.process_response_headers/1", "Callback", "HTTPoison.Base.html#c:process_response_headers/1"}, + {"HTTPoison.Base.process_response_status_code/1", "Callback", "HTTPoison.Base.html#c:process_response_status_code/1"}, + {"HTTPoison.Base.process_status_code/1", "Callback", "HTTPoison.Base.html#c:process_status_code/1"}, + {"HTTPoison.Base.process_url/1", "Callback", "HTTPoison.Base.html#c:process_url/1"}, + {"HTTPoison.Base.put/1", "Callback", "HTTPoison.Base.html#c:put/1"}, + {"HTTPoison.Base.put/2", "Callback", "HTTPoison.Base.html#c:put/2"}, + {"HTTPoison.Base.put/3", "Callback", "HTTPoison.Base.html#c:put/3"}, + {"HTTPoison.Base.put/4", "Callback", "HTTPoison.Base.html#c:put/4"}, + {"HTTPoison.Base.put!/1", "Callback", "HTTPoison.Base.html#c:put!/1"}, + {"HTTPoison.Base.put!/2", "Callback", "HTTPoison.Base.html#c:put!/2"}, + {"HTTPoison.Base.put!/3", "Callback", "HTTPoison.Base.html#c:put!/3"}, + {"HTTPoison.Base.put!/4", "Callback", "HTTPoison.Base.html#c:put!/4"}, + {"HTTPoison.Base.request/1", "Callback", "HTTPoison.Base.html#c:request/1"}, + {"HTTPoison.Base.request/2", "Callback", "HTTPoison.Base.html#c:request/2"}, + {"HTTPoison.Base.request/3", "Callback", "HTTPoison.Base.html#c:request/3"}, + {"HTTPoison.Base.request/4", "Callback", "HTTPoison.Base.html#c:request/4"}, + {"HTTPoison.Base.request/5", "Callback", "HTTPoison.Base.html#c:request/5"}, + {"HTTPoison.Base.request!/2", "Callback", "HTTPoison.Base.html#c:request!/2"}, + {"HTTPoison.Base.request!/3", "Callback", "HTTPoison.Base.html#c:request!/3"}, + {"HTTPoison.Base.request!/4", "Callback", "HTTPoison.Base.html#c:request!/4"}, + {"HTTPoison.Base.request!/5", "Callback", "HTTPoison.Base.html#c:request!/5"}, + {"HTTPoison.Base.start/0", "Callback", "HTTPoison.Base.html#c:start/0"}, + {"HTTPoison.Base.stream_next/1", "Callback", "HTTPoison.Base.html#c:stream_next/1"} + + ] + }, + "exdoc-0.25.5-httpoison-2.2.1--HTTPoison.Error.html" => %{ + callbacks: [ + {"HTTPoison.Error", "Exception", "HTTPoison.Error.html#content"}, + {"HTTPoison.Error.message/1", "Function", "HTTPoison.Error.html#message/1"}, + {"HTTPoison.Error.t/0", "Type", "HTTPoison.Error.html#t:t/0"} + ] + }, + "exdoc-0.25.5-httpoison-2.2.1--changelog.html" => %{ + callbacks: [ + {"Changelog", "Guide", "changelog.html#content"} + ] + }, + "exdoc-0.25.5-httpoison-2.2.1--404.html" => %{ + # We expect 404 to be skipped because it's a "banned file" + callbacks: [] + }, + "exdoc-0.11.3-guardsafe-0.5.1--Guardsafe.html" => %{ + callbacks: [ + {"Guardsafe", "Module", "Guardsafe.html#content"}, + {"Guardsafe.atom?/1", "Macro", "Guardsafe.html#atom?/1"}, + {"Guardsafe.binary?/1", "Macro", "Guardsafe.html#binary?/1"}, + {"Guardsafe.bitstring?/1", "Macro", "Guardsafe.html#bitstring?/1"}, + {"Guardsafe.boolean?/1", "Macro", "Guardsafe.html#boolean?/1"}, + {"Guardsafe.date?/1", "Macro", "Guardsafe.html#date?/1"}, + {"Guardsafe.datetime?/1", "Macro", "Guardsafe.html#datetime?/1"}, + {"Guardsafe.divisible_by?/2", "Macro", "Guardsafe.html#divisible_by?/2"}, + {"Guardsafe.even?/1", "Macro", "Guardsafe.html#even?/1"}, + {"Guardsafe.float?/1", "Macro", "Guardsafe.html#float?/1"}, + {"Guardsafe.function?/1", "Macro", "Guardsafe.html#function?/1"}, + {"Guardsafe.function?/2", "Macro", "Guardsafe.html#function?/2"}, + {"Guardsafe.integer?/1", "Macro", "Guardsafe.html#integer?/1"}, + {"Guardsafe.list?/1", "Macro", "Guardsafe.html#list?/1"}, + {"Guardsafe.map?/1", "Macro", "Guardsafe.html#map?/1"}, + {"Guardsafe.nil?/1", "Macro", "Guardsafe.html#nil?/1"}, + {"Guardsafe.number?/1", "Macro", "Guardsafe.html#number?/1"}, + {"Guardsafe.odd?/1", "Macro", "Guardsafe.html#odd?/1"}, + {"Guardsafe.pid?/1", "Macro", "Guardsafe.html#pid?/1"}, + {"Guardsafe.port?/1", "Macro", "Guardsafe.html#port?/1"}, + {"Guardsafe.reference?/1", "Macro", "Guardsafe.html#reference?/1"}, + {"Guardsafe.time?/1", "Macro", "Guardsafe.html#time?/1"}, + {"Guardsafe.tuple?/1", "Macro", "Guardsafe.html#tuple?/1"}, + {"Guardsafe.within?/3", "Macro", "Guardsafe.html#within?/3"} + ] + } + } + + # Split between fixtures specs & html filename + @fixtures Map.keys(@specs) + |> Enum.map(&{&1, String.split(&1, "--")}) + |> Enum.map(fn + {fixture_filename, [fixture, filename]} -> + [doc, doc_version, pkg, pkg_version] = String.split(fixture, "-") + + # Build a data structure for the metadata associated with a fixture. + %{ + documenting_tool: doc, + documenting_tool_version: doc_version, + pkg: {pkg, pkg_version}, + document_filename: filename, + fixture_filename: fixture_filename + } + + unparseable -> + raise("Cannot parse #{inspect(unparseable)}. Please check its format.") + end) + # Group the results by documentation tool name & + # version, so that they can be grouped into an exunit + # describe block together. + |> Enum.group_by( + &"#{&1.documenting_tool}-#{&1.documenting_tool_version}", + &%{ + fixture_filename: &1.fixture_filename, + document_filename: &1.document_filename, + pkg: &1.pkg, + doc: { + # This will always be the first instance of this atom + String.to_atom(&1.documenting_tool), + &1.documenting_tool_version + }, + specs: Map.fetch!(@specs, &1.fixture_filename) + } + ) + + for {doc, fixtures} <- @fixtures do + describe "fixture: [#{String.replace(doc, "-", " ")}]" do + for %{document_filename: filename, pkg: {pkg, pkg_version}} = fixture <- fixtures do + # While filename is in scope when the first argument of the + # test call is being interpolated, this is not the case for + # inside the block. + # + # Instead, we set the fixture to the @fixture module attribute, + # which adds it to the test context. + @fixture fixture + + # For each ExUnit version, build a test to ensure that it can be + # identified correctly. + test "[#{pkg} #{pkg_version}] [ID] " <> filename, %{ + registered: %{fixture: %{doc: {doc_n, doc_v}, fixture_filename: filename}} + } do + file_path = Path.join([File.cwd!(), "test/support/fixtures", filename]) + + {:ok, html} = + file_path + |> File.read!() + |> Floki.parse_document() + + assert {doc_n, Version.parse!(doc_v)} == + FileParser.identify_documenting_tool_version(html, file_path) + end + + # For each ExUnit version, build a suite of tests to + # ensure indexing can be done correctly. + test "[#{pkg} #{pkg_version}] [INDEX] " <> filename, %{ + registered: %{ + fixture: %{specs: specs, fixture_filename: filename, document_filename: doc} + } + } do + Process.register(self(), :test) + + file_path = Path.join([File.cwd!(), "test/support/fixtures", filename]) + + {:ok, html} = + file_path + |> File.read!() + |> Floki.parse_document() + + logs = + capture_log(fn -> + FileParser.parse(html, doc, fn name, type, path -> + # dbg({name, type, path}) + # Tally the states by sending them to the `:test` process to + # be received and asserted against. + send(:test, {:called_back, name, type, path}) + end) + end) + + # Ensure we have categorised everything + if logs =~ "Could not categorise" do + raise """ + There are fixtures which aren't being categorised by the + test, and so haven't been implemented correctly. This is a + bug. + + #{logs} + """ + end + + for {name, type, path} <- specs[:callbacks] do + assert_receive {:called_back, ^name, ^type, ^path} + end + + # Check the :test mailbox and if there are any callbacks + # which weren't accounted for, raise an error, since we + # didn't expect to receive them and haven't implemented + # expectations for them. This way there's a nice workflow. + [message_queue_len: queue_len, messages: messages] = + Process.info( + Process.whereis(:test), + [:message_queue_len, :messages] + ) + + if queue_len > 0 do + raise """ + + There were #{queue_len} unexpected callback(s). Should + they be added to the expectations list? + + #{for msg <- messages, do: inspect(msg) <> "\n"} + + """ + end + + Process.unregister(:test) + end + end + end + end + + # Unset the fixture module attribute once we've finished misusing it. + @fixture nil +end diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex index 5a376f9..5df4cd5 100644 --- a/test/support/channel_case.ex +++ b/test/support/channel_case.ex @@ -19,23 +19,13 @@ defmodule DocsetApi.ChannelCase do quote do # Import conveniences for testing with channels use Phoenix.ChannelTest - - alias DocsetApi.Repo - import Ecto - import Ecto.Changeset - import Ecto.Query - - # The default endpoint for testing @endpoint DocsetApi.Endpoint end end setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(DocsetApi.Repo) - unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(DocsetApi.Repo, {:shared, self()}) end :ok diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 42b5a7c..81e384d 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -18,13 +18,8 @@ defmodule DocsetApi.ConnCase do using do quote do # Import conveniences for testing with connections - use Phoenix.ConnTest - - alias DocsetApi.Repo - import Ecto - import Ecto.Changeset - import Ecto.Query - + import Plug.Conn + import Phoenix.ConnTest import DocsetApi.Router.Helpers # The default endpoint for testing @@ -33,10 +28,7 @@ defmodule DocsetApi.ConnCase do end setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(DocsetApi.Repo) - unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(DocsetApi.Repo, {:shared, self()}) end {:ok, conn: Phoenix.ConnTest.build_conn()} diff --git a/test/support/fixtures/exdoc-0.11.3-guardsafe-0.5.1--Guardsafe.html b/test/support/fixtures/exdoc-0.11.3-guardsafe-0.5.1--Guardsafe.html new file mode 100644 index 0000000..fb42ba4 --- /dev/null +++ b/test/support/fixtures/exdoc-0.11.3-guardsafe-0.5.1--Guardsafe.html @@ -0,0 +1,64 @@ +Guardsafe – Guardsafe v0.5.1

Guardsafe v0.5.1 + Guardsafe + + +

Provides readability-enhancing macros that can safely be used in guard clauses.

Examples

defmodule MacrofyAllTheThings do
+  import Guardsafe
+  require Integer
+
+  def magic(number) when number |> nil? do
+    "Staring into the void..."
+  end
+
+  def magic(number) when number |> even? do
+    "That's not odd."
+  end
+
+  def magic(number) when number |> divisible_by?(5) do
+    "High five!"
+  end
+end
+
+iex> MacrofyAllTheThings.magic(8)
+"That's not odd."

+ Summary +

Macros

Expands atom?(term) into Kernel.is_atom(term)

Expands binary?(term) into Kernel.is_binary(term)

Expands bitstring?(term) into Kernel.is_bitstring(term)

Expands boolean?(term) into Kernel.is_boolean(term)

Returns true if the term is considered to be a date tuple

Returns true if the term is considered to be a time tuple

Returns true if the integer number is evenly divisible by the divisor

Returns true for even integers

Expands float?(term) into Kernel.is_float(term)

Expands function?(term) into Kernel.is_function(term)

Expands function?(term, arity) into is_function(term, arity)

Expands integer?(term) into Kernel.is_integer(term)

Expands list?(term) into Kernel.is_list(term)

Expands map?(term) into Kernel.is_map(term)

Expands nil?(term) into Kernel.is_nil(term)

Expands number?(term) into Kernel.is_number(term)

Returns true for odd integers

Expands pid?(term) into Kernel.is_pid(term)

Expands port?(term) into Kernel.is_port(term)

Expands reference?(term) into Kernel.is_reference(term)

Returns true if the term is considered to be a time tuple

Expands tuple?(term) into Kernel.is_tuple(term)

Returns true if the term is between the low and high values, inclusively

+ Macros +

atom?(term)

Expands atom?(term) into Kernel.is_atom(term).

Examples

iex> some_atom_variable |> atom?
+true
binary?(term)

Expands binary?(term) into Kernel.is_binary(term).

Examples

iex> some_binary_variable |> binary?
+true
bitstring?(term)

Expands bitstring?(term) into Kernel.is_bitstring(term).

Examples

iex> some_bitstring_variable |> bitstring?
+true
boolean?(term)

Expands boolean?(term) into Kernel.is_boolean(term).

Examples

iex> some_boolean_variable |> boolean?
+true
date?(term)

Returns true if the term is considered to be a date tuple.

The date is not checked for validity other than the months being in +the 1..12 range and the days being in the 1..31 range.

Examples

iex> 5 |> date?({2015, 3, 20})
+true
datetime?(term)

Returns true if the term is considered to be a time tuple.

date? and time? are used for checking and validating the +date and time portions of the datetime.

Examples

iex> datetime? {{2015, 3, 20}, {15, 33, 42}}
+true
divisible_by?(number, divisor)

Returns true if the integer number is evenly divisible by the divisor.

Examples

iex> 25 |> divisible_by?(5)
+true
even?(number)

Returns true for even integers.

Examples

iex> even? 7
+false
float?(term)

Expands float?(term) into Kernel.is_float(term).

Examples

iex> some_float_variable |> float?
+true
function?(term)

Expands function?(term) into Kernel.is_function(term).

Examples

iex> some_function_variable |> function?
+true
function?(term, arity)

Expands function?(term, arity) into is_function(term, arity)

Examples

iex> &String.to_integer/1 |> function?(2)
+false
integer?(term)

Expands integer?(term) into Kernel.is_integer(term).

Examples

iex> some_integer_variable |> integer?
+true
list?(term)

Expands list?(term) into Kernel.is_list(term).

Examples

iex> some_list_variable |> list?
+true
map?(term)

Expands map?(term) into Kernel.is_map(term).

Examples

iex> some_map_variable |> map?
+true
nil?(term)

Expands nil?(term) into Kernel.is_nil(term).

Examples

iex> some_nil_variable |> nil?
+true
number?(term)

Expands number?(term) into Kernel.is_number(term).

Examples

iex> some_number_variable |> number?
+true
odd?(number)

Returns true for odd integers.

Examples

iex> odd? 5
+true
pid?(term)

Expands pid?(term) into Kernel.is_pid(term).

Examples

iex> some_pid_variable |> pid?
+true
port?(term)

Expands port?(term) into Kernel.is_port(term).

Examples

iex> some_port_variable |> port?
+true
reference?(term)

Expands reference?(term) into Kernel.is_reference(term).

Examples

iex> some_reference_variable |> reference?
+true
time?(term)

Returns true if the term is considered to be a time tuple.

The time is not checked for validity other than the hours being in +the 0..23 range, the minutes being in the 0..59 range, and seconds +being in the 0..60 range. The latter due to the existance of leap seconds.

Examples

iex> 5 |> time?({15, 33, 42})
+true
tuple?(term)

Expands tuple?(term) into Kernel.is_tuple(term).

Examples

iex> some_tuple_variable |> tuple?
+true
within?(term, low, high)

Returns true if the term is between the low and high values, inclusively.

Examples

iex> 5 |> within?(2, 10)
+true
\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--404.html b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--404.html new file mode 100644 index 0000000..79d3bb5 --- /dev/null +++ b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--404.html @@ -0,0 +1,26 @@ +404 — HTTPoison v2.2.1

Page not found

Sorry, but the page you were trying to get to, does not exist. You +may want to try searching this site using the sidebar + + or using our API Reference page + +to find what you were looking for.

\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.AsyncChunk.html b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.AsyncChunk.html new file mode 100644 index 0000000..a38ce40 --- /dev/null +++ b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.AsyncChunk.html @@ -0,0 +1,26 @@ +HTTPoison.AsyncChunk — HTTPoison v2.2.1

+HTTPoison.AsyncChunk (HTTPoison v2.2.1)View Source

Link to this section + Summary +

Link to this section +Types +

Specs

t() :: %HTTPoison.AsyncChunk{chunk: binary(), id: reference()}
\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.Base.html b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.Base.html new file mode 100644 index 0000000..e21ebc3 --- /dev/null +++ b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.Base.html @@ -0,0 +1,219 @@ +HTTPoison.Base — HTTPoison v2.2.1

+HTTPoison.Base behaviour(HTTPoison v2.2.1)View Source

Provides a default implementation for HTTPoison functions.

This module is meant to be use'd in custom modules in order to wrap the +functionalities provided by HTTPoison. For example, this is very useful to +build API clients around HTTPoison:

defmoduleGitHubdouseHTTPoison.Base@endpoint"https://api.github.com"defprocess_request_url(url)do@endpoint<>urlendend

The example above shows how the GitHub module can wrap HTTPoison +functionalities to work with the GitHub API in particular; this way, for +example, all requests done through the GitHub module will be done to the +GitHub API:

GitHub.get("/users/octocat/orgs")#=> will issue a GET request at https://api.github.com/users/octocat/orgs

+ Overriding functions +

HTTPoison.Base defines the following list of functions, all of which can be +overridden (by redefining them). The following list also shows the typespecs +for these functions and a short description.

# Called in order to process the url passed to any request method before# actually issuing the request.@specprocess_request_url(binary)::binarydefprocess_request_url(url)# Called to arbitrarily process the request body before sending it with the# request.@specprocess_request_body(term)::binarydefprocess_request_body(body)# Called to arbitrarily process the request headers before sending them# with the request.@specprocess_request_headers(term)::[{binary,term}]defprocess_request_headers(headers)# Called to arbitrarily process the request options before sending them# with the request.@specprocess_request_options(keyword)::keyworddefprocess_request_options(options)# Called before returning the response body returned by a request to the# caller.@specprocess_response_body(binary)::termdefprocess_response_body(body)# Used when an async request is made; it's called on each chunk that gets# streamed before returning it to the streaming destination.@specprocess_response_chunk(binary)::termdefprocess_response_chunk(chunk)# Called to process the response headers before returning them to the# caller.@specprocess_headers([{binary,term}])::termdefprocess_headers(headers)# Used to arbitrarily process the status code of a response before# returning it to the caller.@specprocess_response_status_code(integer)::termdefprocess_response_status_code(status_code)

Link to this section + Summary +

Callbacks

Link to this section +Types +

Link to this section +Callbacks +

Specs

Specs

Link to this callback

delete(url, headers, options)

View Source

Specs

Specs

Specs

Link to this callback

delete!(url, headers, options)

View Source

Specs

Specs

get(url()) ::
+  {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()}
+  | {:error, HTTPoison.Error.t()}

Specs

get(url(), headers()) ::
+  {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()}
+  | {:error, HTTPoison.Error.t()}
Link to this callback

get(url, headers, options)

View Source

Specs

Specs

Specs

Link to this callback

get!(url, headers, options)

View Source

Specs

Specs

head(url()) ::
+  {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()}
+  | {:error, HTTPoison.Error.t()}

Specs

head(url(), headers()) ::
+  {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()}
+  | {:error, HTTPoison.Error.t()}
Link to this callback

head(url, headers, options)

View Source

Specs

head(url(), headers(), options()) ::
+  {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()}
+  | {:error, HTTPoison.Error.t()}

Specs

Specs

Link to this callback

head!(url, headers, options)

View Source

Specs

Specs

Specs

Link to this callback

options(url, headers, options)

View Source

Specs

Specs

Link to this callback

options!(url, headers)

View Source

Specs

Link to this callback

options!(url, headers, options)

View Source

Specs

Specs

Link to this callback

patch(url, body, headers)

View Source

Specs

Link to this callback

patch(url, body, headers, options)

View Source

Specs

Specs

Link to this callback

patch!(url, body, headers)

View Source

Specs

Link to this callback

patch!(url, body, headers, options)

View Source

Specs

Specs

Link to this callback

post(url, body, headers)

View Source

Specs

Link to this callback

post(url, body, headers, options)

View Source

Specs

Specs

Link to this callback

post!(url, body, headers)

View Source

Specs

Link to this callback

post!(url, body, headers, options)

View Source

Specs

Specs

process_headers(list()) :: term()
Link to this callback

process_request_body(body)

View Source

Specs

process_request_body(body()) :: body()
Link to this callback

process_request_headers(headers)

View Source

Specs

process_request_headers(headers()) :: headers()
Link to this callback

process_request_options(options)

View Source

Specs

process_request_options(options()) :: options()
Link to this callback

process_request_params(params)

View Source

Specs

process_request_params(params()) :: params()
Link to this callback

process_request_url(url)

View Source

Specs

process_request_url(url()) :: url()
Link to this callback

process_response(response)

View Source

Specs

process_response(response()) :: term()
Link to this callback

process_response_body(binary)

View Source

Specs

process_response_body(binary()) :: term()
Link to this callback

process_response_chunk(binary)

View Source

Specs

process_response_chunk(binary()) :: term()
Link to this callback

process_response_headers(list)

View Source

Specs

process_response_headers(list()) :: term()
Link to this callback

process_response_status_code(integer)

View Source

Specs

process_response_status_code(integer()) :: term()
Link to this callback

process_status_code(integer)

View Source

Specs

process_status_code(integer()) :: term()

Specs

process_url(url()) :: url()

Specs

Specs

Link to this callback

put(url, body, headers)

View Source

Specs

Link to this callback

put(url, body, headers, options)

View Source

Specs

Specs

Specs

Link to this callback

put!(url, body, headers)

View Source

Specs

Link to this callback

put!(url, body, headers, options)

View Source

Specs

Specs

Specs

Link to this callback

request(method, url, body)

View Source

Specs

request(method(), url(), body()) ::
+  {:ok,
+   HTTPoison.Response.t()
+   | HTTPoison.AsyncResponse.t()
+   | HTTPoison.MaybeRedirect.t()}
+  | {:error, HTTPoison.Error.t()}
Link to this callback

request(method, url, body, headers)

View Source

Specs

Link to this callback

request(method, url, body, headers, options)

View Source

Specs

Specs

Link to this callback

request!(method, url, body)

View Source

Specs

Link to this callback

request!(method, url, body, headers)

View Source

Specs

Link to this callback

request!(method, url, body, headers, options)

View Source

Specs

Specs

start() :: {:ok, [atom()]} | {:error, term()}

Specs

stream_next(HTTPoison.AsyncResponse.t()) ::
+  {:ok, HTTPoison.AsyncResponse.t()} | {:error, HTTPoison.Error.t()}

Link to this section +Functions +

Link to this function

maybe_process_form(body)

View Source
\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.Error.html b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.Error.html new file mode 100644 index 0000000..a3eee9a --- /dev/null +++ b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--HTTPoison.Error.html @@ -0,0 +1,32 @@ +HTTPoison.Error — HTTPoison v2.2.1

+HTTPoison.Error exception(HTTPoison v2.2.1)View Source

Link to this section + Summary +

Functions

Callback implementation for Exception.message/1.

Link to this section +Types +

Specs

t() :: %HTTPoison.Error{
+  __exception__: true,
+  id: reference() | nil,
+  reason: any()
+}

Link to this section +Functions +

Callback implementation for Exception.message/1.

\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--changelog.html b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--changelog.html new file mode 100644 index 0000000..a7944cf --- /dev/null +++ b/test/support/fixtures/exdoc-0.25.5-httpoison-2.2.1--changelog.html @@ -0,0 +1,28 @@ +Changelog — HTTPoison v2.2.1 \ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.EncodeError.html b/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.EncodeError.html new file mode 100644 index 0000000..b791e00 --- /dev/null +++ b/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.EncodeError.html @@ -0,0 +1,30 @@ +Jason.EncodeError — jason v1.5.0-alpha.2

View SourceJason.EncodeErrorexception(jason v1.5.0-alpha.2)

Link to this section + Summary +

Link to this section +Types +

@type t() :: %Jason.EncodeError{__exception__: true, message: String.t()}

Link to this section +Functions +

\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.Encoder.html b/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.Encoder.html new file mode 100644 index 0000000..ac40cfe --- /dev/null +++ b/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.Encoder.html @@ -0,0 +1,46 @@ +Jason.Encoder — jason v1.5.0-alpha.2

View SourceJason.Encoderprotocol(jason v1.5.0-alpha.2)

Protocol controlling how a value is encoded to JSON.

deriving

+ Deriving +

The protocol allows leveraging the Elixir's @derive feature +to simplify protocol implementation in trivial cases. Accepted +options are:

  • :only - encodes only values of specified keys.
  • :except - encodes all struct fields except specified keys.

By default all keys except the :__struct__ key are encoded.

example

+ Example +

Let's assume a presence of the following struct:

defmoduleTestdodefstruct[:foo,:bar,:baz]end

If we were to call @derive Jason.Encoder just before defstruct, +an implementation similar to the following implementation would be generated:

defimplJason.Encoder,for:Testdodefencode(value,opts)doJason.Encode.map(Map.take(value,[:foo,:bar,:baz]),opts)endend

If we called @derive {Jason.Encoder, only: [:foo]}, an implementation +similar to the following implementation would be generated:

defimplJason.Encoder,for:Testdodefencode(value,opts)doJason.Encode.map(Map.take(value,[:foo]),opts)endend

If we called @derive {Jason.Encoder, except: [:foo]}, an implementation +similar to the following implementation would be generated:

defimplJason.Encoder,for:Testdodefencode(value,opts)doJason.Encode.map(Map.take(value,[:bar,:baz]),opts)endend

The actually generated implementations are more efficient computing some data +during compilation similar to the macros from the Jason.Helpers module.

explicit-implementation

+ Explicit implementation +

If you wish to implement the protocol fully yourself, it is advised to +use functions from the Jason.Encode module to do the actual iodata +generation - they are highly optimized and verified to always produce +valid JSON.

Link to this section + Summary +

Functions

Encodes value to JSON.

Link to this section +Types +

Link to this section +Functions +

@spec encode(t(), opts()) :: iodata()

Encodes value to JSON.

The argument opts is opaque - it can be passed to various functions in +Jason.Encode (or to the protocol function itself) for encoding values to JSON.

\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.OrderedObject.html b/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.OrderedObject.html new file mode 100644 index 0000000..632c01f --- /dev/null +++ b/test/support/fixtures/exdoc-0.28.6-jason-1.5.0.alpha2--Jason.OrderedObject.html @@ -0,0 +1,32 @@ +Jason.OrderedObject — jason v1.5.0-alpha.2

View SourceJason.OrderedObject(jason v1.5.0-alpha.2)

Link to this section + Summary +

Functions

Struct implementing a JSON object retaining order of properties.

Link to this section +Types +

@type t() :: %Jason.OrderedObject{values: [{String.Chars.t(), term()}]}

Link to this section +Functions +

Link to this function

%Jason.OrderedObject{}

View Source(struct)

Struct implementing a JSON object retaining order of properties.

A wrapper around a keyword (that supports non-atom keys) allowing for +proper protocol implementations.

Implements the Access behaviour and Enumerable protocol with +complexity similar to keywords/lists.

\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Mix.Tasks.Phx.Gen.Release.html b/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Mix.Tasks.Phx.Gen.Release.html new file mode 100644 index 0000000..41de661 --- /dev/null +++ b/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Mix.Tasks.Phx.Gen.Release.html @@ -0,0 +1,29 @@ +mix phx.gen.release — Phoenix v1.7.14

View Sourcemix phx.gen.release(Phoenix v1.7.14)

Generates release files and optional Dockerfile for release-based deployments.

The following release files are created:

  • lib/app_name/release.ex - A release module containing tasks for running +migrations inside a release

  • rel/overlays/bin/migrate - A migrate script for conveniently invoking +the release system migrations

  • rel/overlays/bin/server - A server script for conveniently invoking +the release system with environment variables to start the phoenix web server

Note, the rel/overlays directory is copied into the release build by default when +running mix release.

To skip generating the migration-related files, use the --no-ecto flag. To +force these migration-related files to be generated, the use --ecto flag.

Docker

When the --docker flag is passed, the following docker files are generated:

  • Dockerfile - The Dockerfile for use in any standard docker deployment

  • .dockerignore - A docker ignore file with standard elixir defaults

For extended release configuration, the mix release.init task can be used +in addition to this task. See the Mix.Release docs for more details.

Summary

Functions

\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Phoenix.Param.html b/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Phoenix.Param.html new file mode 100644 index 0000000..1e21e7b --- /dev/null +++ b/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Phoenix.Param.html @@ -0,0 +1,32 @@ +Phoenix.Param — Phoenix v1.7.14

View SourcePhoenix.Paramprotocol(Phoenix v1.7.14)

A protocol that converts data structures into URL parameters.

This protocol is used by URL helpers and other parts of the +Phoenix stack. For example, when you write:

user_path(conn,:edit,@user)

Phoenix knows how to extract the :id from @user thanks +to this protocol.

By default, Phoenix implements this protocol for integers, binaries, atoms, +and structs. For structs, a key :id is assumed, but you may provide a +specific implementation.

Nil values cannot be converted to param.

Custom parameters

In order to customize the parameter for any struct, +one can simply implement this protocol.

However, for convenience, this protocol can also be +derivable. For example:

defmoduleUserdo@derivePhoenix.Paramdefstruct[:id,:username]end

By default, the derived implementation will also use +the :id key. In case the user does not contain an +:id key, the key can be specified with an option:

defmoduleUserdo@derive{Phoenix.Param,key::username}defstruct[:username]end

will automatically use :username in URLs.

When using Ecto, you must call @derive before +your schema call:

@derive{Phoenix.Param,key::username}schema"users"do

Summary

Types

All the types that implement this protocol.

Types

@type t() :: term()

All the types that implement this protocol.

Functions

@spec to_param(term()) :: String.t()
\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Phoenix.Socket.Transport.html b/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Phoenix.Socket.Transport.html new file mode 100644 index 0000000..32572eb --- /dev/null +++ b/test/support/fixtures/exdoc-0.34.0-phoenix-1.7.14--Phoenix.Socket.Transport.html @@ -0,0 +1,105 @@ +Phoenix.Socket.Transport — Phoenix v1.7.14

View SourcePhoenix.Socket.Transportbehaviour(Phoenix v1.7.14)

Outlines the Socket <-> Transport communication.

Each transport, such as websockets and longpolling, must interact +with a socket. This module defines said behaviour.

Phoenix.Socket is just one possible implementation of a socket +that multiplexes events over multiple channels. If you implement +this behaviour, then a transport can directly invoke your +implementation, without passing through channels.

This module also provides convenience functions for implementing +transports.

Example

Here is a simple echo socket implementation:

defmoduleEchoSocketdo@behaviourPhoenix.Socket.Transportdefchild_spec(opts)do# We won't spawn any process, so let's ignore the child spec:ignoreenddefconnect(state)do# Callback to retrieve relevant data from the connection.# The map contains options, params, transport and endpoint keys.{:ok,state}enddefinit(state)do# Now we are effectively inside the process that maintains the socket.{:ok,state}enddefhandle_in({text,_opts},state)do{:reply,:ok,{:text,text},state}enddefhandle_info(_,state)do{:ok,state}enddefterminate(_reason,_state)do:okendend

It can be mounted in your endpoint like any other socket:

socket"/socket",EchoSocket,websocket:true,longpoll:true

You can now interact with the socket under /socket/websocket +and /socket/longpoll.

Custom transports

Sockets are operated by a transport. When a transport is defined, +it usually receives a socket module and the module will be invoked +when certain events happen at the transport level.

Whenever the transport receives a new connection, it should invoke +the connect/1 callback with a map of metadata. Different sockets +may require different metadata.

If the connection is accepted, the transport can move the connection +to another process, if so desires, or keep using the same process. The +process responsible for managing the socket should then call init/1.

For each message received from the client, the transport must call +handle_in/2 on the socket. For each informational message the +transport receives, it should call handle_info/2 on the socket.

Transports can optionally implement handle_control/2 for handling +control frames such as :ping and :pong.

On termination, terminate/2 must be called. A special atom with +reason :closed can be used to specify that the client terminated +the connection.

Booting

Whenever your endpoint starts, it will automatically invoke the +child_spec/1 on each listed socket and start that specification +under the endpoint supervisor.

Since the socket supervision tree is started by the endpoint, +any custom transport must be started after the endpoint in a +supervision tree.

Summary

Callbacks

Returns a child specification for socket management.

Connects to the socket.

Returns a child specification for terminating the socket.

Handles incoming control frames.

Handles incoming socket messages.

Handles info messages.

Initializes the socket state.

Invoked on termination.

Functions

Checks the origin request header against the list of allowed origins.

Checks the Websocket subprotocols request header against the allowed subprotocols.

Runs the code reloader if enabled.

Extracts connection information from conn and returns a map.

Logs the transport request.

Types

@type state() :: term()

Callbacks

@callback child_spec(keyword()) :: :supervisor.child_spec() | :ignore

Returns a child specification for socket management.

This is invoked only once per socket regardless of +the number of transports and should be responsible +for setting up any process structure used exclusively +by the socket regardless of transports.

Each socket connection is started by the transport +and the process that controls the socket likely +belongs to the transport. However, some sockets spawn +new processes, such as Phoenix.Socket which spawns +channels, and this gives the ability to start a +supervision tree associated to the socket.

It receives the socket options from the endpoint, +for example:

socket"/my_app",MyApp.Socket,shutdown:5000

means child_spec([shutdown: 5000]) will be invoked.

:ignore means no child spec is necessary for this socket.

Link to this callback

connect(transport_info)

View Source
@callback connect(transport_info :: map()) :: {:ok, state()} | {:error, term()} | :error

Connects to the socket.

The transport passes a map of metadata and the socket +returns {:ok, state}, {:error, reason} or :error. +The state must be stored by the transport and returned +in all future operations. When {:error, reason} is +returned, some transports - such as WebSockets - allow +customizing the response based on reason via a custom +:error_handler.

This function is used for authorization purposes and it +may be invoked outside of the process that effectively +runs the socket.

In the default Phoenix.Socket implementation, the +metadata expects the following keys:

  • :endpoint - the application endpoint
  • :transport - the transport name
  • :params - the connection parameters
  • :options - a keyword list of transport options, often +given by developers when configuring the transport. +It must include a :serializer field with the list of +serializers and their requirements
Link to this callback

drainer_spec(keyword)

View Source(optional)
@callback drainer_spec(keyword()) :: :supervisor.child_spec() | :ignore

Returns a child specification for terminating the socket.

This is a process that is started late in the supervision +tree with the specific goal of draining connections on +application shutdown.

Similar to child_spec/1, it receives the socket options +from the endpoint.

Link to this callback

handle_control({}, state)

View Source(optional)
@callback handle_control(
+  {message :: term(), opts :: keyword()},
+  state()
+) ::
+  {:ok, state()}
+  | {:reply, :ok | :error, {opcode :: atom(), message :: term()}, state()}
+  | {:stop, reason :: term(), state()}

Handles incoming control frames.

The message is represented as {payload, options}. It must +return one of:

  • {:ok, state} - continues the socket with no reply
  • {:reply, status, reply, state} - continues the socket with reply
  • {:stop, reason, state} - stops the socket

Control frames only supported when using websockets.

The options contains an opcode key, this will be either :ping or +:pong.

If a control frame doesn't have a payload, then the payload value +will be nil.

@callback handle_in(
+  {message :: term(), opts :: keyword()},
+  state()
+) ::
+  {:ok, state()}
+  | {:reply, :ok | :error, {opcode :: atom(), message :: term()}, state()}
+  | {:stop, reason :: term(), state()}

Handles incoming socket messages.

The message is represented as {payload, options}. It must +return one of:

  • {:ok, state} - continues the socket with no reply
  • {:reply, status, reply, state} - continues the socket with reply
  • {:stop, reason, state} - stops the socket

The reply is a tuple contain an opcode atom and a message that can +be any term. The built-in websocket transport supports both :text and +:binary opcode and the message must be always iodata. Long polling only +supports text opcode.

Link to this callback

handle_info(message, state)

View Source
@callback handle_info(message :: term(), state()) ::
+  {:ok, state()}
+  | {:push, {opcode :: atom(), message :: term()}, state()}
+  | {:stop, reason :: term(), state()}

Handles info messages.

The message is a term. It must return one of:

  • {:ok, state} - continues the socket with no reply
  • {:push, reply, state} - continues the socket with reply
  • {:stop, reason, state} - stops the socket

The reply is a tuple contain an opcode atom and a message that can +be any term. The built-in websocket transport supports both :text and +:binary opcode and the message must be always iodata. Long polling only +supports text opcode.

@callback init(state()) :: {:ok, state()}

Initializes the socket state.

This must be executed from the process that will effectively +operate the socket.

Link to this callback

terminate(reason, state)

View Source
@callback terminate(reason :: term(), state()) :: :ok

Invoked on termination.

If reason is :closed, it means the client closed the socket. This is +considered a :normal exit signal, so linked process will not automatically +exit. See Process.exit/2 for more details on exit signals.

Functions

Link to this function

check_origin(conn, handler, endpoint, opts, sender \\ &Plug.Conn.send_resp/1)

View Source

Checks the origin request header against the list of allowed origins.

Should be called by transports before connecting when appropriate. +If the origin header matches the allowed origins, no origin header was +sent or no origin was configured, it will return the given connection.

Otherwise a 403 Forbidden response will be sent and the connection halted. +It is a noop if the connection has been halted.

Link to this function

check_subprotocols(conn, subprotocols)

View Source

Checks the Websocket subprotocols request header against the allowed subprotocols.

Should be called by transports before connecting when appropriate. +If the sec-websocket-protocol header matches the allowed subprotocols, +it will put sec-websocket-protocol response header and return the given connection. +If no sec-websocket-protocol header was sent it will return the given connection.

Otherwise a 403 Forbidden response will be sent and the connection halted. +It is a noop if the connection has been halted.

Link to this function

code_reload(conn, endpoint, opts)

View Source

Runs the code reloader if enabled.

Link to this function

connect_info(conn, endpoint, keys)

View Source

Extracts connection information from conn and returns a map.

Keys are retrieved from the optional transport option :connect_info. +This functionality is transport specific. Please refer to your transports' +documentation for more information.

The supported keys are:

  • :peer_data - the result of Plug.Conn.get_peer_data/1

  • :trace_context_headers - a list of all trace context headers

  • :x_headers - a list of all request headers that have an "x-" prefix

  • :uri - a %URI{} derived from the conn

  • :user_agent - the value of the "user-agent" request header

Link to this function

transport_log(conn, level)

View Source

Logs the transport request.

Available for transports that generate a connection.

\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.Adapter.html b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.Adapter.html new file mode 100644 index 0000000..2d2b06a --- /dev/null +++ b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.Adapter.html @@ -0,0 +1,542 @@ + + + + + + + + Ecto.Adapter — Ecto v3.12.4 + + + + + + + + + + +
+ +
+ + +
+ +
+

+ + + View Source + + + Ecto.Adapter + + behaviour + + (Ecto v3.12.4) + +

+
+

Specifies the minimal API required from adapters. +

+
+
+
+

+ + + + Summary + +

+
+

+ Types + +

+
+ +
+

The metadata returned by the adapter + + init/1 + + . +

+
+
+
+
+ t() + +
+
+
+
+

+ Callbacks + +

+
+ +
+

The callback invoked in case the adapter needs to inject code. +

+
+
+
+ +
+

Returns true if a connection has been checked out. +

+
+
+
+ +
+

Checks out a connection for the duration of the given function. +

+
+
+
+ +
+

Returns the dumpers for a given type. +

+
+
+
+ +
+

Ensure all applications necessary to run the adapter are started. +

+
+
+
+ +
+

Initializes the adapter supervision tree by returning the children and adapter metadata. +

+
+
+
+ +
+

Returns the loaders for a given type. +

+
+
+
+
+

+ Functions + +

+
+ +
+

Returns the adapter metadata from its + + init/1 + + callback. +

+
+
+
+
+
+

+ + + + Types + +

+
+
+ +
+
+
+                      @type
+                       adapter_meta() :: %{
+                      optional(:stacktrace) =>
+                      boolean
+                      (),
+                      optional(
+                      any
+                      ()) =>
+                      any
+                      ()
+                      }
+                    
+
+

The metadata returned by the adapter + + init/1 + + . +

+

It must be a map and Ecto itself will always inject + two keys into the meta: +

+
    +
  • the + :cache + key, which as ETS table that can be used as a cache (if available) +
  • +
  • the + :pid + key, which is the PID returned by the child spec returned in + + init/1 + + +
  • +
+
+
+
+ +
+
+
+                      @type
+                       t() ::
+                      module
+                      ()
+                    
+
+
+
+
+
+
+

+ + + + Callbacks + +

+
+
+ +
+
+
+                      @macrocallback
+                       __before_compile__(env ::
+                      Macro.Env.t
+                      ()) ::
+                      Macro.t
+                      ()
+                    
+
+

The callback invoked in case the adapter needs to inject code. +

+
+
+
+ +
+
+
+                      @callback
+                       checked_out?(
+                      adapter_meta
+                      ()) ::
+                      boolean
+                      ()
+                    
+
+

Returns true if a connection has been checked out. +

+
+
+
+
+ + + Link to this callback + + +

checkout(adapter_meta, config, function) +

+ + + View Source + + +
+
+
+
+                      @callback
+                       checkout(
+                      adapter_meta
+                      (), config ::
+                      Keyword.t
+                      (), (-> result)) :: result
+                      when result: var
+                    
+
+

Checks out a connection for the duration of the given function. +

+

In case the adapter provides a pool, this guarantees all of the code + inside the given + fun + runs against the same connection, which +might improve performance by for instance allowing multiple related + calls to the datastore to share cache information: +

+
+                    
+                      Repo
+                      
+                      .
+                      
+                      checkout
+                      
+                      (
+                      
+                      fn
+                      
+                      
+                      
+                      ->
+                      
+                      
+                      
+                      for
+                      
+                      
+                      
+                      _
+                      
+                      
+                      
+                      
+                        <-1..100doRepo.insert!(%Post{})endend)

If the adapter does not provide a pool, just calling the passed function +and returning its result are enough.

If the adapter provides a pool, it is supposed to "check out" one of the +pool connections for the duration of the function call. Which connection +is checked out is not passed to the calling function, so it should be done +using a stateful method like using the current process' dictionary, process +tracking, or some kind of other lookup method. Make sure that this stored +connection is then used in the other callbacks implementations, such as +Ecto.Adapter.Queryable and Ecto.Adapter.Schema.

Link to this callback

dumpers(primitive_type, ecto_type)

View Source
@callback dumpers(primitive_type :: Ecto.Type.primitive(), ecto_type :: Ecto.Type.t()) ::
+  [
+    (term() -> {:ok, term()} | :error) | Ecto.Type.t()
+  ]

Returns the dumpers for a given type.

It receives the primitive type and the Ecto type (which may be +primitive as well). It returns a list of dumpers with the given +type usually at the beginning.

This allows developers to properly translate values coming from +the Ecto into adapter ones. For example, if the database does not +support booleans but instead returns 0 and 1 for them, you could +add:

defdumpers(:boolean,type),do:[type,&bool_encode/1]defdumpers(_primitive,type),do:[type]defpbool_encode(false),do:{:ok,0}defpbool_encode(true),do:{:ok,1}

All adapters are required to implement a clause for :binary_id types, +since they are adapter specific. If your adapter does not provide +binary ids, you may simply use Ecto.UUID:

defdumpers(:binary_id,type),do:[type,Ecto.UUID]defdumpers(_primitive,type),do:[type]
Link to this callback

ensure_all_started(config, type)

View Source
@callback ensure_all_started(
+  config :: Keyword.t(),
+  type :: :permanent | :transient | :temporary
+) :: {:ok, [atom()]} | {:error, atom()}

Ensure all applications necessary to run the adapter are started.

@callback init(config :: Keyword.t()) :: {:ok, :supervisor.child_spec(), adapter_meta()}

Initializes the adapter supervision tree by returning the children and adapter metadata.

Link to this callback

loaders(primitive_type, ecto_type)

View Source
@callback loaders(primitive_type :: Ecto.Type.primitive(), ecto_type :: Ecto.Type.t()) ::
+  [
+    (term() -> {:ok, term()} | :error) | Ecto.Type.t()
+  ]

Returns the loaders for a given type.

It receives the primitive type and the Ecto type (which may be +primitive as well). It returns a list of loaders with the given +type usually at the end.

This allows developers to properly translate values coming from +the adapters into Ecto ones. For example, if the database does not +support booleans but instead returns 0 and 1 for them, you could +add:

defloaders(:boolean,type),do:[&bool_decode/1,type]defloaders(_primitive,type),do:[type]defpbool_decode(0),do:{:ok,false}defpbool_decode(1),do:{:ok,true}

All adapters are required to implement a clause for :binary_id types, +since they are adapter specific. If your adapter does not provide binary +ids, you may simply use Ecto.UUID:

defloaders(:binary_id,type),do:[Ecto.UUID,type]defloaders(_primitive,type),do:[type]

Functions

Link to this function

lookup_meta(repo_name_or_pid)

View Source

Returns the adapter metadata from its init/1 callback.

It expects a process name of a repository. The name is either +an atom or a PID. For a given repository, you often want to +call this function based on the repository dynamic repo:

Ecto.Adapter.lookup_meta(repo.get_dynamic_repo())
diff --git a/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.Query.API.html b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.Query.API.html new file mode 100644 index 0000000..6bfa2dc --- /dev/null +++ b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.Query.API.html @@ -0,0 +1,6632 @@ + + + + + + + + Ecto.Query.API — Ecto v3.12.4 + + + + + + + + + + +
+ +
+ + +
+ +
+

+ + + View Source + + + Ecto.Query.API + + (Ecto v3.12.4) + +

+
+

Lists all functions allowed in the query API. +

+ +

Note the functions in this module exist for documentation +purposes and one should never need to invoke them directly. +Furthermore, it is possible to define your own macros and +use them in Ecto queries (see docs for + + fragment/1 + + ). +

+

+ + + + Intervals + +

+

Ecto supports following values for + interval + option: + "year" + , + "month" + , + "week" + , + "day" + , + "hour" + , + "minute" + , + "second" + , + "millisecond" + , and + "microsecond" + . +

+

+ + Date + + / + + Time + + functions like + + datetime_add/3 + + , + + date_add/3 + + , + + from_now/2 + + , + + ago/2 + + take + interval + as an argument. +

+

+ + + + Window API + +

+

Ecto also supports many of the windows functions found +in SQL databases. See + + Ecto.Query.WindowAPI + + for more +information. +

+

+ + + + About the arithmetic operators + +

+

The Ecto implementation of these operators provide only +a thin layer above the adapters. So if your adapter allows you +to use them in a certain way (like adding a date and an +interval in PostgreSQL), it should work just fine in Ecto +queries. +

+
+
+
+

+ + + + Summary + +

+
+

+ Functions + +

+
+ +
+

Binary + * + operation. +

+
+
+
+ +
+

Binary + + + operation. +

+
+
+
+ +
+

Binary + - + operation. +

+
+
+
+ +
+

Binary + / + operation. +

+
+
+
+ +
+

Binary + != + operation. +

+
+
+
+

Binary < operation.

Binary <= operation.

Binary == operation.

Binary > operation.

Binary >= + operation. +

+
+
+
+ +
+

Subtracts the given interval from the current time in UTC. +

+
+
+
+ +
+

Evaluates whether all values returned from the provided subquery match in a comparison operation. +

+
+
+
+ +
+

Binary + and + operation. +

+
+
+
+ +
+

Tests whether one or more values returned from the provided subquery match in a comparison operation. +

+
+
+
+ +
+

Refer to a named atom binding. +

+
+
+
+ +
+

Calculates the average for the given entry. +

+
+
+
+ +
+

Takes the first value which is not null, or null if they both are. +

+
+
+
+ +
+

Counts the entries in the table. +

+
+
+
+ +
+

Counts the given entry. +

+
+
+
+ +
+

Counts the distinct values in given entry. +

+
+
+
+ +
+

Adds a given interval to a date. +

+
+
+
+ +
+

Adds a given interval to a datetime. +

+
+
+
+ +
+

Evaluates to true if the provided subquery returns 1 or more rows. +

+
+
+
+ +
+

Allows a field to be dynamically accessed. +

+
+
+
+ +
+

Applies the given expression as a FILTER clause against an +aggregate. This is currently only supported by Postgres. +

+
+
+
+ +
+

Send fragments directly to the database. +

+
+
+
+ +
+

Adds the given interval to the current time in UTC. +

+
+
+
+ +
+

Searches for + search + in + string + in a case insensitive fashion. +

+
+
+
+ +
+

Checks if the left-value is included in the right one. +

+
+
+
+ +
+

Checks if the given value is nil. +

+
+
+
+ +
+

Returns value from the + json_field + pointed to by + path + . +

+
+
+
+ +
+

Searches for + search + in + string + . +

+
+
+
+ +
+

Allows a literal identifier to be injected into a fragment +

+
+
+
+ +
+

Used in + select + to specify which fields should be returned as a map. +

+
+
+
+ +
+

Calculates the maximum for the given entry. +

+
+
+
+ +
+

Merges the map on the right over the map on the left. +

+
+
+
+ +
+

Calculates the minimum for the given entry. +

+
+
+
+ +
+

Unary + not + operation. +

+
+
+
+ +
+

Binary + or + operation. +

+
+
+
+ +
+

Refer to a named atom binding in the parent query. +

+
+
+
+ +
+

Refer to an alias of a selected value. +

+
+
+
+ +
+

Creates an alias for the given selected value. +

+
+
+
+ +
+

Allows a list argument to be spliced into a fragment. +

+
+
+
+ +
+

Used in + select + to specify which struct fields should be returned. +

+
+
+
+ +
+

Calculates the sum for the given entry. +

+
+
+
+ +
+

Casts the given value to the given type at the database level. +

+
+
+
+ +
+

Creates a values list/constant table. +

+
+
+
+
+
+

+ + + + Functions + +

+
+
+ +
+

Binary + * + operation. +

+
+
+
+ +
+

Binary + + + operation. +

+
+
+
+ +
+

Binary + - + operation. +

+
+
+
+ +
+

Binary + / + operation. +

+
+
+
+ +
+

Binary + != + operation. +

+
+
+
+

Binary < operation.

Binary <= operation.

Binary == operation.

Binary > operation.

Binary >= + operation. +

+
+
+
+ +
+

Subtracts the given interval from the current time in UTC. +

+

The current time in UTC is retrieved from Elixir and +not from the database. +

+

See + Intervals + for supported + interval + values. +

+

+ + + + Examples + +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      published_at
+                      
+                      
+                      
+                      >
+                      
+                      
+                      
+                      ago
+                      
+                      (
+                      
+                      3
+                      
+                      ,
+                      
+                      
+                      
+                      "month"
+                      
+                      )
+                      
+                    
+                  
+
+
+
+ +
+

Evaluates whether all values returned from the provided subquery match in a comparison operation. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      
+                      
+                      >=
+                      
+                      
+                      
+                      all
+                      
+                      (
+                      
+                      
+                      
+                      from
+                      
+                      (
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      avg
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      group_by
+                      
+                      :
+                      
+                      
+                      
+                      [
+                      
+                      p
+                      
+                      .
+                      
+                      category_id
+                      
+                      ]
+                      
+                      )
+                      
+                      
+                      
+                      )
+                      
+                    
+                  
+

For a post to match in the above example it must be visited at least as much as the average post in all categories. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      
+                      
+                      ==
+                      
+                      
+                      
+                      all
+                      
+                      (
+                      
+                      
+                      
+                      from
+                      
+                      (
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      max
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      )
+                      
+                      )
+                      
+                      
+                      
+                      )
+                      
+                    
+                  
+

The above example matches all the posts which are tied for being the most visited. +

+

Both + any + and + all + must be given a subquery as an argument, and they must be used on the right hand side of a comparison. +Both can be used with every comparison operator: + == + , + != + , + > + , + >= + , + + <, <=.

Binary and operation.

Tests whether one or more values returned from the provided subquery match in a comparison operation.

frompinProduct,where:p.id==any(from(liinLineItem,select:[li.product_id],where:li.created_at>^sinceandli.qty>=
+                      
+                        
+                        
+                        10
+                        
+                        )
+                        
+                        
+                        
+                        )
+                        
+                      
+                      
+

A product matches in the above example if a line item was created since the provided date where the customer purchased +at least 10 units. +

+

Both + any + and + all + must be given a subquery as an argument, and they must be used on the right hand side of a comparison. +Both can be used with every comparison operator: + == + , + != + , + > + , + >= + , + + <, <=.

Refer to a named atom binding.

See the "Named bindings" section in Ecto.Query for more information.

Calculates the average for the given entry.

frompinPayment,select:avg(p.value)

Takes the first value which is not null, or null if they both are.

In SQL, COALESCE takes any number of arguments, but in ecto +it only takes two, so it must be chained to achieve the same +effect.

frompinPayment,select:p.value|>coalesce(p.backup_value)|>
+
+  
+  
+  coalesce
+  
+  (
+  
+  0
+  
+  )
+  
+
+
+
+
+
+ +
+

Counts the entries in the table. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      count
+                      
+                      (
+                      
+                      )
+                      
+                    
+                  
+
+
+
+ +
+

Counts the given entry. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      count
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      id
+                      
+                      )
+                      
+                    
+                  
+
+
+
+ +
+

Counts the distinct values in given entry. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      count
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      id
+                      
+                      ,
+                      
+                      
+                      
+                      :distinct
+                      
+                      )
+                      
+                    
+                  
+
+
+
+
+ + + Link to this function + + +

date_add(date, count, interval) +

+ + + View Source + + +
+
+

Adds a given interval to a date. +

+

See + + datetime_add/3 + + for more information. +

+

See + Intervals + for supported + interval + values. +

+
+
+
+
+ + + Link to this function + + +

datetime_add(datetime, count, interval) +

+ + + View Source + + +
+
+

Adds a given interval to a datetime. +

+

The first argument is a + datetime + , the second one is the count +for the interval, which may be either positive or negative and +the interval value: +

+
+                    
+                      # Get all items published since the last month
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      published_at
+                      
+                      
+                      
+                      >
+                      
+                      
+                      
+                      datetime_add
+                      
+                      (
+                      
+                      ^
+                      
+                      NaiveDateTime
+                      
+                      .
+                      
+                      utc_now
+                      
+                      (
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      -
+                      
+                      1
+                      
+                      ,
+                      
+                      
+                      
+                      "month"
+                      
+                      )
+                      
+                    
+                  
+

In the example above, we used + + datetime_add/3 + + to subtract one month +from the current datetime and compared it with the + p.published_at + . +If you want to perform operations on date, + + date_add/3 + + could be used. +

+

See + Intervals + for supported + interval + values. +

+
+
+
+ +
+

Evaluates to true if the provided subquery returns 1 or more rows. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      as
+                      
+                      :
+                      
+                      
+                      
+                      :post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      exists
+                      
+                      (
+                      
+                      
+                      
+                      from
+                      
+                      (
+                      
+                      
+                      
+                      c
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Comment
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      parent_as
+                      
+                      (
+                      
+                      :post
+                      
+                      )
+                      
+                      .
+                      
+                      id
+                      
+                      
+                      
+                      ==
+                      
+                      
+                      
+                      c
+                      
+                      .
+                      
+                      post_id
+                      
+                      
+                      
+                      and
+                      
+                      
+                      
+                      c
+                      
+                      .
+                      
+                      replies_count
+                      
+                      
+                      
+                      >
+                      
+                      
+                      
+                      5
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      1
+                      
+                      
+                      
+                      )
+                      
+                      
+                      
+                      )
+                      
+                    
+                  
+

This is best used in conjunction with + parent_as + to correlate the subquery +with the parent query to test some condition on related rows in a different table. +In the above example the query returns posts which have at least one comment that +has more than 5 replies. +

+
+
+
+ +
+

Allows a field to be dynamically accessed. +

+
+                    
+                      def
+                      
+                      
+                      
+                      at_least_four
+                      
+                      (
+                      
+                      doors_or_tires
+                      
+                      )
+                      
+                      
+                      
+                      do
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      c
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Car
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      field
+                      
+                      (
+                      
+                      c
+                      
+                      ,
+                      
+                      
+                      
+                      ^
+                      
+                      doors_or_tires
+                      
+                      )
+                      
+                      
+                      
+                      >=
+                      
+                      
+                      
+                      4
+                      
+                      
+                      
+                      end
+                      
+                    
+                  
+

In the example above, both + at_least_four(:doors) + and + at_least_four(:tires) + + would be valid calls as the field is dynamically generated. +

+
+
+
+ +
+

Applies the given expression as a FILTER clause against an +aggregate. This is currently only supported by Postgres. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Payment
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      filter
+                      
+                      (
+                      
+                      avg
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      value
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      value
+                      
+                      
+                      
+                      >
+                      
+                      
+                      
+                      0
+                      
+                      
+                      
+                      and
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      value
+                      
+                      
+                      
+                      
+                        <100)frompinPayment,select:avg(p.value)|>
+                        
+                      
+                      
+                      filter
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      value
+                      
+                      
+                      
+                      
+                        <0)

Send fragments directly to the database.

It is not possible to represent all possible database queries using +Ecto's query syntax. When such is required, it is possible to use +fragments to send any expression to the database:

defunpublished_by_title(title)dofrompinPost,where:is_nil(p.published_at)andfragment("lower(?)",p.title)==^titleend

Every occurrence of the ? character will be interpreted as a place +for parameters, which must be given as additional arguments to + fragment. If the literal character ? is required as part of the +fragment, it can be escaped with \\? (one escape for strings, +another for fragment).

In the example above, we are using the lower procedure in the +database to downcase the title column.

It is very important to keep in mind that Ecto is unable to do any +type casting when fragments are used. Therefore it may be necessary +to explicitly cast parameters via type/2:

fragment("lower(?)",p.title)==type(^title,:string)

Literals

Sometimes you need to interpolate a literal value into a fragment, +instead of a parameter. For example, you may need to pass a table +name or a collation, such as:

collation="es_ES"fragment("? COLLATE ?",^name,^collation)

The example above won't work because collation will be passed +as a parameter, while it has to be a literal part of the query.

You can address this by telling Ecto that variable is a literal:

fragment("? COLLATE ?",^name,literal(^collation))

Ecto will then escape it and make it part of the query.

Literals and query caching

Because literals are made part of the query, each interpolated +literal will generate a separate query, with its own cache.

Splicing

Sometimes you may need to interpolate a variable number of arguments +into the same fragment. For example, when overriding Ecto's default + where behaviour for Postgres:

frompinPost,where:fragment("? in (?, ?)",p.id,val1,val2)

The example above will only work if you know the number of arguments +upfront. If it can vary, the above will not work.

You can address this by telling Ecto to splice a list argument into +the fragment:

frompinPost,where:fragment("? in (?)",p.id,splice(^val_list))

This will let Ecto know it should expand the values of the list into +separate fragment arguments. For example:

frompinPost,where:fragment("? in (?)",p.id,splice(^[1,2,3]))

would be expanded into

frompinPost,where:fragment("? in (?,?,?)",p.id,^1,^2,^3)

Defining custom functions using macros and fragment

You can add a custom Ecto query function using macros. For example +to expose SQL's coalesce function you can define this macro:

defmoduleCustomFunctionsdodefmacrocoalesce(left,right)doquotedofragment("coalesce(?, ?)",unquote(left),unquote(right))endendend

To have coalesce/2 available, just import the module that defines it.

importCustomFunctions

The only downside is that it will show up as a fragment when +inspecting the Elixir query. Other than that, it should be +equivalent to a built-in Ecto query function.

Keyword fragments

In order to support databases that do not have string-based +queries, like MongoDB, fragments also allow keywords to be given:

frompinPost,where:fragment(title:["$eq":^some_value])
Link to this function

from_now(count, interval)

View Source

Adds the given interval to the current time in UTC.

The current time in UTC is retrieved from Elixir and +not from the database.

See Intervals for supported interval values.

Examples

fromainAccount,where:a.expires_at<from_now(3,"month")

Searches for search in string in a case insensitive fashion.

frompinPost,where:ilike(p.body,"Chapter%")

Translates to the underlying SQL ILIKE query. This operation is +only available on PostgreSQL.

Checks if the left-value is included in the right one.

frompinPost,where:p.idin[1,2,3]

The right side may either be a literal list, an interpolated list, +any struct that implements the Enumerable protocol, or even a +column in the database with array type:

frompinPost,where:"elixir"inp.tags

Additionally, the right side may also be a subquery, which should return +a single column:

fromcinComment,where:c.post_idinsubquery(from(pinPost,where:p.created_at>^since,select:p.id))

Checks if the given value is nil.

frompinPost,where:is_nil(p.published_at)

To check if a given value is not nil use:

frompinPost,where:notis_nil(p.published_at)
Link to this function

json_extract_path(json_field, path)

View Source

Returns value from the json_field pointed to by path.

from(postinPost,select:json_extract_path(post.meta,["author","name"]))

The path can be dynamic:

path=["author","name"]from(postinPost,select:json_extract_path(post.meta,^path))

And the field can also be dynamic in combination with it:

path=["author","name"]from(postinPost,select:json_extract_path(field(post,:meta),^path))

The query can be also rewritten as:

from(postinPost,select:post.meta["author"]["name"])

Path elements can be integers to access values in JSON arrays:

from(postinPost,select:post.meta["tags"][0]["name"])

Any element of the path can be dynamic:

field="name"from(postinPost,select:post.meta["author"][^field])

Warning: indexes on PostgreSQL

PostgreSQL supports indexing on jsonb columns via GIN indexes. +Whenever comparing the value of a jsonb field against a string +or integer, Ecto will use the containment operator @> which +is optimized. You can even use the more efficient + jsonb_path_ops + + GIN index variant. For more information, consult PostgreSQL's docs +on + JSON indexing + . +

+

+ + + + Warning: return types + +

+

The underlying data in the JSON column is returned without any +additional decoding. This means "null" JSON values are not the +same as SQL's "null". For example, the + Repo.all + operation below +returns an empty list because + p.meta["author"] + returns JSON's +null and therefore + is_nil + does not succeed: +

+
+    
+      Repo
+      
+      .
+      
+      insert!
+      
+      (
+      
+      %
+      
+      Post
+      
+      {
+      
+      meta
+      
+      :
+      
+      
+      
+      %{
+      
+      author
+      
+      :
+      
+      
+      
+      nil
+      
+      }
+      
+      }
+      
+      )
+      
+      
+      
+      Repo
+      
+      .
+      
+      all
+      
+      (
+      
+      from
+      
+      (
+      
+      post
+      
+      
+      
+      in
+      
+      
+      
+      Post
+      
+      ,
+      
+      
+      
+      where
+      
+      :
+      
+      
+      
+      is_nil
+      
+      (
+      
+      p
+      
+      .
+      
+      meta
+      
+      [
+      
+      "author"
+      
+      ]
+      
+      )
+      
+      )
+      
+      )
+      
+    
+  
+

Similarly, other types, such as datetimes, are returned as strings. +This means conditions like + post.meta["published_at"] > from_now(-1, "day") + + may return incorrect results or fail as the underlying database +tries to compare incompatible types. You can, however, use + + type/2 + + + to force the types on the database level. +

+
+
+
+ +
+

Searches for + search + in + string + . +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      like
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      body
+                      
+                      ,
+                      
+                      
+                      
+                      "Chapter%"
+                      
+                      )
+                      
+                    
+                  
+

Translates to the underlying SQL LIKE query, therefore +its behaviour is dependent on the database. In particular, +PostgreSQL will do a case-sensitive operation, while the +majority of other databases will be case-insensitive. For +performing a case-insensitive + like + in PostgreSQL, see + + ilike/2 + + . +

+

You should be very careful when allowing user sent data to be used +as part of LIKE query, since they allow to perform + LIKE-injections + . +

+
+
+
+ +
+

Allows a literal identifier to be injected into a fragment: +

+
+                    
+                      collation
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      "es_ES"
+                      
+                      
+                      
+                      fragment
+                      
+                      (
+                      
+                      "? COLLATE ?"
+                      
+                      ,
+                      
+                      
+                      
+                      ^
+                      
+                      name
+                      
+                      ,
+                      
+                      
+                      
+                      literal
+                      
+                      (
+                      
+                      ^
+                      
+                      collation
+                      
+                      )
+                      
+                      )
+                      
+                    
+                  
+

The example above will inject + collation + into the query as +a literal identifier instead of a query parameter. Note that +each different value of + collation + will emit a different query, +which will be independently prepared and cached. +

+
+
+
+ +
+

Used in + select + to specify which fields should be returned as a map. +

+

For example, if you don't need all fields to be returned or +neither need a struct, you can use + + map/2 + + to achieve both: +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      map
+                      
+                      (
+                      
+                      p
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :title
+                      
+                      ,
+                      
+                      
+                      
+                      :body
+                      
+                      ]
+                      
+                      )
+                      
+                    
+                  
+

+ + map/2 + + can also be used to dynamically select fields: +

+
+                    
+                      fields
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      [
+                      
+                      :title
+                      
+                      ,
+                      
+                      
+                      
+                      :body
+                      
+                      ]
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      map
+                      
+                      (
+                      
+                      p
+                      
+                      ,
+                      
+                      
+                      
+                      ^
+                      
+                      fields
+                      
+                      )
+                      
+                    
+                  
+

If the same source is selected multiple times with a + map + , +the fields are merged in order to avoid fetching multiple copies +from the database. In other words, the expression below: +

+
+                    
+                      from
+                      
+                      (
+                      
+                      city
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      City
+                      
+                      ,
+                      
+                      
+                      
+                      preload
+                      
+                      :
+                      
+                      
+                      
+                      :country
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      {
+                      
+                      map
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :country_id
+                      
+                      ]
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      map
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :name
+                      
+                      ]
+                      
+                      )
+                      
+                      }
+                      
+                      )
+                      
+                    
+                  
+

is expanded to: +

+
+                    
+                      from
+                      
+                      (
+                      
+                      city
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      City
+                      
+                      ,
+                      
+                      
+                      
+                      preload
+                      
+                      :
+                      
+                      
+                      
+                      :country
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      {
+                      
+                      map
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :country_id
+                      
+                      ,
+                      
+                      
+                      
+                      :name
+                      
+                      ]
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      map
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :country_id
+                      
+                      ,
+                      
+                      
+                      
+                      :name
+                      
+                      ]
+                      
+                      )
+                      
+                      }
+                      
+                      )
+                      
+                    
+                  
+

For preloads, the selected fields may be specified from the parent: +

+
+                    
+                      from
+                      
+                      (
+                      
+                      city
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      City
+                      
+                      ,
+                      
+                      
+                      
+                      preload
+                      
+                      :
+                      
+                      
+                      
+                      :country
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      map
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :country_id
+                      
+                      ,
+                      
+                      
+                      
+                      :name
+                      
+                      ,
+                      
+                      
+                      
+                      country
+                      
+                      :
+                      
+                      
+                      
+                      [
+                      
+                      :id
+                      
+                      ,
+                      
+                      
+                      
+                      :population
+                      
+                      ]
+                      
+                      ]
+                      
+                      )
+                      
+                      )
+                      
+                    
+                  
+

It's also possible to select a struct from one source but only a subset of +fields from one of its associations: +

+
+                    
+                      from
+                      
+                      (
+                      
+                      city
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      City
+                      
+                      ,
+                      
+                      
+                      
+                      preload
+                      
+                      :
+                      
+                      
+                      
+                      :country
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      %{
+                      
+                      city
+                      
+                      
+                      
+                      |
+                      
+                      
+                      
+                      country
+                      
+                      :
+                      
+                      
+                      
+                      map
+                      
+                      (
+                      
+                      country
+                      
+                      :
+                      
+                      
+                      
+                      [
+                      
+                      :id
+                      
+                      ,
+                      
+                      
+                      
+                      :population
+                      
+                      ]
+                      
+                      )
+                      
+                      }
+                      
+                      )
+                      
+                    
+                  
+

+ IMPORTANT + : When filtering fields for associations, you +MUST include the foreign keys used in the relationship, +otherwise Ecto will be unable to find associated records. +

+
+
+
+ +
+

Calculates the maximum for the given entry. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Payment
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      max
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      value
+                      
+                      )
+                      
+                    
+                  
+
+
+
+ +
+

Merges the map on the right over the map on the left. +

+

If the map on the left side is a struct, Ecto will check +all of the field on the right previously exist on the left +before merging. +

+
+                    
+                      from
+                      
+                      (
+                      
+                      city
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      City
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      merge
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      %{
+                      
+                      virtual_field
+                      
+                      :
+                      
+                      
+                      
+                      "some_value"
+                      
+                      }
+                      
+                      )
+                      
+                      )
+                      
+                    
+                  
+

This function is primarily used by + + Ecto.Query.select_merge/3 + + + to merge different select clauses. +

+
+
+
+ +
+

Calculates the minimum for the given entry. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Payment
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      min
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      value
+                      
+                      )
+                      
+                    
+                  
+
+
+
+ +
+

Unary + not + operation. +

+

It is used to negate values in + :where + . It is also used to match +the assert the opposite of + + in/2 + + , + + is_nil/1 + + , and + + exists/1 + + . +For example: +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      id
+                      
+                      
+                      
+                      not
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      [
+                      
+                      1
+                      
+                      ,
+                      
+                      
+                      
+                      2
+                      
+                      ,
+                      
+                      
+                      
+                      3
+                      
+                      ]
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      not
+                      
+                      
+                      
+                      is_nil
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      title
+                      
+                      )
+                      
+                      
+                      
+                      # Retrieve all the posts that doesn't have comments.
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      as
+                      
+                      :
+                      
+                      
+                      
+                      :post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      not
+                      
+                      
+                      
+                      exists
+                      
+                      (
+                      
+                      
+                      
+                      from
+                      
+                      (
+                      
+                      
+                      
+                      c
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Comment
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      parent_as
+                      
+                      (
+                      
+                      :post
+                      
+                      )
+                      
+                      .
+                      
+                      id
+                      
+                      
+                      
+                      ==
+                      
+                      
+                      
+                      c
+                      
+                      .
+                      
+                      post_id
+                      
+                      
+                      
+                      )
+                      
+                      
+                      
+                      )
+                      
+                    
+                  
+
+
+
+ +
+

Binary + or + operation. +

+
+
+
+ +
+

Refer to a named atom binding in the parent query. +

+

This is available only inside subqueries. +

+

See the "Named bindings" section in + + Ecto.Query + + for more information. +

+
+
+
+ +
+

Refer to an alias of a selected value. +

+

This can be used to refer to aliases created using + + selected_as/2 + + . If +the alias hasn't been created using + + selected_as/2 + + , an error will be raised. +

+

Each database has its own rules governing which clauses can reference these aliases. +If an error is raised mentioning an unknown column, most likely the alias is being +referenced somewhere that is not allowed. Consult the documentation for the database +to ensure the alias is being referenced correctly. +

+
+
+
+
+ + + Link to this function + + +

selected_as(selected_value, name) +

+ + + View Source + + +
+
+

Creates an alias for the given selected value. +

+

When working with calculated values, an alias can be used to simplify +the query. Otherwise, the entire expression would need to be copied when +referencing it outside of select statements. +

+

This comes in handy when, for instance, you would like to use the calculated +value in + + Ecto.Query.group_by/3 + + or + + Ecto.Query.order_by/3 + + : +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      %{
+                      
+                      
+                      
+                      posted
+                      
+                      :
+                      
+                      
+                      
+                      selected_as
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      posted
+                      
+                      ,
+                      
+                      
+                      
+                      :date
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      sum_visits
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      coalesce
+                      
+                      (
+                      
+                      0
+                      
+                      )
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      sum
+                      
+                      (
+                      
+                      )
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      selected_as
+                      
+                      (
+                      
+                      :sum_visits
+                      
+                      )
+                      
+                      
+                      
+                      }
+                      
+                      ,
+                      
+                      
+                      
+                      group_by
+                      
+                      :
+                      
+                      
+                      
+                      selected_as
+                      
+                      (
+                      
+                      :date
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      order_by
+                      
+                      :
+                      
+                      
+                      
+                      selected_as
+                      
+                      (
+                      
+                      :sum_visits
+                      
+                      )
+                      
+                    
+                  
+

The name of the alias must be an atom and it can only be used in the outer most +select expression, otherwise an error is raised. Please note that the alias name +does not have to match the key when + select + returns a map, struct or keyword list. +

+

Using this in conjunction with + + selected_as/1 + + is recommended to ensure only defined aliases +are referenced. +

+

+ + + + Subqueries and CTEs + +

+

Subqueries and CTEs automatically alias the selected fields, for example, one can write: +

+
+                    
+                      # Subquery
+                      
+                      
+                      
+                      s
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      %{
+                      
+                      visits
+                      
+                      :
+                      
+                      
+                      
+                      coalesce
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      ,
+                      
+                      
+                      
+                      0
+                      
+                      )
+                      
+                      }
+                      
+                      
+                      
+                      from
+                      
+                      (
+                      
+                      s
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      subquery
+                      
+                      (
+                      
+                      s
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      s
+                      
+                      .
+                      
+                      visits
+                      
+                      )
+                      
+                      
+                      
+                      # CTE
+                      
+                      
+                      
+                      cte_query
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      %{
+                      
+                      visits
+                      
+                      :
+                      
+                      
+                      
+                      coalesce
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      ,
+                      
+                      
+                      
+                      0
+                      
+                      )
+                      
+                      }
+                      
+                      
+                      
+                      Post
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      with_cte
+                      
+                      (
+                      
+                      "cte"
+                      
+                      ,
+                      
+                      
+                      
+                      as
+                      
+                      :
+                      
+                      
+                      
+                      ^
+                      
+                      cte_query
+                      
+                      )
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      join
+                      
+                      (
+                      
+                      :inner
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      p
+                      
+                      ]
+                      
+                      ,
+                      
+                      
+                      
+                      c
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      "cte"
+                      
+                      )
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      select
+                      
+                      (
+                      
+                      [
+                      
+                      p
+                      
+                      ,
+                      
+                      
+                      
+                      c
+                      
+                      ]
+                      
+                      ,
+                      
+                      
+                      
+                      c
+                      
+                      .
+                      
+                      visits
+                      
+                      )
+                      
+                    
+                  
+

However, one can also use + selected_as + to override the default naming: +

+
+                    
+                      # Subquery
+                      
+                      
+                      
+                      s
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      %{
+                      
+                      visits
+                      
+                      :
+                      
+                      
+                      
+                      coalesce
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      ,
+                      
+                      
+                      
+                      0
+                      
+                      )
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      selected_as
+                      
+                      (
+                      
+                      :num_visits
+                      
+                      )
+                      
+                      }
+                      
+                      
+                      
+                      from
+                      
+                      (
+                      
+                      s
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      subquery
+                      
+                      (
+                      
+                      s
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      s
+                      
+                      .
+                      
+                      num_visits
+                      
+                      )
+                      
+                      
+                      
+                      # CTE
+                      
+                      
+                      
+                      cte_query
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      %{
+                      
+                      visits
+                      
+                      :
+                      
+                      
+                      
+                      coalesce
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      ,
+                      
+                      
+                      
+                      0
+                      
+                      )
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      selected_as
+                      
+                      (
+                      
+                      :num_visits
+                      
+                      )
+                      
+                      }
+                      
+                      
+                      
+                      Post
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      with_cte
+                      
+                      (
+                      
+                      "cte"
+                      
+                      ,
+                      
+                      
+                      
+                      as
+                      
+                      :
+                      
+                      
+                      
+                      ^
+                      
+                      cte_query
+                      
+                      )
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      join
+                      
+                      (
+                      
+                      :inner
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      p
+                      
+                      ]
+                      
+                      ,
+                      
+                      
+                      
+                      c
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      "cte"
+                      
+                      )
+                      
+                      
+                      
+                      |>
+                      
+                      
+                      
+                      select
+                      
+                      (
+                      
+                      [
+                      
+                      p
+                      
+                      ,
+                      
+                      
+                      
+                      c
+                      
+                      ]
+                      
+                      ,
+                      
+                      
+                      
+                      c
+                      
+                      .
+                      
+                      num_visits
+                      
+                      )
+                      
+                    
+                  
+

The name given to + + selected_as/2 + + can also be referenced in + + selected_as/1 + + , +as in regular queries. +

+
+
+
+ +
+

Allows a list argument to be spliced into a fragment. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      fragment
+                      
+                      (
+                      
+                      "? in (?)"
+                      
+                      ,
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      id
+                      
+                      ,
+                      
+                      
+                      
+                      splice
+                      
+                      (
+                      
+                      ^
+                      
+                      [
+                      
+                      1
+                      
+                      ,
+                      
+                      
+                      
+                      2
+                      
+                      ,
+                      
+                      
+                      
+                      3
+                      
+                      ]
+                      
+                      )
+                      
+                      )
+                      
+                    
+                  
+

The example above will be transformed at runtime into the following: +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      fragment
+                      
+                      (
+                      
+                      "? in (?,?,?)"
+                      
+                      ,
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      id
+                      
+                      ,
+                      
+                      
+                      
+                      ^
+                      
+                      1
+                      
+                      ,
+                      
+                      
+                      
+                      ^
+                      
+                      2
+                      
+                      ,
+                      
+                      
+                      
+                      ^
+                      
+                      3
+                      
+                      )
+                      
+                    
+                  
+

You may only splice runtime values. For example, this would not work because +query bindings are compile-time constructs: +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      fragment
+                      
+                      (
+                      
+                      "concat(?)"
+                      
+                      ,
+                      
+                      
+                      
+                      splice
+                      
+                      (
+                      
+                      ^
+                      
+                      [
+                      
+                      p
+                      
+                      .
+                      
+                      count
+                      
+                      ,
+                      
+                      
+                      
+                      " "
+                      
+                      ,
+                      
+                      
+                      
+                      "count"
+                      
+                      ]
+                      
+                      )
+                      
+                    
+                  
+
+
+
+ +
+

Used in + select + to specify which struct fields should be returned. +

+

For example, if you don't need all fields to be returned +as part of a struct, you can filter it to include only certain +fields by using + + struct/2 + + : +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      struct
+                      
+                      (
+                      
+                      p
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :title
+                      
+                      ,
+                      
+                      
+                      
+                      :body
+                      
+                      ]
+                      
+                      )
+                      
+                    
+                  
+

+ + struct/2 + + can also be used to dynamically select fields: +

+
+                    
+                      fields
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      [
+                      
+                      :title
+                      
+                      ,
+                      
+                      
+                      
+                      :body
+                      
+                      ]
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      struct
+                      
+                      (
+                      
+                      p
+                      
+                      ,
+                      
+                      
+                      
+                      ^
+                      
+                      fields
+                      
+                      )
+                      
+                    
+                  
+

As a convenience, + select + allows developers to take fields +without an explicit call to + + struct/2 + + : +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      [
+                      
+                      :title
+                      
+                      ,
+                      
+                      
+                      
+                      :body
+                      
+                      ]
+                      
+                    
+                  
+

Or even dynamically: +

+
+                    
+                      fields
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      [
+                      
+                      :title
+                      
+                      ,
+                      
+                      
+                      
+                      :body
+                      
+                      ]
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      ^
+                      
+                      fields
+                      
+                    
+                  
+

For preloads, the selected fields may be specified from the parent: +

+
+                    
+                      from
+                      
+                      (
+                      
+                      city
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      City
+                      
+                      ,
+                      
+                      
+                      
+                      preload
+                      
+                      :
+                      
+                      
+                      
+                      :country
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      struct
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :country_id
+                      
+                      ,
+                      
+                      
+                      
+                      :name
+                      
+                      ,
+                      
+                      
+                      
+                      country
+                      
+                      :
+                      
+                      
+                      
+                      [
+                      
+                      :id
+                      
+                      ,
+                      
+                      
+                      
+                      :population
+                      
+                      ]
+                      
+                      ]
+                      
+                      )
+                      
+                      )
+                      
+                    
+                  
+

If the same source is selected multiple times with a + struct + , +the fields are merged in order to avoid fetching multiple copies +from the database. In other words, the expression below: +

+
+                    
+                      from
+                      
+                      (
+                      
+                      city
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      City
+                      
+                      ,
+                      
+                      
+                      
+                      preload
+                      
+                      :
+                      
+                      
+                      
+                      :country
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      {
+                      
+                      struct
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :country_id
+                      
+                      ]
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      struct
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :name
+                      
+                      ]
+                      
+                      )
+                      
+                      }
+                      
+                      )
+                      
+                    
+                  
+

is expanded to: +

+
+                    
+                      from
+                      
+                      (
+                      
+                      city
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      City
+                      
+                      ,
+                      
+                      
+                      
+                      preload
+                      
+                      :
+                      
+                      
+                      
+                      :country
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      {
+                      
+                      struct
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :country_id
+                      
+                      ,
+                      
+                      
+                      
+                      :name
+                      
+                      ]
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      struct
+                      
+                      (
+                      
+                      city
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      :country_id
+                      
+                      ,
+                      
+                      
+                      
+                      :name
+                      
+                      ]
+                      
+                      )
+                      
+                      }
+                      
+                      )
+                      
+                    
+                  
+

+ IMPORTANT + : When filtering fields for associations, you +MUST include the foreign keys used in the relationship, +otherwise Ecto will be unable to find associated records. +

+
+
+
+ +
+

Calculates the sum for the given entry. +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Payment
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      sum
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      value
+                      
+                      )
+                      
+                    
+                  
+
+
+
+
+ + + Link to this function + + +

type(interpolated_value, type) +

+ + + View Source + + +
+
+

Casts the given value to the given type at the database level. +

+

Most of the times, Ecto is able to proper cast interpolated +values due to its type checking mechanism. In some situations +though, you may want to tell Ecto that a parameter has some +particular type: +

+
+                    
+                      type
+                      
+                      (
+                      
+                      ^
+                      
+                      title
+                      
+                      ,
+                      
+                      
+                      
+                      :string
+                      
+                      )
+                      
+                    
+                  
+

It is also possible to say the type must match the same of a column: +

+
+                    
+                      type
+                      
+                      (
+                      
+                      ^
+                      
+                      title
+                      
+                      ,
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      title
+                      
+                      )
+                      
+                    
+                  
+

Or a parameterized type, which must be previously initialized +with + + Ecto.ParameterizedType.init/2 + + : +

+
+                    
+                      @my_enum
+                      
+                      
+                      
+                      Ecto.ParameterizedType
+                      
+                      .
+                      
+                      init
+                      
+                      (
+                      
+                      Ecto.Enum
+                      
+                      ,
+                      
+                      
+                      
+                      values
+                      
+                      :
+                      
+                      
+                      
+                      [
+                      
+                      :foo
+                      
+                      ,
+                      
+                      
+                      
+                      :bar
+                      
+                      ,
+                      
+                      
+                      
+                      :baz
+                      
+                      ]
+                      
+                      )
+                      
+                      
+                      
+                      type
+                      
+                      (
+                      
+                      ^
+                      
+                      title
+                      
+                      ,
+                      
+                      
+                      
+                      ^
+                      
+                      @my_enum
+                      
+                      )
+                      
+                    
+                  
+

Ecto will ensure + ^title + is cast to the given type and enforce such +type at the database level. If the value is returned in a + select + , +Ecto will also enforce the proper type throughout. +

+

When performing arithmetic operations, + + type/2 + + can be used to cast +all the parameters in the operation to the same type: +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      type
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      visits
+                      
+                      
+                      
+                      +
+                      
+                      
+                      
+                      ^
+                      
+                      a_float
+                      
+                      
+                      
+                      +
+                      
+                      
+                      
+                      ^
+                      
+                      a_integer
+                      
+                      ,
+                      
+                      
+                      
+                      :decimal
+                      
+                      )
+                      
+                    
+                  
+

Inside + select + , + + type/2 + + can also be used to cast fragments: +

+
+                    
+                      type
+                      
+                      (
+                      
+                      fragment
+                      
+                      (
+                      
+                      "NOW"
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      :naive_datetime
+                      
+                      )
+                      
+                    
+                  
+

Or to type fields from schemaless queries: +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      "posts"
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      type
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      cost
+                      
+                      ,
+                      
+                      
+                      
+                      :decimal
+                      
+                      )
+                      
+                    
+                  
+

Or to type aggregation results: +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      type
+                      
+                      (
+                      
+                      avg
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      cost
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      :integer
+                      
+                      )
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      type
+                      
+                      (
+                      
+                      filter
+                      
+                      (
+                      
+                      avg
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      cost
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      cost
+                      
+                      
+                      
+                      >
+                      
+                      
+                      
+                      0
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      :integer
+                      
+                      )
+                      
+                    
+                  
+

Or to type comparison expression results: +

+
+                    
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      type
+                      
+                      (
+                      
+                      coalesce
+                      
+                      (
+                      
+                      p
+                      
+                      .
+                      
+                      cost
+                      
+                      ,
+                      
+                      
+                      
+                      0
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      :integer
+                      
+                      )
+                      
+                    
+                  
+

Or to type fields from a parent query using + + parent_as/1 + + : +

+
+                    
+                      child
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      c
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Comment
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      type
+                      
+                      (
+                      
+                      parent_as
+                      
+                      (
+                      
+                      :posts
+                      
+                      )
+                      
+                      .
+                      
+                      id
+                      
+                      ,
+                      
+                      
+                      
+                      :string
+                      
+                      )
+                      
+                      
+                      
+                      ==
+                      
+                      
+                      
+                      c
+                      
+                      .
+                      
+                      text
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      as
+                      
+                      :
+                      
+                      
+                      
+                      :posts
+                      
+                      ,
+                      
+                      
+                      
+                      inner_lateral_join
+                      
+                      :
+                      
+                      
+                      
+                      c
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      subquery
+                      
+                      (
+                      
+                      child
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      select
+                      
+                      :
+                      
+                      
+                      
+                      c
+                      
+                      .
+                      
+                      text
+                      
+                    
+                  
+

+ + + + + type + vs + fragment + + +

+

+ + type/2 + + is all about Ecto types. Therefore, you can perform + type(expr, :string) + + but not + type(expr, :text) + , because + :text + is not an actual Ecto type. If you want +to perform casting exclusively at the database level, you can use fragment. For example, +in PostgreSQL, you might do + fragment("?::text", p.column) + . +

+
+
+
+ +
+

Creates a values list/constant table. +

+

A values list can be used as a source in a query, both in + + Ecto.Query.from/2 + + + and + + Ecto.Query.join/5 + + . +

+

The first argument is a list of maps representing the values of the constant table. +Each entry in the list must have exactly the same fields or an error is raised. +

+

The second argument is a map of types corresponding to the fields in the first argument. +Each field must be given a type or an error is raised. Any type that can be specified in +a schema may be used. +

+

Queries using a values list are not cacheable by Ecto. +

+

+ + + + Select example + +

+
+                    
+                      values
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      [
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      1
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      "abc"
+                      
+                      }
+                      
+                      ,
+                      
+                      
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      2
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      "xyz"
+                      
+                      }
+                      
+                      ]
+                      
+                      
+                      
+                      types
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      :integer
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      :string
+                      
+                      }
+                      
+                      
+                      
+                      query
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      v1
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      values
+                      
+                      (
+                      
+                      values
+                      
+                      ,
+                      
+                      
+                      
+                      types
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      join
+                      
+                      :
+                      
+                      
+                      
+                      v2
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      values
+                      
+                      (
+                      
+                      values
+                      
+                      ,
+                      
+                      
+                      
+                      types
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      on
+                      
+                      :
+                      
+                      
+                      
+                      v1
+                      
+                      .
+                      
+                      id
+                      
+                      
+                      
+                      ==
+                      
+                      
+                      
+                      v2
+                      
+                      .
+                      
+                      id
+                      
+                      
+                      
+                      Repo
+                      
+                      .
+                      
+                      all
+                      
+                      (
+                      
+                      query
+                      
+                      )
+                      
+                    
+                  
+

+ + + + Delete example + +

+
+                    
+                      values
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      [
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      1
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      "abc"
+                      
+                      }
+                      
+                      ,
+                      
+                      
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      2
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      "xyz"
+                      
+                      }
+                      
+                      ]
+                      
+                      
+                      
+                      types
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      :integer
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      :string
+                      
+                      }
+                      
+                      
+                      
+                      query
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      join
+                      
+                      :
+                      
+                      
+                      
+                      v
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      values
+                      
+                      (
+                      
+                      values
+                      
+                      ,
+                      
+                      
+                      
+                      types
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      on
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      id
+                      
+                      
+                      
+                      ==
+                      
+                      
+                      
+                      v
+                      
+                      .
+                      
+                      id
+                      
+                      ,
+                      
+                      
+                      
+                      where
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      counter
+                      
+                      
+                      
+                      ==
+                      
+                      
+                      
+                      ^
+                      
+                      0
+                      
+                      
+                      
+                      Repo
+                      
+                      .
+                      
+                      delete_all
+                      
+                      (
+                      
+                      query
+                      
+                      )
+                      
+                    
+                  
+

+ + + + Update example + +

+
+                    
+                      values
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      [
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      1
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      "abc"
+                      
+                      }
+                      
+                      ,
+                      
+                      
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      2
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      "xyz"
+                      
+                      }
+                      
+                      ]
+                      
+                      
+                      
+                      types
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      %{
+                      
+                      id
+                      
+                      :
+                      
+                      
+                      
+                      :integer
+                      
+                      ,
+                      
+                      
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      :string
+                      
+                      }
+                      
+                      
+                      
+                      query
+                      
+                      
+                      
+                      =
+                      
+                      
+                      
+                      from
+                      
+                      
+                      
+                      p
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      Post
+                      
+                      ,
+                      
+                      
+                      
+                      join
+                      
+                      :
+                      
+                      
+                      
+                      v
+                      
+                      
+                      
+                      in
+                      
+                      
+                      
+                      values
+                      
+                      (
+                      
+                      values
+                      
+                      ,
+                      
+                      
+                      
+                      types
+                      
+                      )
+                      
+                      ,
+                      
+                      
+                      
+                      on
+                      
+                      :
+                      
+                      
+                      
+                      p
+                      
+                      .
+                      
+                      id
+                      
+                      
+                      
+                      ==
+                      
+                      
+                      
+                      v
+                      
+                      .
+                      
+                      id
+                      
+                      ,
+                      
+                      
+                      
+                      update
+                      
+                      :
+                      
+                      
+                      
+                      [
+                      
+                      set
+                      
+                      :
+                      
+                      
+                      
+                      [
+                      
+                      text
+                      
+                      :
+                      
+                      
+                      
+                      v
+                      
+                      .
+                      
+                      text
+                      
+                      ]
+                      
+                      ]
+                      
+                      
+                      
+                      Repo
+                      
+                      .
+                      
+                      update_all
+                      
+                      (
+                      
+                      query
+                      
+                      ,
+                      
+                      
+                      
+                      [
+                      
+                      ]
+                      
+                      )
+                      
+                    
+                  
+
+
+
+
+ +
+
+
+ + + + diff --git a/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.QueryError.html b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.QueryError.html new file mode 100644 index 0000000..f5e4b03 --- /dev/null +++ b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--Ecto.QueryError.html @@ -0,0 +1,43 @@ +Ecto.QueryError — Ecto v3.12.4

View SourceEcto.QueryErrorexception(Ecto v3.12.4)

Raised at runtime when the query is invalid.

\ No newline at end of file diff --git a/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--crud.html b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--crud.html new file mode 100644 index 0000000..6780457 --- /dev/null +++ b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--crud.html @@ -0,0 +1,371 @@ + + + + + + + + + + + Basic CRUD — Ecto v3.12.4 + + + + + + + + + + + + + + + +
+ + + + + +
+ + +
+ + +
+

+ + + + View Source + + + + + + Basic CRUD +

+ +

In this document, "Internal data" represents data or logic hardcoded into your Elixir code. "External data" means data that comes from the user via forms, APIs, and often need to be normalized, pruned, and validated via Ecto.Changeset.

+ + + + Fetching records +

+

+ + + + Single record +

+

Fetching record by ID

Repo.get(Movie, 1)

Fetching record by attributes

Repo.get_by(Movie, title: "Ready Player One")

Fetching the first record

Movie |> Ecto.Query.first() |> Repo.one()

Fetching the last record

Movie |> Ecto.Query.last() |> Repo.one()

Use ! to raise if none is found

Repo.get!(Movie, 1)
+Repo.get_by!(Movie, title: "Ready Player One")
+Movie |> Ecto.Query.first() |> Repo.one!()

+ + + + Multiple records +

+

Fetch all at once

Movie |> Repo.all()

Stream all

Movie |> Repo.stream() |> Enum.each(fn record -> ... end)

Check at least one exists?

Movie |> Repo.exists?()

+ + + + Querying records +

+

+ + + + Keyword-based queries +

+

Bindingless queries

query =
+  from Movie,
+  where: [title: "Ready Player One"],
+  select: [:title, :tagline]
+Repo.all(query)

Bindings in queries

query =
+  from m in Movie,
+  where: m.title == "Ready Player One",
+  select: [m.title, m.tagline]
+Repo.all(query)

+ + + + Interpolation with ^ +

+
title = "Ready Player One"
+query =
+  from m in Movie,
+  where: m.title == ^title,
+  select: [m.title, m.tagline]
+Repo.all(query)

+ + + + Pipe-based queries +

+
Movie
+|> where([m], m.title == "Ready Player One")
+|> select([m], {m.title, m.tagline})
+|> Repo.all

+ + + + Inserting records +

+

+ + + + Single record +

+

Using internal data

%Person{name: "Bob"}
+|> Repo.insert()

Using external data

# Params represent data from a form, API, CLI, etc
+params = %{"name" => "Bob"}
+
+%Person{}
+|> Ecto.Changeset.cast(params, [:name])
+|> Repo.insert()

+ + + + Multiple records +

+
data = [%{name: "Bob"}, %{name: "Alice"}]
+Repo.insert_all(Person, data)

+ + + + Updating records +

+

+ + + + Single record +

+

Using internal data

person =
+  Person
+  |> Ecto.Query.first()
+  |> Repo.one!()
+
+changeset = change(person, %{age: 29})
+Repo.update(changeset)

Using external data

# Params represent data from a form, API, CLI, etc
+params = %{"age" => "29"}
+
+person =
+  Person
+  |> Ecto.Query.first()
+  |> Repo.one!()
+
+changeset = cast(person, params, [:age])
+Repo.update(changeset)

+ + + + Multiple records (using queries) +

+
Repo.update_all(Person, set: [age: 29])

+ + + + Deleting records +

+

+ + + + Single record +

+
person = Repo.get!(Person, 1)
+Repo.delete(person)

+ + + + Multiple records (using queries) +

+
Repo.delete_all(Person)
+
+ + + +
+
+
+ + + + + diff --git a/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--getting-started.html b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--getting-started.html new file mode 100644 index 0000000..457bea2 --- /dev/null +++ b/test/support/fixtures/exdoc-0.34.2-ecto-3.12.4--getting-started.html @@ -0,0 +1,82 @@ +Getting Started — Ecto v3.12.4

View SourceGetting Started

This guide is an introduction to Ecto, +the database wrapper and query generator for Elixir. Ecto provides a +standardized API and a set of abstractions for talking to all the different +kinds of databases, so that Elixir developers can query whatever database +they're using by employing similar constructs.

In this guide, we're going to learn some basics about Ecto, such as creating, +reading, updating and destroying records from a PostgreSQL database. If you want +to see the code from this guide, you can view it at ecto/examples/friends on GitHub.

This guide will require you to have setup PostgreSQL beforehand.

Adding Ecto to an application

To start off with, we'll generate a new Elixir application by running this command:

mixnewfriends--sup

The --sup option ensures that this application has a supervision tree, which we'll need for Ecto a little later on.

To add Ecto to this application, there are a few steps that we need to take. The first step will be adding Ecto and a driver called Postgrex to our mix.exs file, which we'll do by changing the deps definition in that file to this:

defpdepsdo[{:ecto_sql,"~> 3.0"},{:postgrex,">= 0.0.0"}]end

Ecto provides the common querying API, but we need the Postgrex driver installed too, as that is what Ecto uses to speak in terms a PostgreSQL database can understand. Ecto talks to its own Ecto.Adapters.Postgres module, which then in turn talks to the postgrex package to talk to PostgreSQL.

To install these dependencies, we will run this command:

mixdeps.get

The Postgrex application will receive queries from Ecto and execute them +against our database. If we didn't do this step, we wouldn't be able to do any +querying at all.

That's the first two steps taken now. We have installed Ecto and Postgrex as +dependencies of our application. We now need to setup some configuration for +Ecto so that we can perform actions on a database from within the +application's code.

We can set up this configuration by running this command:

mixecto.gen.repo-rFriends.Repo

This command will generate the configuration required to connect to a database. The first bit of configuration is in config/config.exs:

config:friends,Friends.Repo,database:"friends",username:"user",password:"pass",hostname:"localhost"

NOTE: Your PostgreSQL database may be setup to

  • not require a username and password. If the above configuration doesn't work, try removing the username and password fields, or setting them both to "postgres".
  • be running on a non-standard port. The default port is 5432. You can specify your specific port by adding it to the config: e.g. port: 15432.

This configures how Ecto will connect to our database, called "friends". Specifically, it configures a "repo". More information about Ecto.Repo can be found in its documentation.

The Friends.Repo module is defined in lib/friends/repo.ex by our mix ecto.gen.repo command:

defmoduleFriends.RepodouseEcto.Repo,otp_app::friends,adapter:Ecto.Adapters.Postgresend

This module is what we'll be using to query our database shortly. It uses the Ecto.Repo module, and the otp_app tells Ecto which Elixir application it can look for database configuration in. In this case, we've specified that it is the :friends application where Ecto can find that configuration and so Ecto will use the configuration that was set up in config/config.exs. Finally, we configure the database :adapter to Postgres.

Finally, the Friends.Repo must be started within the application's supervision tree, which we can do in lib/friends/application.ex, inside the start/2 function:

defstart(_type,_args)dochildren=[Friends.Repo,]...

This piece of configuration will start the Ecto process which receives and executes our application's queries. Without it, we wouldn't be able to query the database at all!

There's one final bit of configuration that we'll need to add ourselves, since the generator does not add it. Underneath the configuration in config/config.exs, add this line:

config:friends,ecto_repos:[Friends.Repo]

This tells our application about the repo, which will allow us to run commands such as mix ecto.create very soon.

We've now configured our application so that it's able to make queries to our database. Let's now create our database, add a table to it, and then perform some queries.

Setting up the database

To be able to query a database, it first needs to exist. We can create the database with this command:

mixecto.create

If the database has been created successfully, then you will see this message:

ThedatabaseforFriends.Repohasbeencreated.

NOTE: If you get an error, you should try changing your configuration in config/config.exs, as it may be an authentication error.

A database by itself isn't very queryable, so we will need to create a table within that database. To do that, we'll use what's referred to as a migration. If you've come from Active Record (or similar), you will have seen these before. A migration is a single step in the process of constructing your database.

Let's create a migration now with this command:

mixecto.gen.migrationcreate_people

This command will generate a brand new migration file in priv/repo/migrations, which is empty by default:

defmoduleFriends.Repo.Migrations.CreatePeopledouseEcto.Migrationdefchangedoendend

Let's add some code to this migration to create a new table called "people", with a few columns in it:

defmoduleFriends.Repo.Migrations.CreatePeopledouseEcto.Migrationdefchangedocreatetable(:people)doadd:first_name,:stringadd:last_name,:stringadd:age,:integerendendend

This new code will tell Ecto to create a new table called people, and add three new fields: first_name, last_name and age to that table. The types of these fields are string and integer. (The different types that Ecto supports are covered in the Ecto.Schema documentation.)

NOTE: The naming convention for tables in Ecto databases is to use a pluralized name.

To run this migration and create the people table in our database, we will run this command:

mixecto.migrate

If we found out that we made a mistake in this migration, we could run mix ecto.rollback to undo the changes in the migration. We could then fix the changes in the migration and run mix ecto.migrate again. If we ran mix ecto.rollback now, it would delete the table that we just created.

We now have a table created in our database. The next step that we'll need to do is to create the schema.

Creating the schema

The schema is an Elixir representation of data from our database. Schemas are commonly associated with a database table, however they can be associated with a database view as well.

Let's create the schema within our application at lib/friends/person.ex:

defmoduleFriends.PersondouseEcto.Schemaschema"people"dofield:first_name,:stringfield:last_name,:stringfield:age,:integerendend

This defines the schema from the database that this schema maps to. In this case, we're telling Ecto that the Friends.Person schema maps to the people table in the database, and the first_name, last_name and age fields in that table. The second argument passed to field tells Ecto how we want the information from the database to be represented in our schema.

We've called this schema Person because the naming convention in Ecto for schemas is a singularized name.

We can play around with this schema in an IEx session by starting one up with iex -S mix and then running this code in it:

person=%Friends.Person{}

This code will give us a new Friends.Person struct, which will have nil values for all the fields. We can set values on these fields by generating a new struct:

person=%Friends.Person{age:28}

Or with syntax like this:

person=%{person|age:28}

We can retrieve values using this syntax:

person.age# => 28

Let's take a look at how we can insert data into the database.

Inserting data

We can insert a new record into our people table with this code:

person=%Friends.Person{}Friends.Repo.insert(person)

To insert the data into our database, we call insert on Friends.Repo, which is the module that uses Ecto to talk to our database. This function tells Ecto that we want to insert a new Friends.Person record into the database corresponding with Friends.Repo. The person struct here represents the data that we want to insert into the database.

A successful insertion will return a tuple, like so:

{:ok,%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:nil,first_name:nil,id:1,last_name:nil}}

The :ok atom can be used for pattern matching purposes to ensure that the insertion succeeds. A situation where the insertion may not succeed is if you have a constraint on the database itself. For instance, if the database had a unique constraint on a field called email so that an email can only be used for one person record, then the insertion would fail.

You may wish to pattern match on the tuple in order to refer to the record inserted into the database:

{:ok,person}=Friends.Repo.insertperson

Validating changes

In Ecto, you may wish to validate changes before they go to the database. For instance, you may wish that a person has both a first name and a last name before a record can be entered into the database. For this, Ecto has changesets.

Let's add a changeset to our Friends.Person module inside lib/friends/person.ex now:

defchangeset(person,params\\%{})doperson|>Ecto.Changeset.cast(params,[:first_name,:last_name,:age])|>Ecto.Changeset.validate_required([:first_name,:last_name])end

This changeset takes a person and a set of params, which are to be the changes to apply to this person. The changeset function first casts the first_name, last_name and age keys from the parameters passed in to the changeset. Casting tells the changeset what parameters are allowed to be passed through in this changeset, and anything not in the list will be ignored.

On the next line, we call validate_required which says that, for this changeset, we expect first_name and last_name to have values specified. Let's use this changeset to attempt to create a new record without a first_name and last_name:

person=%Friends.Person{}changeset=Friends.Person.changeset(person,%{})Friends.Repo.insert(changeset)

On the first line here, we get a struct from the Friends.Person module. We know what that does, because we saw it not too long ago. On the second line we do something brand new: we define a changeset. This changeset says that on the specified person object, we're looking to make some changes. In this case, we're not looking to change anything at all.

On the final line, rather than inserting the person, we insert the changeset. The changeset knows about the person, the changes and the validation rules that must be met before the data can be entered into the database. When this third line runs, we'll see this:

{:error,#Ecto.Changeset<action::insert,changes:%{},errors:[first_name:"can't be blank",last_name:"can't be blank"],data:#Friends.Person<>,valid?:false>}

Just like the last time we did an insertion, this returns a tuple. This time however, the first element in the tuple is :error, which indicates something bad happened. The specifics of what happened are included in the changeset which is returned. We can access these by doing some pattern matching:

{:error,changeset}=Friends.Repo.insert(changeset)

Then we can get to the errors by doing changeset.errors:

[first_name:{"can't be blank",[validation::required]},last_name:{"can't be blank",[validation::required]}]

And we can ask the changeset itself if it is valid, even before doing an insertion:

changeset.valid?#=> false

Since this changeset has errors, no new record was inserted into the people +table.

Let's try now with some valid data.

person=%Friends.Person{}changeset=Friends.Person.changeset(person,%{first_name:"Ryan",last_name:"Bigg"})

We start out here with a normal Friends.Person struct. We then create a changeset for that person which has a first_name and a last_name parameter specified. At this point, we can ask the changeset if it has errors:

changeset.errors#=> []

And we can ask if it's valid or not:

changeset.valid?#=> true

The changeset does not have errors, and is valid. Therefore if we try to insert this changeset it will work:

Friends.Repo.insert(changeset)#=> {:ok,%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:nil,first_name:"Ryan",id:3,last_name:"Bigg"}}

Due to Friends.Repo.insert returning a tuple, we can use a case to determine different code paths depending on what happens:

caseFriends.Repo.insert(changeset)do{:ok,person}-># do something with person{:error,changeset}-># do something with changesetend

NOTE:changeset.valid? will not check constraints (such as uniqueness_constraint). For that, you will need to attempt to do an insertion and check for errors from the database. It's for this reason it's best practice to try inserting data and validate the returned tuple from Friends.Repo.insert to get the correct errors, as prior to insertion the changeset will only contain validation errors from the application itself.

If the insertion of the changeset succeeds, then you can do whatever you wish with the person returned in that result. If it fails, then you have access to the changeset and its errors. In the failure case, you may wish to present these errors to the end user. The errors in the changeset are a keyword list that looks like this:

[first_name:{"can't be blank",[validation::required]},last_name:{"can't be blank",[validation::required]}]

The first element of the tuple is the validation message, and the second element is a keyword list of options for the validation message. Imagine that we had a field called bio that we were validating, and that field has to be longer than 15 characters. This is what would be returned:

[first_name:{"can't be blank",[validation::required]},last_name:{"can't be blank",[validation::required]},bio:{"should be at least %{count} character(s)",[count:15,validation::length,kind::min,type::string]}]

To display these error messages in a human friendly way, we can use Ecto.Changeset.traverse_errors/2:

traverse_errors(changeset,fn{msg,opts}->Enum.reduce(opts,msg,fn{key,value},acc->String.replace(acc,"%{#{key}}",to_string(value))end)end)

This will return the following for the errors shown above:

%{first_name:["can't be blank"],last_name:["can't be blank"],bio:["should be at least 15 character(s)"],}

One more final thing to mention here: you can trigger an exception to be thrown by using Friends.Repo.insert!/2. If a changeset is invalid, you will see an Ecto.InvalidChangesetError exception. Here's a quick example of that:

Friends.Repo.insert!Friends.Person.changeset(%Friends.Person{},%{first_name:"Ryan"})** (Ecto.InvalidChangesetError) could not perform insert because changeset is invalid.Errors%{last_name:[{"can't be blank",[validation::required]}]}Appliedchanges%{first_name:"Ryan"}Params%{"first_name"=>"Ryan"}Changeset#Ecto.Changeset<action::insert,changes:%{first_name:"Ryan"},errors:[last_name:{"can't be blank",[validation::required]}],data:#Friends.Person<>,valid?:false>(ecto)lib/ecto/repo/schema.ex:257:Ecto.Repo.Schema.insert!/4

This exception shows us the changes from the changeset, and how the changeset is invalid. This can be useful if you want to insert a bunch of data and then have an exception raised if that data is not inserted correctly at all.

Now that we've covered inserting data into the database, let's look at how we can pull that data back out.

Our first queries

Querying a database requires two steps in Ecto. First, we must construct the query and then we must execute that query against the database by passing the query to the repository. Before we do this, let's re-create the database for our app and setup some test data. To re-create the database, we'll run these commands:

mixecto.dropmixecto.createmixecto.migrate

Then to create the test data, we'll run this in an iex -S mix session:

people=[%Friends.Person{first_name:"Ryan",last_name:"Bigg",age:28},%Friends.Person{first_name:"John",last_name:"Smith",age:27},%Friends.Person{first_name:"Jane",last_name:"Smith",age:26},]Enum.each(people,fn(person)->Friends.Repo.insert(person)end)

This code will create three new people in our database, Ryan, John and Jane. Note here that we could've used a changeset to validate the data going into the database, but the choice was made not to use one.

We'll be querying for these people in this section. Let's jump in!

Fetching a single record

Let's start off with fetching just one record from our people table:

Friends.Person|>Ecto.Query.first

That code will generate an Ecto.Query, which will be this:

#Ecto.Query<fromp0inFriends.Person,order_by:[asc:p0.id],limit:1>

The code between the angle brackets <...> here shows the Ecto query which has been constructed. We could construct this query ourselves with almost exactly the same syntax:

requireEcto.QueryEcto.Query.frompinFriends.Person,order_by:[asc:p.id],limit:1

We need to require Ecto.Query here to enable the macros from that module. Then it's a matter of calling the from function from Ecto.Query and passing in the code from between the angle brackets. As we can see here, Ecto.Query.first saves us from having to specify the order and limit for the query.

To execute the query that we've just constructed, we can call Friends.Repo.one:

Friends.Person|>Ecto.Query.first|>Friends.Repo.one

The one function retrieves just one record from our database and returns a new struct from the Friends.Person module:

%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:28,first_name:"Ryan",id:1,last_name:"Bigg"}

Similar to first, there is also last:

Friends.Person|>Ecto.Query.last|>Friends.Repo.one#=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 26,first_name:"Jane",id:3,last_name:"Smith"}

The Ecto.Repo.one function will only return a struct if there is one record in the +result from the database. If there is more than one record returned, an +Ecto.MultipleResultsError exception will be thrown. Some code that would +cause that issue to happen is:

Friends.Person|>Friends.Repo.one

We've left out the Ecto.Query.first here, and so there is no limit or order clause applied to the executed query. We'll see the executed query in the debug log:

[timestamp][debug]SELECTp0."id",p0."first_name",p0."last_name",p0."age"FROM"people"ASp0[]OKquery=1.8ms

Then immediately after that, we will see the Ecto.MultipleResultsError exception:

** (Ecto.MultipleResultsError) expected at most one result but got 3 in query:frompinFriends.Personlib/ecto/repo/queryable.ex:67:Ecto.Repo.Queryable.one/4

This happens because Ecto doesn't know what one record out of all the records +returned that we want. Ecto will only return a result if we are explicit in +our querying about which result we want.

If there is no record which matches the query, one will return nil.

Fetching all records

To fetch all records from the schema, Ecto provides the all function:

Friends.Person|>Friends.Repo.all

This will return a Friends.Person struct representation of all the records that currently exist within our people table:

[%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:28,first_name:"Ryan",id:1,last_name:"Bigg"},%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:27,first_name:"John",id:2,last_name:"Smith"},%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:26,first_name:"Jane",id:3,last_name:"Smith"}]

Fetch a single record based on ID

To fetch a record based on its ID, you use the get function:

Friends.Person|>Friends.Repo.get(1)#=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 28,first_name:"Ryan",id:1,last_name:"Bigg"}

Fetch a single record based on a specific attribute

If we want to get a record based on something other than the id attribute, we can use get_by:

Friends.Person|>Friends.Repo.get_by(first_name:"Ryan")#=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 28,first_name:"Ryan",id:1,last_name:"Bigg"}

Filtering results

If we want to get multiple records matching a specific attribute, we can use where:

Friends.Person|>Ecto.Query.where(last_name:"Smith")|>Friends.Repo.all
[%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:27,first_name:"John",id:2,last_name:"Smith"},%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:26,first_name:"Jane",id:3,last_name:"Smith"}]

If we leave off the Friends.Repo.all on the end of this, we will see the query Ecto generates:

#Ecto.Query<frompinFriends.Person,where:p.last_name=="Smith">

We can also use this query syntax to fetch these same records:

Ecto.Query.from(pinFriends.Person,where:p.last_name=="Smith")|>Friends.Repo.all

One important thing to note with both query syntaxes is that they require variables to be pinned, using the pin operator (^). Otherwise, this happens:

last_name="Smith"Friends.Person|>Ecto.Query.where(last_name:last_name)|>Friends.Repo.all
** (Ecto.Query.CompileError) unbound variable `last_name` in query. If you are attempting to interpolate a value, use ^var
+    (ecto)   expanding macro: Ecto.Query.where/2
+             iex:15: (file)
+    (elixir) expanding macro: Kernel.|>/2
+             iex:15: (file)

The same will happen in the longer query syntax too:

Ecto.Query.from(pinFriends.Person,where:p.last_name==last_name)|>Friends.Repo.all
** (Ecto.Query.CompileError) unbound variable `last_name` in query. If you are attempting to interpolate a value, use ^var
+    (ecto)   expanding macro: Ecto.Query.where/3
+             iex:15: (file)
+    (ecto)   expanding macro: Ecto.Query.from/2
+             iex:15: (file)
+    (elixir) expanding macro: Kernel.|>/2
+             iex:15: (file)

To get around this, we use the pin operator (^):

last_name="Smith"Friends.Person|>Ecto.Query.where(last_name:^last_name)|>Friends.Repo.all

Or:

last_name="Smith"Ecto.Query.from(pinFriends.Person,where:p.last_name==^last_name)|>Friends.Repo.all

The pin operator instructs the query builder to use parameterized SQL queries protecting against SQL injection.

Composing Ecto queries

Ecto queries don't have to be built in one spot. They can be built up by calling Ecto.Query functions on existing queries. For instance, if we want to find all people with the last name "Smith", we can do:

query=Friends.Person|>Ecto.Query.where(last_name:"Smith")

If we want to scope this down further to only people with the first name of "Jane", we can do this:

query=query|>Ecto.Query.where(first_name:"Jane")

Our query will now have two where clauses in it:

#Ecto.Query<frompinFriends.Person,where:p.last_name=="Smith",where:p.first_name=="Jane">

This can be useful if you want to do something with the first query, and then build off that query later on.

Updating records

Updating records in Ecto requires us to first fetch a record from the database. We then create a changeset from that record and the changes we want to make to that record, and then call the Ecto.Repo.update function.

Let's fetch the first person from our database and change their age. First, we'll fetch the person:

person=Friends.Person|>Ecto.Query.first|>Friends.Repo.one

Next, we'll build a changeset. We need to build a changeset because if we just create a new Friends.Person struct with the new age, Ecto wouldn't be able to know that the age has changed without inspecting the database. Let's build that changeset:

changeset=Friends.Person.changeset(person,%{age:29})

This changeset will inform the database that we want to update the record to have the age set to 29. To tell the database about the change we want to make, we run this command:

Friends.Repo.update(changeset)

Just like Friends.Repo.insert, Friends.Repo.update will return a tuple:

{:ok,%Friends.Person{__meta__:#Ecto.Schema.Metadata<:loaded,"people">,age:29,first_name:"Ryan",id:1,last_name:"Bigg"}}

If the changeset fails for any reason, the result of Friends.Repo.update will be {:error, changeset}. We can see this in action by passing through a blank first_name in our changeset's parameters:

changeset=Friends.Person.changeset(person,%{first_name:""})Friends.Repo.update(changeset)#=> {:error,#Ecto.Changeset<action::update,changes:%{},errors:[first_name:{"can't be blank",[validation::required]}],data:#Friends.Person<>,valid?:false>}

This means that you can also use a case statement to do different things depending on the outcome of the update function:

caseFriends.Repo.update(changeset)do{:ok,person}-># do something with person{:error,changeset}-># do something with changesetend

Similar to insert!, there is also update! which will raise an exception if the changeset is invalid:

changeset=Friends.Person.changeset(person,%{first_name:""})Friends.Repo.update!changeset** (Ecto.InvalidChangesetError) could not perform update because changeset is invalid.Errors%{first_name:[{"can't be blank",[validation::required]}]}Appliedchanges%{}Params%{"first_name"=>""}Changeset#Ecto.Changeset<action::update,changes:%{},errors:[first_name:{"can't be blank",[validation::required]}],data:#Friends.Person<>,valid?:false>(ecto)lib/ecto/repo/schema.ex:270:Ecto.Repo.Schema.update!/4

Deleting records

We've now covered creating (insert), reading (get, get_by, where) and updating records. The last thing that we'll cover in this guide is how to delete a record using Ecto.

Similar to updating, we must first fetch a record from the database and then call Friends.Repo.delete to delete that record:

person=Friends.Repo.get(Friends.Person,1)Friends.Repo.delete(person)#=> {:ok,%Friends.Person{__meta__:#Ecto.Schema.Metadata<:deleted,"people">,age:29,first_name:"Ryan",id:2,last_name:"Bigg"}}

Similar to insert and update, delete returns a tuple. If the deletion succeeds, then the first element in the tuple will be :ok, but if it fails then it will be an :error.

\ No newline at end of file diff --git a/test/support/model_case.ex b/test/support/model_case.ex deleted file mode 100644 index 8b032fa..0000000 --- a/test/support/model_case.ex +++ /dev/null @@ -1,65 +0,0 @@ -defmodule DocsetApi.ModelCase do - @moduledoc """ - This module defines the test case to be used by - model tests. - - You may define functions here to be used as helpers in - your model tests. See `errors_on/2`'s definition as reference. - - Finally, if the test case interacts with the database, - it cannot be async. For this reason, every test runs - inside a transaction which is reset at the beginning - of the test unless the test case is marked as async. - """ - - use ExUnit.CaseTemplate - - using do - quote do - alias DocsetApi.Repo - - import Ecto - import Ecto.Changeset - import Ecto.Query - import DocsetApi.ModelCase - end - end - - setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(DocsetApi.Repo) - - unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(DocsetApi.Repo, {:shared, self()}) - end - - :ok - end - - @doc """ - Helper for returning list of errors in a struct when given certain data. - - ## Examples - - Given a User schema that lists `:name` as a required field and validates - `:password` to be safe, it would return: - - iex> errors_on(%User{}, %{password: "password"}) - [password: "is unsafe", name: "is blank"] - - You could then write your assertion like: - - assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) - - You can also create the changeset manually and retrieve the errors - field directly: - - iex> changeset = User.changeset(%User{}, password: "password") - iex> {:password, "is unsafe"} in changeset.errors - true - """ - def errors_on(struct, data) do - struct.__struct__.changeset(struct, data) - |> Ecto.Changeset.traverse_errors(&DocsetApi.ErrorHelpers.translate_error/1) - |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) - end -end diff --git a/test/test_helper.exs b/test/test_helper.exs index 2cd7d26..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,4 +1 @@ -ExUnit.start - -Ecto.Adapters.SQL.Sandbox.mode(DocsetApi.Repo, :manual) - +ExUnit.start() diff --git a/test/views/error_view_test.exs b/test/views/error_view_test.exs deleted file mode 100644 index 3308eb1..0000000 --- a/test/views/error_view_test.exs +++ /dev/null @@ -1,21 +0,0 @@ -defmodule DocsetApi.ErrorViewTest do - use DocsetApi.ConnCase, async: true - - # Bring render/3 and render_to_string/3 for testing custom views - import Phoenix.View - - test "renders 404.html" do - assert render_to_string(DocsetApi.ErrorView, "404.html", []) == - "Page not found" - end - - test "render 500.html" do - assert render_to_string(DocsetApi.ErrorView, "500.html", []) == - "Internal server error" - end - - test "render any other" do - assert render_to_string(DocsetApi.ErrorView, "505.html", []) == - "Internal server error" - end -end diff --git a/test/views/layout_view_test.exs b/test/views/layout_view_test.exs deleted file mode 100644 index b5a38bd..0000000 --- a/test/views/layout_view_test.exs +++ /dev/null @@ -1,3 +0,0 @@ -defmodule DocsetApi.LayoutViewTest do - use DocsetApi.ConnCase, async: true -end diff --git a/test/views/page_view_test.exs b/test/views/page_view_test.exs deleted file mode 100644 index c2ef935..0000000 --- a/test/views/page_view_test.exs +++ /dev/null @@ -1,3 +0,0 @@ -defmodule DocsetApi.PageViewTest do - use DocsetApi.ConnCase, async: true -end diff --git a/web/channels/user_socket.ex b/web/channels/user_socket.ex deleted file mode 100644 index 3beb9b3..0000000 --- a/web/channels/user_socket.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule DocsetApi.UserSocket do - use Phoenix.Socket - - ## Channels - # channel "room:*", DocsetApi.RoomChannel - - # Socket params are passed from the client and can - # be used to verify and authenticate a user. After - # verification, you can put default assigns into - # the socket that will be set for all channels, ie - # - # {:ok, assign(socket, :user_id, verified_user_id)} - # - # To deny connection, return `:error`. - # - # See `Phoenix.Token` documentation for examples in - # performing token verification on connect. - def connect(_params, socket) do - {:ok, socket} - end - - # Socket id's are topics that allow you to identify all sockets for a given user: - # - # def id(socket), do: "users_socket:#{socket.assigns.user_id}" - # - # Would allow you to broadcast a "disconnect" event and terminate - # all active sockets and channels for a given user: - # - # DocsetApi.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) - # - # Returning `nil` makes this socket anonymous. - def id(_socket), do: nil -end diff --git a/web/controllers/feed_controller.ex b/web/controllers/feed_controller.ex deleted file mode 100644 index e659a83..0000000 --- a/web/controllers/feed_controller.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule DocsetApi.FeedController do - use DocsetApi.Web, :controller - alias DocsetApi.BuilderServer - - def show(conn, %{"package_name" => package}) do - release = - BuilderServer.fetch_package( - package, - Path.absname("priv/static/docsets/#{package}.tgz")) - - render conn, "show.xml", release: release - end - -end diff --git a/web/router.ex b/web/router.ex deleted file mode 100644 index 207b45b..0000000 --- a/web/router.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule DocsetApi.Router do - use DocsetApi.Web, :router - - pipeline :api do - plug :accepts, ["xml"] - end - - scope "/feeds", DocsetApi do - pipe_through :api - get "/:package_name", FeedController, :show - end -end diff --git a/web/templates/feed/show.xml.eex b/web/templates/feed/show.xml.eex deleted file mode 100644 index 98c80f1..0000000 --- a/web/templates/feed/show.xml.eex +++ /dev/null @@ -1,4 +0,0 @@ - - <%= @release.version %> - <%= base_url() %>/docsets/<%= @release.name %>.tgz -