diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..470afcf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: Tests & Linting + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name || github.run_id }} + cancel-in-progress: true + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + rubocop: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Rubocop check + run: bundle exec rubocop --cache true --display-only-fail-level-offenses --extra-details + brakeman: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Rubocop check + run: bundle exec brakeman --no-exit-on-warn + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Run tests + run: bundle exec rspec --backtrace lib/**/*_spec.rb diff --git a/.ruby-version b/.ruby-version index c0013a8..a9cadc6 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-2.7.3 +ruby-3.2.0 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..0b2d858 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.1.2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f819a51..0000000 --- a/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: ruby diff --git a/CONDUCT.md b/CONDUCT.md index 151c6e2..45d257b 100644 --- a/CONDUCT.md +++ b/CONDUCT.md @@ -1,73 +1,133 @@ + # Contributor Covenant Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at kurtis@rainbolt-greene.online. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2773b48 --- /dev/null +++ b/LICENSE @@ -0,0 +1,420 @@ +HIPPOCRATIC LICENSE + +Version 3.0, October 2021 + +https://firstdonoharm.dev/version/3/0/bod-cl-eco-extr-ffd-law-media-mil-my-soc-sup-sv-tal-xuar.txt + +TERMS AND CONDITIONS + +TERMS AND CONDITIONS FOR USE, COPY, MODIFICATION, PREPARATION OF DERIVATIVE +WORK, REPRODUCTION, AND DISTRIBUTION: + +1. DEFINITIONS: + +This section defines certain terms used throughout this license agreement. + +1.1. “License” means the terms and conditions, as stated herein, for use, copy, +modification, preparation of derivative work, reproduction, and distribution of +Software (as defined below). + +1.2. “Licensor” means the copyright and/or patent owner or entity authorized by +the copyright and/or patent owner that is granting the License. + +1.3. “Licensee” means the individual or entity exercising permissions granted by +this License, including the use, copy, modification, preparation of derivative +work, reproduction, and distribution of Software (as defined below). + +1.4. “Software” means any copyrighted work, including but not limited to +software code, authored by Licensor and made available under this License. + +1.5. “Supply Chain” means the sequence of processes involved in the production +and/or distribution of a commodity, good, or service offered by the Licensee. + +1.6. “Supply Chain Impacted Party” or “Supply Chain Impacted Parties” means any +person(s) directly impacted by any of Licensee’s Supply Chain, including the +practices of all persons or entities within the Supply Chain prior to a good or +service reaching the Licensee. + +1.7. “Duty of Care” is defined by its use in tort law, delict law, and/or +similar bodies of law closely related to tort and/or delict law, including +without limitation, a requirement to act with the watchfulness, attention, +caution, and prudence that a reasonable person in the same or similar +circumstances would use towards any Supply Chain Impacted Party. + +1.8. “Worker” is defined to include any and all permanent, temporary, and agency +workers, as well as piece-rate, salaried, hourly paid, legal young (minors), +part-time, night, and migrant workers. + +2. INTELLECTUAL PROPERTY GRANTS: + +This section identifies intellectual property rights granted to a Licensee. + +2.1. Grant of Copyright License: Subject to the terms and conditions of this +License, Licensor hereby grants to Licensee a worldwide, non-exclusive, +no-charge, royalty-free copyright license to use, copy, modify, prepare +derivative work, reproduce, or distribute the Software, Licensor authored +modified software, or other work derived from the Software. + +2.2. Grant of Patent License: Subject to the terms and conditions of this +License, Licensor hereby grants Licensee a worldwide, non-exclusive, no-charge, +royalty-free patent license to make, have made, use, offer to sell, sell, +import, and otherwise transfer Software. + +3. ETHICAL STANDARDS: + +This section lists conditions the Licensee must comply with in order to have +rights under this License. + +The rights granted to the Licensee by this License are expressly made subject to +the Licensee’s ongoing compliance with the following conditions: + + * 3.1. The Licensee SHALL NOT, whether directly or indirectly, through agents + or assigns: + + * 3.1.1. Infringe upon any person’s right to life or security of person, + engage in extrajudicial killings, or commit murder, without lawful cause + (See Article 3, United Nations Universal Declaration of Human Rights; + Article 6, International Covenant on Civil and Political Rights) + + * 3.1.2. Hold any person in slavery, servitude, or forced labor (See Article + 4, United Nations Universal Declaration of Human Rights; Article 8, + International Covenant on Civil and Political Rights); + + * 3.1.3. Contribute to the institution of slavery, slave trading, forced + labor, or unlawful child labor (See Article 4, United Nations Universal + Declaration of Human Rights; Article 8, International Covenant on Civil and + Political Rights); + + * 3.1.4. Torture or subject any person to cruel, inhumane, or degrading + treatment or punishment (See Article 5, United Nations Universal + Declaration of Human Rights; Article 7, International Covenant on Civil and + Political Rights); + + * 3.1.5. Discriminate on the basis of sex, gender, sexual orientation, race, + ethnicity, nationality, religion, caste, age, medical disability or + impairment, and/or any other like circumstances (See Article 7, United + Nations Universal Declaration of Human Rights; Article 2, International + Covenant on Economic, Social and Cultural Rights; Article 26, International + Covenant on Civil and Political Rights); + + * 3.1.6. Prevent any person from exercising his/her/their right to seek an + effective remedy by a competent court or national tribunal (including + domestic judicial systems, international courts, arbitration bodies, and + other adjudicating bodies) for actions violating the fundamental rights + granted to him/her/them by applicable constitutions, applicable laws, or by + this License (See Article 8, United Nations Universal Declaration of Human + Rights; Articles 9 and 14, International Covenant on Civil and Political + Rights); + + * 3.1.7. Subject any person to arbitrary arrest, detention, or exile (See + Article 9, United Nations Universal Declaration of Human Rights; Article 9, + International Covenant on Civil and Political Rights); + + * 3.1.8. Subject any person to arbitrary interference with a person’s + privacy, family, home, or correspondence without the express written + consent of the person (See Article 12, United Nations Universal Declaration + of Human Rights; Article 17, International Covenant on Civil and Political + Rights); + + * 3.1.9. Arbitrarily deprive any person of his/her/their property (See + Article 17, United Nations Universal Declaration of Human Rights); + + * 3.1.10. Forcibly remove indigenous peoples from their lands or territories + or take any action with the aim or effect of dispossessing indigenous + peoples from their lands, territories, or resources, including without + limitation the intellectual property or traditional knowledge of indigenous + peoples, without the free, prior, and informed consent of indigenous + peoples concerned (See Articles 8 and 10, United Nations Declaration on the + Rights of Indigenous Peoples); + + * 3.1.11. Fossil Fuel Divestment: Be an individual or entity, or a + representative, agent, affiliate, successor, attorney, or assign of an + individual or entity, on the FFI Solutions Carbon Underground 200 list + [https://www.ffisolutions.com/research-analytics-index-solutions/research-screening/the-carbon-underground-200/?cn-reloaded=1]; + + * 3.1.12. Ecocide: Commit ecocide: + + * 3.1.12.1. For the purpose of this section, “ecocide” means unlawful or + wanton acts committed with knowledge that there is a substantial + likelihood of severe and either widespread or long-term damage to the + environment being caused by those acts; + + * 3.1.12.2. For the purpose of further defining ecocide and the terms + contained in the previous paragraph: + + * 3.1.12.2.1. “Wanton” means with reckless disregard for damage which + would be clearly excessive in relation to the social and economic + benefits anticipated; + + * 3.1.12.2.2. “Severe” means damage which involves very serious adverse + changes, disruption, or harm to any element of the environment, + including grave impacts on human life or natural, cultural, or + economic resources; + + * 3.1.12.2.3. “Widespread” means damage which extends beyond a limited + geographic area, crosses state boundaries, or is suffered by an entire + ecosystem or species or a large number of human beings; + + * 3.1.12.2.4. “Long-term” means damage which is irreversible or which + cannot be redressed through natural recovery within a reasonable + period of time; and + + * 3.1.12.2.5. “Environment” means the earth, its biosphere, cryosphere, + lithosphere, hydrosphere, and atmosphere, as well as outer space + + (See Section II, Independent Expert Panel for the Legal Definition of + Ecocide, Stop Ecocide Foundation and the Promise Institute for Human + Rights at UCLA School of Law, June 2021); + + * 3.1.13. Extractive Industries: Be an individual or entity, or a + representative, agent, affiliate, successor, attorney, or assign of an + individual or entity, that engages in fossil fuel or mineral exploration, + extraction, development, or sale; + + * 3.1.14. Taliban: Be an individual or entity that: + + * 3.1.14.1. engages in any commercial transactions with the Taliban; or + + * 3.1.14.2. is a representative, agent, affiliate, successor, attorney, or + assign of the Taliban; + + * 3.1.15. Myanmar: Be an individual or entity that: + + * 3.1.15.1. engages in any commercial transactions with the + Myanmar/Burmese military junta; or + + * 3.1.15.2. is a representative, agent, affiliate, successor, attorney, or + assign of the Myanmar/Burmese government; + + * 3.1.16. Xinjiang Uygur Autonomous Region: Be an individual or entity, or a + representative, agent, affiliate, successor, attorney, or assign of any + individual or entity, that does business in, purchases goods from, or + otherwise benefits from goods produced in the Xinjiang Uygur Autonomous + Region of China; + + * 3.1.17. Mass Surveillance: Be a government agency or multinational + corporation, or a representative, agent, affiliate, successor, attorney, + or assign of a government or multinational corporation, which participates + in mass surveillance programs; + + * 3.1.18. Military Activities: Be an entity or a representative, agent, + affiliate, successor, attorney, or assign of an entity which conducts + military activities; + + * 3.1.19. Law Enforcement: Be an individual or entity, or a or a + representative, agent, affiliate, successor, attorney, or assign of an + individual or entity, that provides good or services to, or otherwise + enters into any commercial contracts with, any local, state, or federal + law enforcement agency; + + * 3.1.20. Media: Be an individual or entity, or a or a representative, + agent, affiliate, successor, attorney, or assign of an individual or + entity, that broadcasts messages promoting killing, torture, or other + forms of extreme violence; + + * 3.1.21. Interfere with Workers' free exercise of the right to organize and + associate (See Article 20, United Nations Universal Declaration of Human + Rights; C087 - Freedom of Association and Protection of the Right to + Organise Convention, 1948 (No. 87), International Labour Organization; + Article 8, International Covenant on Economic, Social and Cultural Rights); + and + + * 3.1.22. Harm the environment in a manner inconsistent with local, state, + national, or international law. + + * 3.2. The Licensee SHALL: + + * 3.2.1. Social Auditing: Only use social auditing mechanisms that adhere to + Worker-Driven Social Responsibility Network’s Statement of Principles + (https://wsr-network.org/what-is-wsr/statement-of-principles/ + [https://wsr-network.org/what-is-wsr/statement-of-principles/]) over + traditional social auditing mechanisms, to the extent the Licensee uses + any social auditing mechanisms at all; + + * 3.2.2. Workers on Board of Directors: Ensure that if the Licensee has a + Board of Directors, 30% of Licensee’s board seats are held by Workers paid + no more than 200% of the compensation of the lowest paid Worker of the + Licensee; + + * 3.2.3. Supply Chain: Provide clear, accessible supply chain data to the + public in accordance with the following conditions: + + * 3.2.3.1. All data will be on Licensee’s website and/or, to the extent + Licensee is a representative, agent, affiliate, successor, attorney, + subsidiary, or assign, on Licensee’s principal’s or parent’s website or + some other online platform accessible to the public via an internet + search on a common internet search engine; and + + * 3.2.3.2. Data published will include, where applicable, manufacturers, + top tier suppliers, subcontractors, cooperatives, component parts + producers, and farms; + + * 3.2.4. Provide equal pay for equal work where the performance of such work + requires equal skill, effort, and responsibility, and which are performed + under similar working conditions, except where such payment is made + pursuant to: + + * 3.2.4.1. A seniority system; + + * 3.2.4.2. A merit system; + + * 3.2.4.3. A system which measures earnings by quantity or quality of + production; or + + * 3.2.4.4. A differential based on any other factor other than sex, gender, + sexual orientation, race, ethnicity, nationality, religion, caste, age, + medical disability or impairment, and/or any other like circumstances + (See 29 U.S.C.A. § 206(d)(1); Article 23, United Nations Universal + Declaration of Human Rights; Article 7, International Covenant on + Economic, Social and Cultural Rights; Article 26, International Covenant + on Civil and Political Rights); and + + * 3.2.5. Allow for reasonable limitation of working hours and periodic + holidays with pay (See Article 24, United Nations Universal Declaration of + Human Rights; Article 7, International Covenant on Economic, Social and + Cultural Rights). + +4. SUPPLY CHAIN IMPACTED PARTIES: + +This section identifies additional individuals or entities that a Licensee could +harm as a result of violating the Ethical Standards section, the condition that +the Licensee must voluntarily accept a Duty of Care for those individuals or +entities, and the right to a private right of action that those individuals or +entities possess as a result of violations of the Ethical Standards section. + +4.1. In addition to the above Ethical Standards, Licensee voluntarily accepts a +Duty of Care for Supply Chain Impacted Parties of this License, including +individuals and communities impacted by violations of the Ethical Standards. The +Duty of Care is breached when a provision within the Ethical Standards section +is violated by a Licensee, one of its successors or assigns, or by an individual +or entity that exists within the Supply Chain prior to a good or service +reaching the Licensee. + +4.2. Breaches of the Duty of Care, as stated within this section, shall create a +private right of action, allowing any Supply Chain Impacted Party harmed by the +Licensee to take legal action against the Licensee in accordance with applicable +negligence laws, whether they be in tort law, delict law, and/or similar bodies +of law closely related to tort and/or delict law, regardless if Licensee is +directly responsible for the harms suffered by a Supply Chain Impacted Party. +Nothing in this section shall be interpreted to include acts committed by +individuals outside of the scope of his/her/their employment. + +5. NOTICE: This section explains when a Licensee must notify others of the +License. + +5.1. Distribution of Notice: Licensee must ensure that everyone who receives a +copy of or uses any part of Software from Licensee, with or without changes, +also receives the License and the copyright notice included with Software (and +if included by the Licensor, patent, trademark, and attribution notice). +Licensee must ensure that License is prominently displayed so that any +individual or entity seeking to download, copy, use, or otherwise receive any +part of Software from Licensee is notified of this License and its terms and +conditions. Licensee must cause any modified versions of the Software to carry +prominent notices stating that Licensee changed the Software. + +5.2. Modified Software: Licensee is free to create modifications of the Software +and distribute only the modified portion created by Licensee, however, any +derivative work stemming from the Software or its code must be distributed +pursuant to this License, including this Notice provision. + +5.3. Recipients as Licensees: Any individual or entity that uses, copies, +modifies, reproduces, distributes, or prepares derivative work based upon the +Software, all or part of the Software’s code, or a derivative work developed by +using the Software, including a portion of its code, is a Licensee as defined +above and is subject to the terms and conditions of this License. + +6. REPRESENTATIONS AND WARRANTIES: + +6.1. Disclaimer of Warranty: TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE +COMES “AS IS,” WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR SHALL NOT +BE LIABLE TO ANY PERSON OR ENTITY FOR ANY DAMAGES OR OTHER LIABILITY ARISING +FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THIS LICENSE, UNDER ANY +LEGAL CLAIM. + +6.2. Limitation of Liability: LICENSEE SHALL HOLD LICENSOR HARMLESS AGAINST ANY +AND ALL CLAIMS, DEBTS, DUES, LIABILITIES, LIENS, CAUSES OF ACTION, DEMANDS, +OBLIGATIONS, DISPUTES, DAMAGES, LOSSES, EXPENSES, ATTORNEYS' FEES, COSTS, +LIABILITIES, AND ALL OTHER CLAIMS OF EVERY KIND AND NATURE WHATSOEVER, WHETHER +KNOWN OR UNKNOWN, ANTICIPATED OR UNANTICIPATED, FORESEEN OR UNFORESEEN, ACCRUED +OR UNACCRUED, DISCLOSED OR UNDISCLOSED, ARISING OUT OF OR RELATING TO LICENSEE’S +USE OF THE SOFTWARE. NOTHING IN THIS SECTION SHOULD BE INTERPRETED TO REQUIRE +LICENSEE TO INDEMNIFY LICENSOR, NOR REQUIRE LICENSOR TO INDEMNIFY LICENSEE. + +7. TERMINATION + +7.1. Violations of Ethical Standards or Breaching Duty of Care: If Licensee +violates the Ethical Standards section or Licensee, or any other person or +entity within the Supply Chain prior to a good or service reaching the Licensee, +breaches its Duty of Care to Supply Chain Impacted Parties, Licensee must remedy +the violation or harm caused by Licensee within 30 days of being notified of the +violation or harm. If Licensee fails to remedy the violation or harm within 30 +days, all rights in the Software granted to Licensee by License will be null and +void as between Licensor and Licensee. + +7.2. Failure of Notice: If any person or entity notifies Licensee in writing +that Licensee has not complied with the Notice section of this License, Licensee +can keep this License by taking all practical steps to comply within 30 days +after the notice of noncompliance. If Licensee does not do so, Licensee’s +License (and all rights licensed hereunder) will end immediately. + +7.3. Judicial Findings: In the event Licensee is found by a civil, criminal, +administrative, or other court of competent jurisdiction, or some other +adjudicating body with legal authority, to have committed actions which are in +violation of the Ethical Standards or Supply Chain Impacted Party sections of +this License, all rights granted to Licensee by this License will terminate +immediately. + +7.4. Patent Litigation: If Licensee institutes patent litigation against any +entity (including a cross-claim or counterclaim in a suit) alleging that the +Software, all or part of the Software’s code, or a derivative work developed +using the Software, including a portion of its code, constitutes direct or +contributory patent infringement, then any patent license, along with all other +rights, granted to Licensee under this License will terminate as of the date +such litigation is filed. + +7.5. Additional Remedies: Termination of the License by failing to remedy harms +in no way prevents Licensor or Supply Chain Impacted Party from seeking +appropriate remedies at law or in equity. + +8. MISCELLANEOUS: + +8.1. Conditions: Sections 3, 4.1, 5.1, 5.2, 7.1, 7.2, 7.3, and 7.4 are +conditions of the rights granted to Licensee in the License. + +8.2. Equitable Relief: Licensor and any Supply Chain Impacted Party shall be +entitled to equitable relief, including injunctive relief or specific +performance of the terms hereof, in addition to any other remedy to which they +are entitled at law or in equity. + +8.3. Copyleft: Modified software, source code, or other derivative work must be +licensed, in its entirety, under the exact same conditions as this License. + +8.4. Severability: If any term or provision of this License is determined to be +invalid, illegal, or unenforceable by a court of competent jurisdiction, any +such determination of invalidity, illegality, or unenforceability shall not +affect any other term or provision of this License or invalidate or render +unenforceable such term or provision in any other jurisdiction. If the +determination of invalidity, illegality, or unenforceability by a court of +competent jurisdiction pertains to the terms or provisions contained in the +Ethical Standards section of this License, all rights in the Software granted to +Licensee shall be deemed null and void as between Licensor and Licensee. + +8.5. Section Titles: Section titles are solely written for organizational +purposes and should not be used to interpret the language within each section. + +8.6. Citations: Citations are solely written to provide context for the source +of the provisions in the Ethical Standards. + +8.7. Section Summaries: Some sections have a brief italicized description which +is provided for the sole purpose of briefly describing the section and should +not be used to interpret the terms of the License. + +8.8. Entire License: This is the entire License between the Licensor and +Licensee with respect to the claims released herein and that the consideration +stated herein is the only consideration or compensation to be paid or exchanged +between them for this License. This License cannot be modified or amended except +in a writing signed by Licensor and Licensee. + +8.9. Successors and Assigns: This License shall be binding upon and inure to the +benefit of the Licensor’s and Licensee’s respective heirs, successors, and +assigns. diff --git a/README.md b/README.md index 92d6e78..c1fa688 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # paper_trail_background - - [![Build](http://img.shields.io/travis-ci/krainboltgreene/paper_trail_background.svg?style=flat-square)](https://travis-ci.org/krainboltgreene/paper_trail_background) - - [![Downloads](http://img.shields.io/gem/dtv/paper_trail_background.svg?style=flat-square)](https://rubygems.org/gems/paper_trail_background) - - [![Version](http://img.shields.io/gem/v/paper_trail_background.svg?style=flat-square)](https://rubygems.org/gems/paper_trail_background) + - [![Downloads](http://img.shields.io/gem/dtv/paper_trail-background.svg?style=flat-square)](https://rubygems.org/gems/paper_trail-background) + - [![Version](http://img.shields.io/gem/v/paper_trail-background.svg?style=flat-square)](https://rubygems.org/gems/paper_trail-background) Allows you to enqueue version creation/deletion as a background job to avoid having business logic blocked by changelog writing. @@ -15,20 +14,29 @@ First you'll need to setup a job for processing versions: ``` ruby # The class MUST be named this class VersionJob < ApplicationJob - - # These are settings you'll probably want, I suggest sidekiq-unique-jobs - sidekiq_options( - :queue => "versions", - :unique_across_queues => true, - :lock => :until_executed, - :log_duplicate_payload => true - ) + queue_as :default # This wires up the background job - include PaperTrail::Background::Sidekiq + include PaperTrail::Background::Job +end +``` + +## Configuration +In an initializer, you can specify whether you want to opt into this behavior on a per-model basis: + +``` ruby +PaperTrail::Background::Config.configure do |config| + config.opt_in = true end ``` +If opt-in behavior is set to `true`, you can enable async paper trails by specifying `async: true` in a given model's paper trail options: + +``` ruby +class SomeModel < ActiveRecord::Base + has_paper_trail async: true +end +``` ## Installing diff --git a/lib/paper_trail-background/rspec.rb b/lib/paper_trail-background/rspec.rb new file mode 100644 index 0000000..b16aa3a --- /dev/null +++ b/lib/paper_trail-background/rspec.rb @@ -0,0 +1,8 @@ +require 'ar_after_transaction' + +require_relative '../paper_trail/background/rspec_helpers' + +RSpec.configure do |config| + config.include PaperTrail::Background::RSpecHelpers::InstanceMethods + config.extend PaperTrail::Background::RSpecHelpers::ClassMethods +end \ No newline at end of file diff --git a/lib/paper_trail_background/background/config.rb b/lib/paper_trail_background/background/config.rb new file mode 100644 index 0000000..51c6e64 --- /dev/null +++ b/lib/paper_trail_background/background/config.rb @@ -0,0 +1,23 @@ +module PaperTrail + module Background + class Configuration + attr_reader :opt_in + + def opt_in=(value) + @opt_in = value + end + end + + class Config + class << self + def configuration + @configuration ||= Configuration.new + end + + def configure(&block) + yield(configuration) + end + end + end + end +end diff --git a/lib/paper_trail_background/background/job.rb b/lib/paper_trail_background/background/job.rb new file mode 100644 index 0000000..9a39914 --- /dev/null +++ b/lib/paper_trail_background/background/job.rb @@ -0,0 +1,9 @@ +module PaperTrail + module Background + module Job + def perform(version_class, attributes, event) + version = version_class.constantize.create!(attributes) + end + end + end +end diff --git a/lib/paper_trail_background/background/rspec_helpers.rb b/lib/paper_trail_background/background/rspec_helpers.rb new file mode 100644 index 0000000..c99bd12 --- /dev/null +++ b/lib/paper_trail_background/background/rspec_helpers.rb @@ -0,0 +1,43 @@ +module PaperTrail + module Background + module RSpecHelpers + module InstanceMethods + # enable versioning for specific blocks (at instance-level) + def with_versioning(expected_open_transactions: 1, &block) + was_enabled = ::PaperTrail.enabled? + ::PaperTrail.enabled = true + + normally_open_transactions = ActiveRecord::Base.normally_open_transactions + ActiveRecord::Base.normally_open_transactions = expected_open_transactions + + # ensure that VersionJobs are executed + perform_enqueued_jobs only: VersionJob, &block + ensure + ::PaperTrail.enabled = was_enabled + ActiveRecord::Base.normally_open_transactions = normally_open_transactions + end + end + + module ClassMethods + # enable versioning for specific blocks (at class-level) + def with_versioning(expected_open_transactions: 1, &block) + context "with versioning", versioning: true do + around do |example| + normally_open_transactions = ActiveRecord::Base.normally_open_transactions + ActiveRecord::Base.normally_open_transactions = expected_open_transactions + + perform_enqueued_jobs only: VersionJob do + example.run + end + + ActiveRecord::Base.normally_open_transactions = normally_open_transactions + end + + class_exec(&block) + end + end + end + + end + end +end \ No newline at end of file diff --git a/lib/paper_trail_background/background_spec.rb b/lib/paper_trail_background/background_spec.rb new file mode 100644 index 0000000..505cbf0 --- /dev/null +++ b/lib/paper_trail_background/background_spec.rb @@ -0,0 +1,200 @@ +require "spec_helper" + +class DummyClass + include PaperTrail::Background + + def initialize(options: {}, record: nil) + @options = options + @record = record + end + + def enabled? + @options[:enabled] + end + + def data_for_create + {} + end + + def data_for_destroy + {} + end + + def data_for_update_columns + {} + end + + def force + true + end +end + +class VersionJob + def self.perform_later(*args); end +end + +RSpec.describe PaperTrail::Background do + before do + allow(VersionJob).to receive(:perform_later) + end + + describe "#record_create" do + it "does not trigger a write if not enabled" do + DummyClass.new(options: { enabled: false }).record_create + + expect(VersionJob).not_to have_received(:perform_later) + end + + context "when enabled and opt_in config is enabled" do + let(:record) { double("SomeRecord") } + + before do + PaperTrail::Background::Config.configure do |config| + config.opt_in = true + end + end + + it "does not trigger a write if record is opted in but async is blank" do + allow(record).to receive(:paper_trail_options).and_return(async: nil) + + DummyClass.new(options: { enabled: true }, record: record).record_create + + expect(VersionJob).not_to have_received(:perform_later) + end + + it "triggers a write if record is opted in and async is true" do + allow(record).to receive(:paper_trail_options).and_return(async: true) + allow(PaperTrail::Events::Create).to receive(:new).and_return(OpenStruct.new(data: {})) + allow(RSpec::Mocks::Double).to receive(:paper_trail).and_return(OpenStruct.new(version_class: Class)) + allow(ActiveRecord::Base).to receive(:after_transaction).and_yield + + DummyClass.new(options: { enabled: true }, record: record).record_create + + expect(VersionJob).to have_received(:perform_later) + end + end + end + + describe "#record_destroy" do + it "does not trigger a write if not enabled" do + DummyClass.new(options: { enabled: false }).record_destroy("after") + + expect(VersionJob).not_to have_received(:perform_later) + end + + context "when enabled and opt_in config is enabled" do + let(:record) { double("SomeRecord") } + + before do + PaperTrail::Background::Config.configure do |config| + config.opt_in = true + end + end + + it "does not trigger a write if record is opted in but async is blank" do + allow(record).to receive(:paper_trail_options).and_return(async: nil) + + DummyClass.new(options: { enabled: true }, record: record).record_destroy("after") + + expect(VersionJob).not_to have_received(:perform_later) + end + + it "does not trigger a write if record is new" do + allow(record).to receive(:paper_trail_options).and_return(async: true) + allow(record).to receive(:new_record?).and_return(true) + + DummyClass.new(options: { enabled: true }, record: record).record_destroy("after") + + expect(VersionJob).not_to have_received(:perform_later) + end + + it "triggers a write if record is opted in and async is true" do + allow(record).to receive(:paper_trail_options).and_return(async: true) + allow(record).to receive(:new_record?).and_return(false) + allow(PaperTrail::Events::Destroy).to receive(:new).and_return(OpenStruct.new(data: {})) + allow(RSpec::Mocks::Double).to receive(:paper_trail).and_return(OpenStruct.new(version_class: Class)) + allow(ActiveRecord::Base).to receive(:after_transaction).and_yield + + DummyClass.new(options: { enabled: true }, record: record).record_destroy("after") + + expect(VersionJob).to have_received(:perform_later) + end + end + end + + describe "#record_update" do + it "does not trigger a write if not enabled" do + DummyClass.new(options: { enabled: false }).record_update(force: true, in_after_callback: true, is_touch: false) + + expect(VersionJob).not_to have_received(:perform_later) + end + + context "when enabled and opt_in config is enabled" do + let(:record) { double("SomeRecord") } + + before do + PaperTrail::Background::Config.configure do |config| + config.opt_in = true + end + end + + it "does not trigger a write if record is opted in but async is blank" do + allow(record).to receive(:paper_trail_options).and_return(async: nil) + + DummyClass.new(options: { enabled: true }, record: record).record_update(force: true, in_after_callback: true, is_touch: false) + + expect(VersionJob).not_to have_received(:perform_later) + end + + it "triggers a write if record is opted in and async is true" do + allow(record).to receive(:paper_trail_options).and_return(async: true) + allow(record).to receive(:new_record?).and_return(false) + allow(PaperTrail::Events::Update).to receive(:new).and_return(OpenStruct.new(data: {})) + allow(RSpec::Mocks::Double).to receive(:paper_trail).and_return(OpenStruct.new(version_class: Class)) + allow(ActiveRecord::Base).to receive(:after_transaction).and_yield + + DummyClass.new(options: { enabled: true }, record: record).record_update(force: true, in_after_callback: true, is_touch: false) + + expect(VersionJob).to have_received(:perform_later) + end + end + end + + describe "#record_update_columns" do + it "does not trigger a write if not enabled" do + DummyClass.new(options: { enabled: false }).record_update_columns({}) + + expect(VersionJob).not_to have_received(:perform_later) + end + + context "when enabled and opt_in config is enabled" do + let(:record) { double("SomeRecord") } + + before do + PaperTrail::Background::Config.configure do |config| + config.opt_in = true + end + end + + it "does not trigger a write if record is opted in but async is blank" do + allow(record).to receive(:paper_trail_options).and_return(async: nil) + + DummyClass.new(options: { enabled: true }, record: record).record_update_columns({}) + + expect(VersionJob).not_to have_received(:perform_later) + end + + it "triggers a write if record is opted in and async is true" do + allow(record).to receive(:paper_trail_options).and_return(async: true) + allow(record).to receive(:new_record?).and_return(false) + allow(PaperTrail::Events::Update).to receive(:new).and_return(OpenStruct.new(data: {})) + allow(RSpec::Mocks::Double).to receive(:paper_trail).and_return(OpenStruct.new(version_class: Class)) + allow(ActiveRecord::Base).to receive(:after_transaction).and_yield + + DummyClass.new(options: { enabled: true }, record: record).record_update_columns({}) + + expect(VersionJob).to have_received(:perform_later) + end + end + end +end diff --git a/lib/paper_trail_background/record_trail.rb b/lib/paper_trail_background/record_trail.rb index 045088e..4781c67 100644 --- a/lib/paper_trail_background/record_trail.rb +++ b/lib/paper_trail_background/record_trail.rb @@ -1,5 +1,10 @@ -module PaperTrailBackground - require_relative 'sidekiq' +require "paper_trail/record_trail" +require "ar_after_transaction" + +module PaperTrail + require_relative "background/config" + require_relative "background/version" + require_relative "background/job" module RecordTrail # @api private @@ -7,6 +12,7 @@ module RecordTrail # paper_trail-association_tracking def record_create return unless enabled? + return if Config.configuration.opt_in && @record.paper_trail_options[:async].blank? event = PaperTrail::Events::Create.new(@record, true) @@ -24,6 +30,7 @@ def record_create # paper_trail-association_tracking def record_destroy(recording_order) return unless enabled? + return if Config.configuration.opt_in && @record.paper_trail_options[:async].blank? return if @record.new_record? in_after_callback = recording_order == "after" @@ -42,6 +49,7 @@ def record_destroy(recording_order) # paper_trail-association_tracking def record_update(force:, in_after_callback:, is_touch:) return unless enabled? + return if Config.configuration.opt_in && @record.paper_trail_options[:async].blank? event = PaperTrail::Events::Update.new(@record, in_after_callback, is_touch, nil) @@ -59,6 +67,7 @@ def record_update(force:, in_after_callback:, is_touch:) # paper_trail-association_tracking def record_update_columns(changes) return unless enabled? + return if Config.configuration.opt_in && @record.paper_trail_options[:async].blank? event = Events::Update.new(@record, false, false, changes) @@ -69,16 +78,14 @@ def record_update_columns(changes) trigger_write(@record, data, :update) end - private + private def trigger_write(record, data, event) + version_class = record.class.paper_trail.version_class.name - def trigger_write(record, data, event) - version_class = record.class.paper_trail.version_class - version_class.after_transaction do - PaperTrailBackground.version_job_class.perform_later( - version_class.to_s, - data.except(:item).merge( - item_id: record.id, item_type: record.class.name - ).as_json, event + ActiveRecord::Base.after_transaction do + VersionJob.perform_later( + version_class, + data, + event ) end end diff --git a/paper_trail_background.gemspec b/paper_trail_background.gemspec index 432b547..0b7c3be 100644 --- a/paper_trail_background.gemspec +++ b/paper_trail_background.gemspec @@ -16,16 +16,16 @@ Gem::Specification.new do |spec| spec.files = Dir[File.join('lib', '**', '*'), 'LICENSE', 'README.md', 'Rakefile'] spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 2.5.0' + spec.required_ruby_version = '>= 3.2.0' - spec.add_development_dependency 'bundler', '~> 2.2.29' + spec.add_development_dependency 'bundler', '~> 2.4.8' spec.add_development_dependency 'byebug', '~> 11.0' - spec.add_development_dependency 'rails', '~> 6.0.4.1' + spec.add_development_dependency 'rails', '>= 6.0.4.1' spec.add_development_dependency 'rake', '~> 13.0.6' spec.add_development_dependency 'rspec', '~> 3.10.0' spec.add_development_dependency 'sidekiq', '~> 6.2.2' spec.add_development_dependency 'sqlite3', '~> 1.4.2' - spec.add_runtime_dependency 'ar_after_transaction', '~> 0.7.0' - spec.add_runtime_dependency 'paper_trail', '~> 12.0.0' + spec.add_runtime_dependency 'ar_after_transaction', '>= 0.7.0' + spec.add_runtime_dependency 'paper_trail', '>= 12.0.0' end