├── .formatter.exs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── question.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lib ├── contexted.ex └── contexted │ ├── crud.ex │ ├── delegator.ex │ ├── mix │ └── tasks │ │ └── compile │ │ └── contexted.ex │ ├── module_analyzer.ex │ ├── tracer.ex │ └── utils.ex ├── mix.exs ├── mix.lock └── test ├── context_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report a bug in Contexted 3 | labels: ["bug", "needs-triage"] 4 | assignees: [] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | 12 | - type: textarea 13 | id: description 14 | attributes: 15 | label: Bug description 16 | description: A clear and concise description of what the bug is. 17 | placeholder: Describe what happened... 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: reproduction 23 | attributes: 24 | label: Steps to Reproduce 25 | description: Steps to reproduce the behavior 26 | placeholder: | 27 | 1. Configure contexted with... 28 | 2. Add context module... 29 | 3. Run `mix compile`... 30 | 4. See error... 31 | value: | 32 | 1. 33 | 2. 34 | 3. 35 | 4. 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: expected 41 | attributes: 42 | label: Expected behavior 43 | description: A clear and concise description of what you expected to happen. 44 | placeholder: What should have happened? 45 | validations: 46 | required: true 47 | 48 | - type: textarea 49 | id: actual 50 | attributes: 51 | label: Actual behavior 52 | description: What actually happened instead? 53 | placeholder: What actually happened? 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | id: config 59 | attributes: 60 | label: Contexted configuration 61 | description: Your contexted configuration from `config.exs` 62 | render: elixir 63 | placeholder: | 64 | config :contexted, 65 | contexts: [MyApp.Accounts, MyApp.Blog], 66 | exclude_paths: [] 67 | 68 | - type: textarea 69 | id: context-code 70 | attributes: 71 | label: Context code (if relevant) 72 | description: Share relevant context module code that demonstrates the issue 73 | render: elixir 74 | placeholder: | 75 | defmodule MyApp.Accounts do 76 | import Contexted.Delegator 77 | # ... 78 | end 79 | 80 | - type: input 81 | id: contexted-version 82 | attributes: 83 | label: Contexted version 84 | description: What version of Contexted are you using? 85 | placeholder: "0.3.4" 86 | validations: 87 | required: true 88 | 89 | - type: input 90 | id: elixir-version 91 | attributes: 92 | label: Elixir version 93 | description: What version of Elixir are you using? 94 | placeholder: "1.15.7" 95 | validations: 96 | required: true 97 | 98 | - type: input 99 | id: otp-version 100 | attributes: 101 | label: OTP version 102 | description: What version of Erlang/OTP are you using? 103 | placeholder: "26.1" 104 | 105 | - type: dropdown 106 | id: component 107 | attributes: 108 | label: Which Contexted component is affected? 109 | multiple: true 110 | options: 111 | - Contexted.Tracer (cross-reference detection) 112 | - Contexted.Delegator (function delegation) 113 | - Contexted.CRUD (CRUD generation) 114 | - Mix compiler integration 115 | - Documentation generation 116 | - Not sure / Multiple components 117 | validations: 118 | required: true 119 | 120 | - type: textarea 121 | id: error-output 122 | attributes: 123 | label: Error output 124 | description: If you're getting error messages, please paste them here 125 | render: shell 126 | placeholder: Paste any error messages, stack traces, or compiler output 127 | 128 | - type: textarea 129 | id: additional-context 130 | attributes: 131 | label: Additional context 132 | description: Add any other context about the problem here, such as workarounds you've tried 133 | placeholder: Any additional information that might help us understand the issue 134 | 135 | - type: checkboxes 136 | id: checks 137 | attributes: 138 | label: Before submitting 139 | options: 140 | - label: I have searched the existing issues to make sure this bug hasn't been reported already 141 | required: true 142 | - label: I have read the documentation and confirmed this is a bug, not a configuration issue 143 | required: true 144 | - label: I can reproduce this issue consistently 145 | required: true 146 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 📚 Documentation 4 | url: https://hexdocs.pm/contexted 5 | about: Read the official documentation for Contexted 6 | - name: 💬 Community Discussion 7 | url: https://elixir-lang.slack.com/archives/C091Q5S0GDU 8 | about: Join the community discussion on Elixir Slack 9 | - name: 📝 Blog & Tutorials 10 | url: https://curiosum.com/blog?search=contexted 11 | about: Check out blog posts and tutorials about Contexted 12 | - name: ❓ General Questions 13 | url: https://github.com/curiosum-dev/contexted/discussions 14 | about: Ask general questions about Contexted usage and best practices 15 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature Request 2 | description: Suggest a new feature or enhancement for Contexted 3 | labels: ["enhancement", "needs-triage"] 4 | assignees: [] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for suggesting a new feature! Please provide as much detail as possible. 11 | 12 | - type: textarea 13 | id: feature-description 14 | attributes: 15 | label: Feature description 16 | description: A clear and concise description of what you want to happen. 17 | placeholder: Describe the feature you'd like to see... 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: problem 23 | attributes: 24 | label: Problem or use case 25 | description: What problem does this feature solve? What use case does it enable? 26 | placeholder: | 27 | Describe the problem you're trying to solve or the use case this feature would enable. 28 | Example: "When working with large Phoenix applications, I often need to..." 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: proposed-solution 34 | attributes: 35 | label: Proposed solution 36 | description: Describe how you envision this feature working 37 | placeholder: | 38 | How do you think this should work? What API would you expect? 39 | 40 | Example: 41 | ```elixir 42 | # New configuration option 43 | config :contexted, new_option: true 44 | 45 | # New macro or function 46 | Contexted.SomeModule.new_function() 47 | ``` 48 | 49 | - type: dropdown 50 | id: component 51 | attributes: 52 | label: Which Contexted component would this affect? 53 | multiple: true 54 | options: 55 | - Contexted.Tracer (cross-reference detection) 56 | - Contexted.Delegator (function delegation) 57 | - Contexted.CRUD (CRUD generation) 58 | - Mix compiler integration 59 | - Configuration system 60 | - Documentation generation 61 | - New component/module 62 | validations: 63 | required: true 64 | 65 | - type: textarea 66 | id: alternatives 67 | attributes: 68 | label: Alternative Solutions 69 | description: What alternatives have you considered? What are the trade-offs? 70 | placeholder: | 71 | Have you considered any alternative approaches to solving this problem? 72 | What are the pros and cons of different solutions? 73 | 74 | - type: dropdown 75 | id: priority 76 | attributes: 77 | label: Priority 78 | description: How important is this feature to you? 79 | options: 80 | - Low - Nice to have 81 | - Medium - Would improve my workflow 82 | - High - Blocking my work or causing significant pain 83 | - Critical - Prevents me from using Contexted 84 | validations: 85 | required: true 86 | 87 | - type: dropdown 88 | id: breaking-change 89 | attributes: 90 | label: Breaking change 91 | description: Would this feature require breaking changes to the current API? 92 | options: 93 | - No - This is backward compatible 94 | - Maybe - Not sure about compatibility 95 | - Yes - This would require breaking changes 96 | validations: 97 | required: true 98 | 99 | - type: textarea 100 | id: examples 101 | attributes: 102 | label: Example usage 103 | description: Show how this feature would be used in practice 104 | render: elixir 105 | placeholder: | 106 | # Example of how the feature would be used 107 | defmodule MyApp.Accounts do 108 | # ... your example code here 109 | end 110 | 111 | - type: textarea 112 | id: context-integration 113 | attributes: 114 | label: Integration with existing features 115 | description: How would this feature work with existing Contexted functionality? 116 | placeholder: | 117 | How should this feature integrate with: 118 | - Cross-reference detection 119 | - Function delegation 120 | - CRUD generation 121 | - Existing configuration options 122 | 123 | - type: textarea 124 | id: additional-context 125 | attributes: 126 | label: Additional context 127 | description: Add any other context, screenshots, links, or examples that help explain your request 128 | placeholder: | 129 | Any additional information, links to relevant discussions, 130 | examples from other libraries, etc. 131 | 132 | - type: checkboxes 133 | id: checks 134 | attributes: 135 | label: Before submitting 136 | options: 137 | - label: I have searched the existing issues to make sure this feature hasn't been requested already 138 | required: true 139 | - label: I have read the documentation and confirmed this functionality doesn't already exist 140 | required: true 141 | - label: I am willing to help implement this feature (optional, but appreciated!) 142 | required: false 143 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: ❓ Question 2 | description: Ask a question about using Contexted 3 | labels: ["question", "needs-triage"] 4 | assignees: [] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for your question! Please provide as much context as possible to help us give you the best answer. 11 | 12 | 💡 **Tip**: For general discussions and community help, consider using [GitHub Discussions](https://github.com/curiosum-dev/contexted/discussions) or the [Elixir Slack channel](https://elixir-lang.slack.com/archives/C091Q5S0GDU). 13 | 14 | - type: textarea 15 | id: question 16 | attributes: 17 | label: Question 18 | description: What would you like to know about Contexted? 19 | placeholder: Ask your question here... 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: context 25 | attributes: 26 | label: Context 27 | description: What are you trying to accomplish? What have you already tried? 28 | placeholder: | 29 | Describe what you're working on and what you've already tried. 30 | This helps us give you better, more specific advice. 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | id: current-setup 36 | attributes: 37 | label: Current Setup 38 | description: Share your current Contexted configuration and relevant code 39 | render: elixir 40 | placeholder: | 41 | # config.exs 42 | config :contexted, 43 | contexts: [MyApp.Accounts] 44 | 45 | # Your context code 46 | defmodule MyApp.Accounts do 47 | # ... 48 | end 49 | 50 | - type: input 51 | id: contexted-version 52 | attributes: 53 | label: Contexted Version 54 | description: What version of Contexted are you using? 55 | placeholder: "0.3.4" 56 | validations: 57 | required: true 58 | 59 | - type: input 60 | id: elixir-version 61 | attributes: 62 | label: Elixir Version 63 | description: What version of Elixir are you using? 64 | placeholder: "1.15.7" 65 | validations: 66 | required: true 67 | 68 | - type: dropdown 69 | id: topic 70 | attributes: 71 | label: Topic Area 72 | description: Which area does your question relate to? 73 | options: 74 | - Getting started / Setup 75 | - Context cross-reference detection 76 | - Function delegation 77 | - CRUD generation 78 | - Configuration 79 | - Best practices 80 | - Integration with Phoenix 81 | - Performance 82 | - Troubleshooting 83 | - Other 84 | validations: 85 | required: true 86 | 87 | - type: textarea 88 | id: additional-info 89 | attributes: 90 | label: Additional Information 91 | description: Any other details that might be helpful 92 | placeholder: Links to relevant documentation you've read, error messages, etc. 93 | 94 | - type: checkboxes 95 | id: checks 96 | attributes: 97 | label: Before asking 98 | options: 99 | - label: I have read the [documentation](https://hexdocs.pm/contexted) 100 | required: true 101 | - label: I have searched existing issues and discussions 102 | required: true 103 | 104 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Type of Change 6 | 7 | 8 | 9 | - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) 10 | - [ ] ✨ New feature (non-breaking change which adds functionality) 11 | - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] 📚 Documentation update 13 | - [ ] 🔧 Refactoring (no functional changes, no api changes) 14 | - [ ] ⚡ Performance improvement 15 | - [ ] 🧪 Test coverage improvement 16 | 17 | ## Changes Made 18 | 19 | 20 | 21 | - 22 | - 23 | - 24 | 25 | ## Testing 26 | 27 | 28 | 29 | - [ ] All existing tests pass 30 | - [ ] New tests added for new functionality 31 | - [ ] Manual testing performed (describe below) 32 | 33 | ### Manual Testing Details 34 | 35 | 36 | 37 | ## Context Integration 38 | 39 | 40 | 41 | - [ ] This change affects `Contexted.Tracer` functionality 42 | - [ ] This change affects `Contexted.Delegator` functionality 43 | - [ ] This change affects `Contexted.CRUD` functionality 44 | - [ ] This change affects compilation behavior 45 | - [ ] This change affects migration/setup process 46 | 47 | ## Documentation 48 | 49 | - [ ] README.md updated (if needed) 50 | - [ ] Inline code documentation updated 51 | 52 | ## Breaking Changes 53 | 54 | 55 | 56 | ## Additional Notes 57 | 58 | 59 | 60 | ## Checklist 61 | 62 | - [ ] My code follows the project's style guidelines 63 | - [ ] I have performed a self-review of my own code 64 | - [ ] I have commented my code, particularly in hard-to-understand areas 65 | - [ ] My changes generate no new warnings 66 | - [ ] I have added tests that prove my fix is effective or that my feature works 67 | - [ ] New and existing unit tests pass locally with my changes 68 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | MIX_ENV: test 9 | 10 | jobs: 11 | build_and_test: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | elixir_version: ['1.15', '1.16', '1.17', '1.18'] 18 | otp_version: ['24', '25', '26', '27'] 19 | exclude: 20 | - elixir_version: '1.15' 21 | otp_version: '27' 22 | - elixir_version: '1.16' 23 | otp_version: '27' 24 | - elixir_version: '1.17' 25 | otp_version: '24' 26 | - elixir_version: '1.18' 27 | otp_version: '24' 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - name: Set up Elixir 34 | uses: erlef/setup-beam@v1 35 | with: 36 | elixir-version: ${{ matrix.elixir_version }} 37 | otp-version: ${{ matrix.otp_version }} 38 | 39 | - name: Restore dependencies and _build cache 40 | uses: actions/cache@v4 41 | with: 42 | path: | 43 | deps 44 | _build 45 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 46 | restore-keys: | 47 | ${{ runner.os }}-mix- 48 | 49 | - name: Install dependencies 50 | run: mix deps.get 51 | 52 | - name: Build project 53 | run: mix compile 54 | 55 | - name: Check formatting 56 | run: mix format --check-formatted 57 | 58 | - name: Run credo 59 | run: mix credo --strict 60 | 61 | - name: Run tests 62 | run: mix test 63 | 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | context-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # Desktop Services Store 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.3 2 | erlang 27.3.2 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Contact: [contact@curiosum.com][0] 4 | 5 | ## Why have a Code of Conduct? 6 | 7 | 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. 8 | 9 | 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 Contexted 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. 10 | 11 | ## Our Values 12 | 13 | These are the values Contexted developers should aspire to: 14 | 15 | * Be friendly and welcoming 16 | * Be kind 17 | * Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) 18 | * Interpret the arguments of others in good faith, do not seek to disagree. 19 | * When we do disagree, try to understand why. 20 | * Be thoughtful 21 | * Productive communication requires effort. Think about how your words will be interpreted. 22 | * Remember that sometimes it is best to refrain entirely from commenting. 23 | * Be respectful 24 | * In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively. 25 | * Be constructive 26 | * Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation. 27 | * 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. 28 | * 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. 29 | * Avoid snarking (pithy, unproductive, sniping comments). 30 | * 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). 31 | * Be responsible 32 | * What you say and do matters. Take responsibility for your words and actions, including their consequences, whether intended or otherwise. 33 | 34 | The following actions are explicitly forbidden: 35 | 36 | * Insulting, demeaning, hateful, or threatening remarks. 37 | * Discrimination based on age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. 38 | * Bullying or systematic harassment. 39 | * Unwelcome sexual advances. 40 | * Incitement to any of these. 41 | 42 | ## Where does the Code of Conduct apply? 43 | 44 | If you contribute to or support Contexted in any way, you are encouraged to follow the Code of Conduct while doing so. 45 | 46 | Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Contexted project: 47 | 48 | * The official [Contexted][1] GitHub project and code reviews. 49 | * The official [Contexted channel][5] in Elixir's Slack workspace. 50 | 51 | Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. 52 | 53 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: [contact@curiosum.com][0]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**. 54 | 55 | **The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group. 56 | 57 | ## Acknowledgements 58 | 59 | This document was derived from the Elixir Code of Conduct (dated Sep/2021), itself based on the Code of Conduct from the Go project (dated Sep/2021) and the Contributor Covenant (v1.4). 60 | 61 | [0]: mailto:contact@curiosum.com?subject=Contexted:%20Code%20of%20Conduct%20Report 62 | [1]: https://github.com/curiosum_dev/contexted/ 63 | [5]: https://elixir-lang.slack.com/archives/C099FL2MAA0 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Contexted 2 | 3 | Thank you for your interest in contributing to Contexted! This document provides guidelines and information for contributors. 4 | 5 | ## Code of Conduct 6 | 7 | This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 8 | 9 | ## Getting started 10 | 11 | ### Development environment 12 | 13 | #### Prerequisites 14 | 15 | * Elixir 1.15+ and OTP 24+ 16 | - _If implementing a proposed feature requires bumping Elixir or OTP, we're open to considering that._ 17 | * Git 18 | 19 | #### Setup 20 | 21 | Just clone the repository, install dependencies normally, develop and run tests. When running Credo and Dialyzer, please use `MIX_ENV=test` to ensure tests and support files are validated, too. 22 | 23 | ## How to contribute 24 | 25 | ### Before proceeding: where to discuss things? 26 | 27 | The best place to discuss fresh ideas for the library's future and ask any sorts of questions (not strictly constituting an issue as defined below) is the [Contexted channel in Elixir's Slack workspace][0]. You can also use the appropriate [GitHub Discussions][1] channel. We're usually easiest approachable via Slack, though 😉 28 | 29 | For example, if you've got a general question about Contexted's development direction, or about its intended behaviour in a specific scenario, it's more convenient to open up a discussion on Slack (or use GitHub discussions) than to file an issue right away. 30 | 31 | However, if there's clearly a bug you'd like to report, or you have a specific feature idea that you'd like to explain, it's perfect material for a GitHub issue inside the project! 32 | 33 | ### Reporting issues 34 | 35 | Before creating an issue, please: 36 | 37 | 1. **Search existing issues** to avoid duplicates 38 | 2. **Use the issue templates** when available 39 | 3. **For bug reports, provide detailed information**: 40 | - Elixir/OTP versions 41 | - Contexted version 42 | - Minimal reproduction case 43 | - Expected vs actual behavior 44 | 4. **For feature requests, check against roadmap outlined in [README](./README.md) and provide the following**: 45 | - Purpose of the new feature 46 | - Intended usage syntax (or pseudocode) 47 | - Expected implementation complexity (if possible to gauge) 48 | - Expected impact on other existing features and backward compatibility 49 | 50 | ### Pull requests 51 | 52 | #### Before you start 53 | 54 | - **Check existing PRs** to avoid duplicate work 55 | - **Open an issue** for discussion on significant changes (we follow the 1 issue <-> 1 PR principle) 56 | - **Follow our coding standards** (see below) 57 | 58 | #### PR process 59 | 60 | 1. **Fork the repository** and create a feature branch 61 | 2. **Make your changes** with clear, focused commits 62 | 3. **Add tests** for new functionality 63 | 4. **Update documentation** as needed, including README 64 | 5. **Run the full test suite** as well as all static checks 65 | 6. **Submit your PR** with a clear description 66 | 67 | #### PR Requirements 68 | 69 | - [ ] Code compiles without warnings (`mix compile --warnings-as-errors`) 70 | - [ ] Tests pass (`mix test`) 71 | - _Includes added tests to new features and fixed bugs._ 72 | - _Tests pass for all combinations of tool versions covered by the CI matrix (see `.github/workflows/elixir.yml`)._ 73 | - [ ] Code follows style guidelines (`MIX_ENV=test mix credo`) 74 | - _Run Credo in test environment to ensure tests and support code adheres to style rules as well._ 75 | - [ ] Types are correct (`MIX_ENV=test mix dialyzer`) 76 | - _Run Credo in test environment to ensure tests and support code has correct typing as well._ 77 | - _Precise typing is encouraged for all newly added modules and functions._ 78 | - _Ignores are permitted with an inline comment with proper justification._ 79 | - [ ] Documentation is updated: a bare minimum is updates to affected public API parts 80 | - _Functions intended for usage by application code must have descriptions of arguments, purpose, and usage examples._ 81 | - _For crucial additions to features, README must also be updated._ 82 | - [ ] Commit messages are clear and descriptive 83 | - If already used throughout commit history, use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 84 | - Otherwise, use single-line messages formatted as: `Commit purpose in imperative mode [#issue_number]`. 85 | - Focus on what vs how, and keep it simple 86 | - Example: `Fix incorrect action definition issue [#123]` 87 | 88 | ## Development guidelines 89 | 90 | ### Code style 91 | 92 | We use [Credo](https://github.com/rrrene/credo) for code analysis: 93 | 94 | ```bash 95 | MIX_ENV=test mix credo 96 | ``` 97 | 98 | Key principles: 99 | - **Clarity over cleverness** - write readable code and avoid convoluted solutions and hacks. Prefer pure Elixir for easier debugging and code clarity. Use macros wisely and only when needed. 100 | * **Minimal dependencies** - Contexted was designed as a zero-dependency library and should remain that way unless there is a very, very good reason to break that rule. Development dependencies can be accepted if deemed an improvement to the process. 101 | - **Follow Elixir conventions** - use standard patterns and don't fight the platform. Avoid [common Elixir anti-patterns](https://hexdocs.pm/elixir/what-anti-patterns.html). Write functional, idiomatic Elixir code. 102 | - **Document public APIs** - include `@doc` and `@moduledoc` written as if you were the one who is to read them. Avoid `@moduledoc false` unless a module is deeply private. As long as typing in Elixir relies on Dialyzer, do type with `@spec` extensively. Avoid typing with the likes of `map()`, `any()`, or `term()` where possible. 103 | - **Handle errors gracefully** - always consider what's the correct scheme of error propagation. There is no globally assumed convention of whether to use error tuples or exceptions, you should use whatever feels appropriate and in line with adjacent elements of the system. 104 | 105 | ### Testing 106 | 107 | - **Write tests for all new functionality and bug fixes** 108 | - **Maintain high test coverage** 109 | - **Use descriptive test names** 110 | - **Test both success and failure cases** 111 | 112 | ### Commit messages 113 | 114 | Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format if already used in the repository. Otherwise, write single-line messages in imperative mode and mark related issue number with [#number]. 115 | 116 | ### Branching Strategy 117 | 118 | Follow [Conventional Branch](https://conventional-branch.github.io/) format if already used in the repository. Otherwise, use `issue_number-issue_name` as branch names. Create feature branches off `main` or `master` and avoid a nested branch tree. 119 | 120 | ## Release Process 121 | 122 | Releases are handled by maintainers: 123 | 124 | 1. Version bump in `mix.exs` 125 | 2. Update `CHANGELOG.md` 126 | 3. Create GitHub release 127 | 4. Publish to Hex.pm 128 | 129 | ## Community 130 | 131 | ### Getting help 132 | 133 | - [Elixir Slack channel][0] - for chat, questions and general discussion 134 | - [GitHub Discussions][1] - for questions and general discussion 135 | - [GitHub Issues][2] - for bug reports and feature requests 136 | - [Curiosum Blog][3] - for updates, tutorials and other content 137 | 138 | ### Recognition 139 | 140 | We are eager to publicly recognize your valuable input as a Contexted contributor! Your contributions will be highlighted in subsequent release notes, as well as blog posts announcing community contributions. 141 | 142 | ## Questions? 143 | 144 | Feel free to: 145 | - Ping us on [Elixir Slack][0] 146 | - Open a [GitHub Discussion](https://github.com/curiosum-dev/contexted/discussions) 147 | - Contact us at [Curiosum](https://curiosum.com/contact) 148 | - Check our [blog](https://curiosum.com/blog) for updates 149 | 150 | Thank you for contributing to Contexted! 🎉 151 | 152 | [0]: https://elixir-lang.slack.com/archives/C099FL2MAA0 153 | [1]: https://github.com/curiosum-dev/contexted/discussions 154 | [2]: https://github.com/curiosum-dev/contexted/issues 155 | [3]: https://curiosum.com/blog?search=contexted 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Curiosum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Contexted

5 |

Tools for clean, maintainable Elixir & Phoenix contexts

6 | 7 | [![Contact Us](https://img.shields.io/badge/Contact%20Us-%23F36D2E?style=for-the-badge&logo=maildotru&logoColor=white&labelColor=F36D2E)](https://curiosum.com/contact) 8 | [![Visit Curiosum](https://img.shields.io/badge/Visit%20Curiosum-%236819E6?style=for-the-badge&logo=elixir&logoColor=white&labelColor=6819E6)](https://curiosum.com/services/elixir-software-development) 9 | [![License: MIT](https://img.shields.io/badge/License-MIT-1D0642?style=for-the-badge&logo=open-source-initiative&logoColor=white&labelColor=1D0642)](https://github.com/curiosum-dev/contexted/blob/main/LICENSE) 10 |
11 | 12 | 13 | ## 📦 Short overview 14 | 15 | [Contexts](https://hexdocs.pm/phoenix/contexts.html) in Elixir & Phoenix are getting complicated over time. 16 | Cross-referencing, big modules and repetitiveness are the most common reasons for this problem. 17 | 18 | Contexted arms you with a set of tools to maintain contexts well. 19 | 20 | [![Hex version badge](https://img.shields.io/hexpm/v/contexted.svg)](https://hex.pm/packages/contexted) 21 | [![Actions Status](https://github.com/curiosum-dev/contexted/actions/workflows/ci.yml/badge.svg)](https://github.com/curiosum-dev/contexted/actions) 22 | [![License badge](https://img.shields.io/hexpm/l/contexted.svg)](https://github.com/curiosum-dev/contexted/blob/main/LICENSE) 23 | 24 |
25 | 26 | --- 27 | 28 | _Note: Official documentation for contexted library is [available on hexdocs][hexdoc]._ 29 | 30 | [hexdoc]: https://hexdocs.pm/contexted 31 | 32 | --- 33 | 34 |
35 | 36 | ## 📚 Table of Contents 37 | 38 | - [Features](#-features) 39 | - [Installation](#-installation) 40 | - [Step by step overview](#-step-by-step-overview) 41 | - [Keep contexts separate](#keep-contexts-separate) 42 | - [Exclude files and folders from check context cross-references](#exclude-files-and-folders-from-cross-references-context-check) 43 | - [Dividing each context into smaller parts](#dividing-each-context-into-smaller-parts) 44 | - [Being able to access docs and specs in auto-delegated functions](#being-able-to-access-docs-and-specs-in-auto-delegated-functions) 45 | - [Don't repeat yourself with CRUD operations](#dont-repeat-yourself-with-crud-operations) 46 | - [Contributing](#-contributing) 47 | - [License](#-license) 48 | 49 |
50 | 51 | ## ✨ Features 52 | 53 | - `Contexted.Tracer` - trace and enforce definite separation between specific context modules. 54 | - `Contexted.Delegator` - divide the big context module into smaller parts and use delegations to build the final context. 55 | - `Contexted.CRUD` - auto-generate the most common CRUD operations whenever needed. 56 | 57 |
58 | 59 | ## 📥 Installation 60 | 61 | Add the following to your `mix.exs` file: 62 | 63 | ```elixir 64 | defp deps do 65 | [ 66 | {:contexted, "~> 0.3.4"} 67 | ] 68 | end 69 | ``` 70 | 71 | Then run `mix deps.get`. 72 | 73 |
74 | 75 | ## 🧭 Step by step overview 76 | 77 | To describe a sample usage of this library, let's assume that your project has three contexts: 78 | 79 | - `Account` 80 | - `Subscription` 81 | - `Blog` 82 | 83 | Our goal, as the project grows, is to: 84 | 85 | 1. Keep contexts separate and not create any cross-references. For this to work, we'll raise errors during compilation whenever such a cross-reference happens. 86 | 2. Divide each context into smaller parts so that it is easier to maintain. In this case, we'll refer to each of these parts as **Subcontext**. It's not a new term added to the Phoenix framework but rather a term proposed to emphasize that it's a subset of Context. For this to work, we'll use delegates. 87 | 3. Not repeat ourselves with common business logic operations. For this to work, we'll be using CRUD functions generator, since these are the most common. 88 | 89 |
90 | 91 | ### Keep contexts separate 92 | 93 | It's very easy to monitor cross-references between context modules with the `contexted` library. 94 | 95 | First, add `contexted` as one of the compilers in _mix.exs_: 96 | 97 | ```elixir 98 | def project do 99 | [ 100 | ... 101 | compilers: [:contexted] ++ Mix.compilers(), 102 | ... 103 | ] 104 | end 105 | ``` 106 | 107 | Next, define a list of contexts available in the app inside config file: 108 | 109 | ```elixir 110 | config :contexted, contexts: [ 111 | # list of context modules goes here, for instance: 112 | # [App.Account, App.Subscription, App.Blog] 113 | ] 114 | ``` 115 | 116 | And that's it. From now on, whenever you will cross-reference one context with another, you will see an error raised during compilation. Here is an example of such an error: 117 | 118 | ``` 119 | == Compilation error in file lib/app/accounts.ex == 120 | ** (RuntimeError) You can't reference App.Blog context within App.Accounts context. 121 | ``` 122 | 123 | Read more about `Contexted.Tracer` and its options in [docs](https://hexdocs.pm/contexted/Contexted.Tracer.html). 124 | 125 |
126 | 127 | #### Exclude files and folders from cross-references context check 128 | 129 | In special cases, you may need to exclude certain folders or files from cross-reference checks due to project structure or naming conventions. To do this, add a list of exclusions in config `exclude_paths` option: 130 | 131 | ```elixir 132 | config :contexted, 133 | exclude_paths: ["app/test"] 134 | ``` 135 | 136 |
137 | 138 | ### Dividing each context into smaller parts 139 | 140 | To divide big Context into smaller Subcontexts, we can use `delegate_all/1` macro from `Contexted.Delegator` module. 141 | 142 | Let's assume that the `Account` context has `User`, `UserToken` and `Admin` resources. Here is how we can split the context module: 143 | 144 | ```elixir 145 | # Users subcontext 146 | 147 | defmodule App.Account.Users do 148 | def get_user(id) do 149 | ... 150 | end 151 | end 152 | 153 | # UserTokens subcontext 154 | 155 | defmodule App.Account.UserTokens do 156 | def get_user_token(id) do 157 | ... 158 | end 159 | end 160 | 161 | # Admins subcontext 162 | 163 | defmodule App.Account.Admins do 164 | def get_admin(id) do 165 | ... 166 | end 167 | end 168 | 169 | # Account context 170 | 171 | defmodule App.Account do 172 | import Contexted.Delegator 173 | 174 | delegate_all App.Account.Users 175 | delegate_all App.Account.UserTokens 176 | delegate_all App.Account.Admins 177 | end 178 | ``` 179 | 180 | From now on, you can treat the `Account` context module as the API for the "outside" world. 181 | 182 | Instead of calling: 183 | 184 | ```elixir 185 | App.Account.Users.find_user(1) 186 | ``` 187 | 188 | You will simply do: 189 | 190 | ```elixir 191 | App.Account.find_user(1) 192 | ``` 193 | 194 |
195 | 196 | #### Being able to access docs and specs in auto-delegated functions 197 | 198 | Both docs and specs are attached as metadata of module once it's compiled and saved as `.beam`. In reference to the example of `App.Account` context, it's possible that `App.Account.Users` will not be saved in `.beam` file before the `delegate_all` macro is executed. Therefore, first, all of the modules have to be compiled, and saved to `.beam` and only then we can create `@doc` and `@spec` of each delegated function. 199 | 200 | As a workaround, in `Contexted.Tracer.after_compiler/1` all of the contexts `.beam` files are first deleted and then recompiled. This is an opt-in functionality, as it extends compilation time. If you want to enable it, set the following config values: 201 | 202 | ```elixir 203 | config :contexted, 204 | app: :your_app_name, # replace 'your_app_name' with your real app name 205 | enable_recompilation: true 206 | ``` 207 | 208 | You may also want to enable it only for certain environments, like `dev`. 209 | 210 | *Please also note that when this functionality is enabled, during the recompilation process, warnings are temporarily silenced to avoid logging conflict warnings. It will still log warnings as intended, during the first compilation, therefore it won't have any affect on normal compilation flow.* 211 | 212 | Read more about `Contexted.Delegator` and its options in [docs](https://hexdocs.pm/contexted/Contexted.Delegator.html). 213 | 214 |
215 | 216 | ### Don't repeat yourself with CRUD operations 217 | 218 | In most web apps CRUD operations are very common. Most of these, have the same pattern. Why not autogenerate them? 219 | 220 | Here is how you can generate common CRUD operations for `App.Account.Users`: 221 | 222 | ```elixir 223 | defmodule App.Account.Users do 224 | use Contexted.CRUD, 225 | repo: App.Repo, 226 | schema: App.Accounts.User 227 | end 228 | ``` 229 | 230 | This will generate the following functions: 231 | 232 | ```elixir 233 | iex> App.Accounts.Users.__info__(:functions) 234 | [ 235 | change_user: 1, 236 | change_user: 2, 237 | create_user: 0, 238 | create_user: 1, 239 | create_user!: 0, 240 | create_user!: 1, 241 | delete_user: 1, 242 | delete_user!: 1, 243 | get_user: 1, 244 | get_user!: 1, 245 | list_users: 0, 246 | update_user: 1, 247 | update_user: 2, 248 | update_user!: 1, 249 | update_user!: 2 250 | ] 251 | ``` 252 | 253 | 254 | Read more about `Contexted.CRUD` and its options in [docs](https://hexdocs.pm/contexted/Contexted.CRUD.html). 255 | 256 |
257 | 258 | # 🤝 Contributing 259 | 260 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details. 261 | 262 | ### Development setup 263 | 264 | Just clone the repository, install dependencies normally, develop and run tests. For running tests and static analysis tools, refer to [GitHub Action workflow definition](.github/workflows/ci.yml). 265 | 266 |
267 | 268 | ## Media 269 | 270 | * [_Introducing Contexted Library_](https://www.youtube.com/watch?v=cjsyPausofM), S. Soppa, Curiosum Elixir Meetup #21, September 2023 271 | * [_Structuring Elixir & Phoenix project - our approach_](https://www.youtube.com/watch?v=oMFQ5N3WzfE), S. Soppa, Curiosum Elixir Meetup #1, January 2022 272 | * [_Introducing Contexted – Phoenix Contexts, Simplified_](https://curiosum.com/blog/introducing-contexted), Curiosum, February 2025 273 | * [_Context maintainability & guidelines in Elixir & Phoenix_](https://curiosum.com/blog/elixir-phoenix-context-maintainability-guildelines), Curiosum, December 2024 274 | 275 |
276 | 277 | ## Community 278 | 279 | - **Slack channel**: [Elixir Slack / #contexted](https://elixir-lang.slack.com/archives/C091Q5S0GDU) 280 | - **Issues**: [GitHub Issues](https://github.com/curiosum-dev/contexted/issues) 281 | - **Blog**: [Curiosum Blog](https://curiosum.com/blog?search=contexted) 282 | 283 |
284 | 285 | ## Contact 286 | 287 | * Library maintainers: [Szymon Soppa](https://github.com/szymon-soppa), [Michal Buszkiewicz](https://github.com/vincentvanbusk) 288 | * [**Curiosum**](https://curiosum.com) - Elixir development team behind Contexted 289 | 290 |
291 | 292 | # 📄 License 293 | 294 | Distributed under the MIT License. See [LICENSE](LICENSE) for more information. 295 | -------------------------------------------------------------------------------- /lib/contexted.ex: -------------------------------------------------------------------------------- 1 | defmodule Contexted do 2 | @moduledoc ~S""" 3 | 4 | Contexted helps you structure Phoenix contexts by enforcing separation, allowing modularization, and generating common CRUD operations. 5 | 6 | ## Features 7 | 8 | - `Contexted.Tracer` - trace and enforce definite separation between specific context modules. 9 | - `Contexted.Delegator` - divide the big context module into smaller parts and use delegations to build the final context. 10 | - `Contexted.CRUD` - auto-generate the most common CRUD operations whenever needed. 11 | 12 |
13 | 14 | ## Installation 15 | 16 | Add the following to your `mix.exs` file: 17 | 18 | ```elixir 19 | defp deps do 20 | [ 21 | {:contexted, "~> 0.3.4"} 22 | ] 23 | end 24 | ``` 25 | 26 | Then run `mix deps.get`. 27 | 28 |
29 | 30 | ## Step by step overview 31 | 32 | To describe a sample usage of this library, let's assume that your project has three contexts: 33 | 34 | - `Account` 35 | - `Subscription` 36 | - `Blog` 37 | 38 | Our goal, as the project grows, is to: 39 | 40 | 1. Keep contexts separate and not create any cross-references. For this to work, we'll raise errors during compilation whenever such a cross-reference happens. 41 | 2. Divide each context into smaller parts so that it is easier to maintain. In this case, we'll refer to each of these parts as **Subcontext**. It's not a new term added to the Phoenix framework but rather a term proposed to emphasize that it's a subset of Context. For this to work, we'll use delegates. 42 | 3. Not repeat ourselves with common business logic operations. For this to work, we'll be using CRUD functions generator, since these are the most common. 43 | 44 |
45 | 46 | ### Keep contexts separate 47 | 48 | It's very easy to monitor cross-references between context modules with the `contexted` library. 49 | 50 | First, add `contexted` as one of the compilers in _mix.exs_: 51 | 52 | ```elixir 53 | def project do 54 | [ 55 | ... 56 | compilers: [:contexted] ++ Mix.compilers(), 57 | ... 58 | ] 59 | end 60 | ``` 61 | 62 | Next, define a list of contexts available in the app inside config file: 63 | 64 | ```elixir 65 | config :contexted, contexts: [ 66 | # list of context modules goes here, for instance: 67 | # [App.Account, App.Subscription, App.Blog] 68 | ] 69 | ``` 70 | 71 | And that's it. From now on, whenever you will cross-reference one context with another, you will see an error raised during compilation. Here is an example of such an error: 72 | 73 | ``` 74 | == Compilation error in file lib/app/accounts.ex == 75 | ** (RuntimeError) You can't reference App.Blog context within App.Accounts context. 76 | ``` 77 | 78 | See more in [Contexted.Tracer](https://hexdocs.pm/contexted/Contexted.Tracer.html). 79 | 80 |
81 | 82 | #### Exclude files and folders from cross-references context check 83 | 84 | In special cases, you may need to exclude certain folders or files from cross-reference checks due to project structure or naming conventions. To do this, add a list of exclusions in config `exclude_paths` option: 85 | 86 | ```elixir 87 | config :contexted, 88 | exclude_paths: ["app/test"] 89 | ``` 90 | 91 |
92 | 93 | ### Dividing each context into smaller parts 94 | 95 | To divide big Context into smaller Subcontexts, we can use `delegate_all/1` macro from `Contexted.Delegator` module. 96 | 97 | Let's assume that the `Account` context has `User`, `UserToken` and `Admin` resources. Here is how we can split the context module: 98 | 99 | ```elixir 100 | # Users subcontext 101 | 102 | defmodule App.Account.Users do 103 | def get_user(id) do 104 | ... 105 | end 106 | end 107 | 108 | # UserTokens subcontext 109 | 110 | defmodule App.Account.UserTokens do 111 | def get_user_token(id) do 112 | ... 113 | end 114 | end 115 | 116 | # Admins subcontext 117 | 118 | defmodule App.Account.Admins do 119 | def get_admin(id) do 120 | ... 121 | end 122 | end 123 | 124 | # Account context 125 | 126 | defmodule App.Account do 127 | import Contexted.Delegator 128 | 129 | delegate_all App.Account.Users 130 | delegate_all App.Account.UserTokens 131 | delegate_all App.Account.Admins 132 | end 133 | ``` 134 | 135 | From now on, you can treat the `Account` context module as the API for the "outside" world. 136 | 137 | Instead of calling: 138 | 139 | ```elixir 140 | App.Account.Users.find_user(1) 141 | ``` 142 | 143 | You will simply do: 144 | 145 | ```elixir 146 | App.Account.find_user(1) 147 | ``` 148 | 149 |
150 | 151 | #### Being able to access docs and specs in auto-delegated functions 152 | 153 | Both docs and specs are attached as metadata of module once it's compiled and saved as `.beam`. In reference to the example of `App.Account` context, it's possible that `App.Account.Users` will not be saved in `.beam` file before the `delegate_all` macro is executed. Therefore, first, all of the modules have to be compiled, and saved to `.beam` and only then we can create `@doc` and `@spec` of each delegated function. 154 | 155 | As a workaround, in `Contexted.Tracer.after_compiler/1` all of the contexts `.beam` files are first deleted and then recompiled. This is an opt-in functionality, as it extends compilation time. If you want to enable it, set the following config values: 156 | 157 | ```elixir 158 | config :contexted, 159 | app: :your_app_name, # replace 'your_app_name' with your real app name 160 | enable_recompilation: true 161 | ``` 162 | 163 | You may also want to enable it only for certain environments, like `dev`. 164 | 165 | *Please also note that when this functionality is enabled, during the recompilation process, warnings are temporarily silenced to avoid logging conflict warnings. It will still log warnings as intended, during the first compilation, therefore it won't have any affect on normal compilation flow.* 166 | 167 | See more in [Contexted.Delegator](https://hexdocs.pm/contexted/Contexted.Delegator.html). 168 | 169 |
170 | 171 | ### Don't repeat yourself with CRUD operations 172 | 173 | In most web apps CRUD operations are very common. Most of these, have the same pattern. Why not autogenerate them? 174 | 175 | Here is how you can generate common CRUD operations for `App.Account.Users`: 176 | 177 | ```elixir 178 | defmodule App.Account.Users do 179 | use Contexted.CRUD, 180 | repo: App.Repo, 181 | schema: App.Accounts.User 182 | end 183 | ``` 184 | 185 | This will generate the following functions: 186 | 187 | ``` 188 | [ 189 | change_user: 1, 190 | change_user: 2, 191 | create_user: 0, 192 | create_user: 1, 193 | create_user!: 0, 194 | create_user!: 1, 195 | delete_user: 1, 196 | delete_user!: 1, 197 | get_user: 1, 198 | get_user!: 1, 199 | list_users: 0, 200 | update_user: 1, 201 | update_user: 2, 202 | update_user!: 1, 203 | update_user!: 2 204 | ] 205 | ``` 206 | 207 | See more in [Contexted.CRUD](https://hexdocs.pm/contexted/Contexted.CRUD.html). 208 | """ 209 | end 210 | -------------------------------------------------------------------------------- /lib/contexted/crud.ex: -------------------------------------------------------------------------------- 1 | defmodule Contexted.CRUD do 2 | @moduledoc """ 3 | The `Contexted.CRUD` module generates common [CRUD](https://pl.wikipedia.org/wiki/CRUD) (Create, Read, Update, Delete) functions for a context, similar to what `mix phx gen context` task generates. 4 | 5 | ## Options 6 | 7 | - `:repo` - The Ecto repository module used for database operations (required) 8 | - `:schema` - The Ecto schema module representing the resource that these CRUD operations will be generated for (required) 9 | - `:exclude` - A list of atoms representing the functions to be excluded from generation (optional) 10 | - `:plural_resource_name` - A custom plural version of the resource name to be used in function names (optional). If not provided, singular version with 's' ending will be used to generate list function 11 | 12 | ## Usage 13 | 14 | ```elixir 15 | defmodule MyApp.Accounts do 16 | use Contexted.CRUD, 17 | repo: MyApp.Repo, 18 | schema: MyApp.Accounts.User, 19 | exclude: [:delete], 20 | plural_resource_name: "users" 21 | end 22 | ``` 23 | 24 | This sample usage will generate all CRUD functions for `MyApp.Accounts.User` resource, excluding `delete_user/1`. 25 | 26 | ## Generated Functions 27 | 28 | The following functions are generated by default. Any of them can be excluded by adding their correspoding atom to the `:exclude` option. 29 | 30 | - `list_{plural resource name}` - Lists all resources in the schema. 31 | - `get_{resource name}` - Retrieves a resource by its ID. Returns `nil` if not found. 32 | - `get_{resource name}!` - Retrieves a resource by its ID. Raises an error if not found. 33 | - `create_{resource name}` - Creates a new resource with the provided attributes. Returns an `:ok` tuple with the resource or an `:error` tuple with changeset. 34 | - `create_{resource name}!` - Creates a new resource with the provided attributes. Raises an error if creation fails. 35 | - `update_{resource name}` - Updates an existing resource with the provided attributes. Returns an `:ok` tuple with the resource or an `:error` tuple with changeset. 36 | - `update_{resource name}!` - Updates an existing resource with the provided attributes. Raises an error if update fails. 37 | - `delete_{resource name}` - Deletes an existing resource. Returns an `:ok` tuple with the resource or an `:error` tuple with changeset. 38 | - `delete_{resource name}!` - Deletes an existing resource. Raises an error if delete fails. 39 | - `change_{resource name}` - Returns changeset for given resource. 40 | 41 | {resource name} and {plural resource name} will be replaced by the singular and plural forms of the resource name. 42 | """ 43 | 44 | defmacro __using__(opts) do 45 | # Expanding opts 46 | opts = Enum.map(opts, fn {key, val} -> {key, Macro.expand(val, __CALLER__)} end) 47 | 48 | repo = Keyword.fetch!(opts, :repo) 49 | schema = Keyword.fetch!(opts, :schema) 50 | 51 | exclude = Keyword.get(opts, :exclude, []) 52 | plural_resource_name = Keyword.get(opts, :plural_resource_name, nil) 53 | 54 | resource_name = schema |> Module.split() |> List.last() |> Macro.underscore() 55 | 56 | plural_resource_name = 57 | if plural_resource_name, do: plural_resource_name, else: "#{resource_name}s" 58 | 59 | # credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks 60 | quote bind_quoted: [ 61 | repo: repo, 62 | schema: schema, 63 | exclude: exclude, 64 | resource_name: resource_name, 65 | plural_resource_name: plural_resource_name 66 | ] do 67 | unless :list in exclude do 68 | function_name = String.to_atom("list_#{plural_resource_name}") 69 | 70 | @doc """ 71 | Returns a list of all #{plural_resource_name} from the database. 72 | 73 | ## Examples 74 | 75 | iex> list_#{plural_resource_name}() 76 | [%#{Macro.camelize(resource_name)}{}, ...] 77 | """ 78 | @spec unquote(function_name)() :: [%unquote(schema){}] 79 | def unquote(function_name)() do 80 | unquote(schema) 81 | |> unquote(repo).all() 82 | end 83 | end 84 | 85 | unless :get in exclude do 86 | function_name = String.to_atom("get_#{resource_name}") 87 | 88 | @doc """ 89 | Retrieves a single #{resource_name} by its ID from the database. Returns nil if the #{resource_name} is not found. 90 | 91 | ## Examples 92 | 93 | iex> get_#{resource_name}(id) 94 | %#{Macro.camelize(resource_name)}{} or nil 95 | """ 96 | 97 | @spec unquote(function_name)(integer() | String.t()) :: %unquote(schema){} | nil 98 | def unquote(function_name)(id) do 99 | unquote(schema) 100 | |> unquote(repo).get(id) 101 | end 102 | 103 | function_name = String.to_atom("get_#{resource_name}!") 104 | 105 | @doc """ 106 | Retrieves a single #{resource_name} by its ID from the database. Raises an error if the #{resource_name} is not found. 107 | 108 | ## Examples 109 | 110 | iex> get_#{resource_name}!(id) 111 | %#{Macro.camelize(resource_name)}{} or raises Ecto.NoResultsError 112 | """ 113 | 114 | @spec unquote(function_name)(integer() | String.t()) :: %unquote(schema){} 115 | def unquote(function_name)(id) do 116 | unquote(schema) 117 | |> unquote(repo).get!(id) 118 | end 119 | end 120 | 121 | unless :create in exclude do 122 | function_name = String.to_atom("create_#{resource_name}") 123 | 124 | @doc """ 125 | Creates a new #{resource_name} with the provided attributes. 126 | 127 | Returns an `:ok` tuple with the #{resource_name} if successful, or an `:error` tuple with a changeset if not. 128 | 129 | ## Examples 130 | 131 | iex> create_#{resource_name}(attrs) 132 | {:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}} 133 | """ 134 | 135 | @spec unquote(function_name)(map()) :: {:ok, %unquote(schema){}} | {:error, map()} 136 | def unquote(function_name)(attrs \\ %{}) do 137 | %unquote(schema){} 138 | |> unquote(schema).changeset(attrs) 139 | |> unquote(repo).insert() 140 | end 141 | 142 | function_name = String.to_atom("create_#{resource_name}!") 143 | 144 | @doc """ 145 | Creates a new #{resource_name} with the provided attributes. 146 | 147 | Returns the #{resource_name} if successful, or raises an error if not. 148 | 149 | ## Examples 150 | 151 | iex> create_#{resource_name}!(attrs) 152 | %#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError 153 | """ 154 | 155 | @spec unquote(function_name)(map()) :: %unquote(schema){} 156 | def unquote(function_name)(attrs \\ %{}) do 157 | %unquote(schema){} 158 | |> unquote(schema).changeset(attrs) 159 | |> unquote(repo).insert!() 160 | end 161 | end 162 | 163 | unless :update in exclude do 164 | function_name = String.to_atom("update_#{resource_name}") 165 | 166 | @doc """ 167 | Updates an existing #{resource_name} with the provided attributes. 168 | 169 | Returns an `:ok` tuple with the updated #{resource_name} if successful, or an `:error` tuple with a changeset if not. 170 | 171 | ## Examples 172 | 173 | iex> update_#{resource_name}(#{resource_name}, attrs) 174 | {:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}} 175 | """ 176 | 177 | @spec unquote(function_name)(%unquote(schema){}, map()) :: 178 | {:ok, %unquote(schema){}} | {:error, map()} 179 | def unquote(function_name)(record, attrs \\ %{}) do 180 | record 181 | |> unquote(schema).changeset(attrs) 182 | |> unquote(repo).update() 183 | end 184 | 185 | function_name = String.to_atom("update_#{resource_name}!") 186 | 187 | @doc """ 188 | Updates an existing #{resource_name} with the provided attributes. 189 | 190 | Returns the updated #{resource_name} if successful, or raises an error if not. 191 | 192 | ## Examples 193 | 194 | iex> update_#{resource_name}!(#{resource_name}, attrs) 195 | %#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError 196 | """ 197 | 198 | @spec unquote(function_name)(%unquote(schema){}, map()) :: %unquote(schema){} 199 | def unquote(function_name)(record, attrs \\ %{}) do 200 | record 201 | |> unquote(schema).changeset(attrs) 202 | |> unquote(repo).update!() 203 | end 204 | end 205 | 206 | unless :delete in exclude do 207 | function_name = String.to_atom("delete_#{resource_name}") 208 | 209 | @doc """ 210 | Deletes an existing #{resource_name}. 211 | 212 | Returns an `:ok` tuple with the deleted #{resource_name} if successful, or an `:error` tuple with a changeset if not. 213 | 214 | ## Examples 215 | 216 | iex> delete_#{resource_name}(#{resource_name}) 217 | {:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}} 218 | """ 219 | 220 | @spec unquote(function_name)(%unquote(schema){}) :: 221 | {:ok, %unquote(schema){}} | {:error, map()} 222 | def unquote(function_name)(record) do 223 | record 224 | |> unquote(repo).delete() 225 | end 226 | 227 | function_name = String.to_atom("delete_#{resource_name}!") 228 | 229 | @spec unquote(function_name)(%unquote(schema){}) :: {:ok, %unquote(schema){}} 230 | def unquote(function_name)(record) do 231 | record 232 | |> unquote(repo).delete!() 233 | end 234 | end 235 | 236 | unless :change in exclude do 237 | function_name = String.to_atom("change_#{resource_name}") 238 | 239 | @doc """ 240 | Deletes an existing #{resource_name}. 241 | 242 | Returns the deleted #{resource_name} if successful, or raises an error if not. 243 | 244 | ## Examples 245 | 246 | iex> delete_#{resource_name}!(#{resource_name}) 247 | %#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError 248 | """ 249 | 250 | @spec unquote(function_name)(%unquote(schema){}, map()) :: map() 251 | def unquote(function_name)(record, attrs \\ %{}) do 252 | record 253 | |> unquote(schema).changeset(attrs) 254 | end 255 | end 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /lib/contexted/delegator.ex: -------------------------------------------------------------------------------- 1 | defmodule Contexted.Delegator do 2 | @moduledoc """ 3 | The `Contexted.Delegator` module provides a macro to delegate all functions defined within a specific module. 4 | 5 | This module can be used to forward all function calls from one module to another without the need to write 6 | individual `defdelegate` statements for each function. 7 | 8 | ## Usage 9 | 10 | To use the `delegate_all` macro, simply include it in your module and pass the target module as an argument: 11 | 12 | defmodule MyContextModule do 13 | import Contexted.Delegator 14 | 15 | delegate_all MyTargetSubcontextModule 16 | end 17 | 18 | All public functions defined in `MyTargetSubcontextModule` will now be accessible within `MyContextModule`. 19 | 20 | Note: This macro should be used with caution, as it may lead to unexpected behaviors if two modules with overlapping function names are delegated. 21 | 22 | ## Additional configuration 23 | 24 | The `Contexted.Delegator` module can be used without `Mix.Tasks.Compile.Contexted` as one of the compilers. 25 | 26 | However, if you wish to enable automatic `@doc` and `@spec` generation for delegated functions, you will need to set the following config: 27 | 28 | config :contexted, 29 | enable_recompilation: true 30 | """ 31 | 32 | alias Contexted.{ModuleAnalyzer, Utils} 33 | 34 | @doc """ 35 | Delegates all public functions of the given module. 36 | 37 | ## Examples 38 | 39 | defmodule MyContextModule do 40 | import Contexted.Delegator 41 | 42 | delegate_all MyTargetSubcontextModule 43 | end 44 | 45 | All public functions defined in `MyTargetSubcontextModule` will now be accessible within `MyContextModule`. 46 | """ 47 | defmacro delegate_all(module) do 48 | # Ensure the module is an atom 49 | module = 50 | case module do 51 | {:__aliases__, _, _} -> 52 | Macro.expand(module, __CALLER__) 53 | 54 | _ -> 55 | module 56 | end 57 | 58 | functions_docs = ModuleAnalyzer.get_functions_docs(module) 59 | functions_specs = ModuleAnalyzer.get_functions_specs(module) 60 | 61 | # Get the module's public functions 62 | functions = 63 | module.__info__(:functions) 64 | |> Enum.filter(fn {name, arity} -> :erlang.function_exported(module, name, arity) end) 65 | |> Enum.map(fn {name, arity} -> 66 | args = ModuleAnalyzer.generate_random_function_arguments(arity) 67 | doc = ModuleAnalyzer.get_function_doc(functions_docs, name, arity) 68 | spec = ModuleAnalyzer.get_function_spec(functions_specs, name, arity) 69 | 70 | {name, arity, args, doc, spec} 71 | end) 72 | 73 | # Generate the defdelegate AST for each function 74 | delegates = 75 | Enum.map(functions, fn {name, _arity, args, doc, spec} -> 76 | quote do 77 | if unquote(doc), do: unquote(Code.string_to_quoted!(doc)) 78 | 79 | if unquote(spec) && Utils.recompilation_enabled?(), 80 | do: unquote(Code.string_to_quoted!(spec)) 81 | 82 | defdelegate unquote(name)(unquote_splicing(args)), 83 | to: unquote(module), 84 | as: unquote(name) 85 | end 86 | end) 87 | 88 | types = 89 | case Code.Typespec.fetch_types(module) do 90 | {:ok, types} -> 91 | Enum.map(types, fn 92 | {type_of_type, type} -> 93 | Code.Typespec.type_to_quoted(type) 94 | |> add_ast_for_type(type_of_type) 95 | 96 | _ -> 97 | nil 98 | end) 99 | 100 | _ -> 101 | [] 102 | end 103 | 104 | # Combine the generated delegates into a single AST 105 | quote do 106 | (unquote_splicing(types)) 107 | (unquote_splicing(delegates)) 108 | end 109 | end 110 | 111 | @spec add_ast_for_type(tuple(), atom()) :: tuple() 112 | defp add_ast_for_type(ast, type_of_type) do 113 | {:@, [context: Elixir, imports: [{1, Kernel}]], 114 | [ 115 | {type_of_type, [context: Elixir], 116 | [ 117 | ast 118 | ]} 119 | ]} 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/contexted/mix/tasks/compile/contexted.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.Contexted do 2 | use Mix.Task.Compiler 3 | 4 | alias Contexted.{Tracer, Utils} 5 | alias Mix.Task.Compiler 6 | 7 | @moduledoc """ 8 | A custom Elixir compiler task that checks for cross-references between specific modules, known as "contexts". 9 | """ 10 | 11 | @contexts Application.compile_env(:contexted, :contexts, []) 12 | 13 | @doc """ 14 | Sets the custom compiler tracer to the Contexted.Tracer module. 15 | """ 16 | @spec run(any()) :: :ok 17 | def run(_argv) do 18 | if Enum.count(@contexts) > 0 do 19 | if Utils.recompilation_enabled?() do 20 | Compiler.after_compiler(:app, &Tracer.after_compiler/1) 21 | end 22 | 23 | tracers = Code.get_compiler_option(:tracers) 24 | Code.put_compiler_option(:tracers, [Tracer | tracers]) 25 | else 26 | :ok 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/contexted/module_analyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Contexted.ModuleAnalyzer do 2 | @moduledoc """ 3 | The `Contexted.ModuleAnalyzer` defines utils functions that analyze and extract information from other modules. 4 | """ 5 | 6 | @doc """ 7 | Fetches the `@doc` definitions for all functions within the given module. 8 | """ 9 | @spec get_functions_docs(module()) :: [tuple()] 10 | def get_functions_docs(module) do 11 | case Code.fetch_docs(module) do 12 | {:docs_v1, _, _, _, _, _, functions_docs} -> 13 | Enum.filter(functions_docs, fn 14 | {{:function, _, _}, _, _, _, _} -> true 15 | {_, _, _, _, _} -> false 16 | end) 17 | 18 | _ -> 19 | [] 20 | end 21 | end 22 | 23 | @doc """ 24 | Fetches the `@spec` definitions for all functions within the given module. 25 | """ 26 | @spec get_functions_specs(module()) :: [tuple()] 27 | def get_functions_specs(module) do 28 | case Code.Typespec.fetch_specs(module) do 29 | {:ok, specs} -> specs 30 | _ -> [] 31 | end 32 | end 33 | 34 | @doc """ 35 | Finds and returns the `@spec` definition in string format for the specified function name and arity. 36 | Returns `nil` if the function is not found in the specs. 37 | """ 38 | @spec get_function_spec([tuple()], atom(), non_neg_integer()) :: String.t() | nil 39 | def get_function_spec(specs, function_name, arity) do 40 | # Find the spec tuple in the specs 41 | spec = find_spec(specs, function_name, arity) 42 | 43 | # If spec is found, build the spec expression 44 | if spec do 45 | build_spec(spec) 46 | else 47 | nil 48 | end 49 | end 50 | 51 | @doc """ 52 | Finds and returns the `@doc` definition in string format for the specified function name and arity. 53 | Returns `nil` if the function is not found in the function docs. 54 | """ 55 | @spec get_function_doc([tuple()], atom(), non_neg_integer()) :: String.t() | nil 56 | def get_function_doc(functions_docs, name, arity) do 57 | Enum.find(functions_docs, fn 58 | {{:function, func_name, func_arity}, _, _, _, _} -> 59 | func_name == name && func_arity == arity 60 | end) 61 | |> case do 62 | {_, _, _, %{"en" => doc}, _} -> 63 | "@doc ~S\"\"\"\n#{doc}\n\"\"\"" 64 | 65 | _ -> 66 | nil 67 | end 68 | end 69 | 70 | @doc """ 71 | Generates a list of unique argument names based on the given arity. 72 | """ 73 | @spec generate_random_function_arguments(non_neg_integer()) :: [atom()] 74 | def generate_random_function_arguments(arity) do 75 | if arity > 0 do 76 | Enum.map(0..(arity - 1), &{String.to_atom("arg_#{&1}"), [], nil}) 77 | else 78 | [] 79 | end 80 | end 81 | 82 | @spec find_spec([tuple()], atom(), non_neg_integer()) :: tuple() | nil 83 | defp find_spec(specs, function_name, arity) do 84 | Enum.find(specs, fn 85 | {{^function_name, ^arity}, _} -> true 86 | _ -> false 87 | end) 88 | end 89 | 90 | @spec build_spec(tuple()) :: String.t() 91 | defp build_spec({{function_name, _arity}, specs}) do 92 | Enum.map_join(specs, "\n", fn spec -> 93 | Code.Typespec.spec_to_quoted(function_name, spec) 94 | |> add_spec_ast() 95 | |> Macro.to_string() 96 | end) 97 | end 98 | 99 | @spec add_spec_ast(tuple()) :: tuple() 100 | defp add_spec_ast(ast) do 101 | {:@, [context: Elixir, imports: [{1, Kernel}]], 102 | [ 103 | {:spec, [context: Elixir], 104 | [ 105 | ast 106 | ]} 107 | ]} 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/contexted/tracer.ex: -------------------------------------------------------------------------------- 1 | defmodule Contexted.Tracer do 2 | @moduledoc """ 3 | The `Contexted.Tracer` module provides a set of functions to trace and enforce separation between specific modules, known as "contexts". 4 | 5 | This is useful when you want to ensure that certain modules do not reference each other directly within your application. 6 | """ 7 | 8 | alias Contexted.Utils 9 | 10 | @doc """ 11 | Trace events are emitted during compilation. 12 | 13 | `trace` function verifies if the provided event contains cross-references between two contexts. 14 | 15 | If so, raises an error with an appropriate message. 16 | """ 17 | @spec trace(tuple(), map()) :: :ok 18 | def trace({action, _meta, module, _name, _arity}, env) 19 | when action in [:imported_function, :remote_function, :remote_macro] do 20 | verify_modules_mismatch(env.module, module, env.file) 21 | end 22 | 23 | def trace({action, _meta, module, _opts}, env) when action in [:require] do 24 | verify_modules_mismatch(env.module, module, env.file) 25 | end 26 | 27 | def trace(_event, _env), do: :ok 28 | 29 | @doc """ 30 | To support automatic docs and specs generation in delegated functions inside contexts, all of the modules have to be compiled first. 31 | 32 | Because of that, we need to run this operation in after compiler callback in two steps: 33 | 34 | 1. Remove all contexts beam files. 35 | 2. Generate contexts beam files again. 36 | 37 | This is an opt-in step, so in order to enable it, you will have to set this config: 38 | 39 | config :contexted, 40 | enable_recompilation: true 41 | """ 42 | @spec after_compiler(tuple()) :: tuple() 43 | def after_compiler({status, diagnostics}) do 44 | file_paths = remove_context_beams_and_return_module_paths() 45 | beam_folder = get_beam_files_folder_path() 46 | 47 | silence_recompilation_warnings(fn -> 48 | Kernel.ParallelCompiler.compile_to_path(file_paths, beam_folder) 49 | end) 50 | 51 | {status, diagnostics} 52 | end 53 | 54 | @spec get_beam_files_folder_path() :: String.t() 55 | def get_beam_files_folder_path do 56 | build_sub_path = Mix.Project.build_path() 57 | app_sub_path = Utils.get_config_app() |> Atom.to_string() 58 | 59 | Path.join([build_sub_path, "lib", app_sub_path, "ebin"]) 60 | end 61 | 62 | @spec remove_context_beams_and_return_module_paths :: list(String.t()) 63 | defp remove_context_beams_and_return_module_paths do 64 | Utils.get_config_contexts() 65 | |> Enum.map(fn module -> 66 | file_path = module.__info__(:compile)[:source] |> List.to_string() 67 | 68 | :code.which(module) |> List.to_string() |> File.rm() 69 | 70 | file_path 71 | end) 72 | end 73 | 74 | @spec verify_modules_mismatch(module(), module(), String.t()) :: :ok 75 | defp verify_modules_mismatch(analyzed_module, referenced_module, file) do 76 | if file_excluded_from_check?(file) do 77 | :ok 78 | else 79 | analyzed_context_module = map_module_to_context_module(analyzed_module) 80 | referenced_context_module = map_module_to_context_module(referenced_module) 81 | 82 | if analyzed_context_module != nil and 83 | referenced_context_module != nil and 84 | analyzed_context_module != referenced_context_module do 85 | stringed_referenced_context_module = 86 | Atom.to_string(referenced_context_module) |> String.replace("Elixir.", "") 87 | 88 | stringed_analyzed_context_module = 89 | Atom.to_string(analyzed_context_module) |> String.replace("Elixir.", "") 90 | 91 | raise "You can't reference #{stringed_referenced_context_module} context within #{stringed_analyzed_context_module} context." 92 | else 93 | :ok 94 | end 95 | end 96 | end 97 | 98 | @spec map_module_to_context_module(module()) :: module() | nil 99 | defp map_module_to_context_module(module) do 100 | Utils.get_config_contexts() 101 | |> Enum.find(fn context -> 102 | regex = build_regex(context) 103 | stringified_module = Atom.to_string(module) 104 | 105 | Regex.match?(regex, stringified_module) 106 | end) 107 | end 108 | 109 | @spec file_excluded_from_check?(String.t()) :: boolean() 110 | defp file_excluded_from_check?(file) do 111 | Utils.get_config_exclude_paths() 112 | |> Enum.any?(&String.contains?(file, &1)) 113 | end 114 | 115 | @spec build_regex(module()) :: Regex.t() 116 | defp build_regex(context) do 117 | context 118 | |> Atom.to_string() 119 | |> then(&~r/\b#{&1}\b/) 120 | end 121 | 122 | @spec silence_recompilation_warnings((-> any())) :: any() 123 | defp silence_recompilation_warnings(fun) do 124 | original_logger_level = Logger.level() 125 | original_compiler_options = Code.compiler_options() 126 | 127 | Logger.configure(level: :error) 128 | Code.compiler_options(ignore_module_conflict: true) 129 | 130 | try do 131 | fun.() 132 | after 133 | Logger.configure(level: original_logger_level) 134 | Code.compiler_options(original_compiler_options) 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/contexted/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Contexted.Utils do 2 | @moduledoc """ 3 | The `Contexted.Utils` defines utils functions for other modules. 4 | """ 5 | 6 | @doc """ 7 | Checks is `enable_recompilation` option is set. 8 | """ 9 | @spec recompilation_enabled? :: boolean() 10 | def recompilation_enabled?, 11 | do: get_from_config(:enable_recompilation, false) && get_from_config(:app, false) 12 | 13 | @doc """ 14 | Returns `contexts` option value from contexted config or `[]` if it's not set. 15 | """ 16 | @spec get_config_contexts :: list(module()) 17 | def get_config_contexts, do: get_from_config(:contexts, []) 18 | 19 | @doc """ 20 | Returns `exclude_paths` option value from contexted config or [] if it's not set. 21 | """ 22 | @spec get_config_exclude_paths :: list(String.t()) 23 | def get_config_exclude_paths, do: get_from_config(:exclude_paths, []) 24 | 25 | @spec get_config_app :: :atom 26 | def get_config_app, do: get_from_config(:app, nil) 27 | 28 | @spec get_from_config(atom(), any()) :: any() 29 | defp get_from_config(option_name, default_value) do 30 | Application.get_env(:contexted, option_name, default_value) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Contexted.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.3.4" 5 | @github_url "https://github.com/curiosum-dev/contexted" 6 | 7 | def project do 8 | [ 9 | app: :contexted, 10 | description: 11 | "Contexted is an Elixir library designed to streamline the management of complex Phoenix contexts in your projects, offering tools for module separation, subcontext creation, and auto-generating CRUD operations for improved code maintainability.", 12 | version: @version, 13 | elixir: "~> 1.15", 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps(), 16 | package: package(), 17 | docs: docs() 18 | ] 19 | end 20 | 21 | # Run "mix help compile.app" to learn about applications. 22 | def application do 23 | [ 24 | extra_applications: [:logger] 25 | ] 26 | end 27 | 28 | # Run "mix help deps" to learn about dependencies. 29 | defp deps do 30 | [ 31 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 32 | {:ex_doc, "~> 0.25", only: :dev, runtime: false}, 33 | {:versioce, "~> 2.0.0", only: :dev, runtime: false} 34 | ] 35 | end 36 | 37 | defp package do 38 | [ 39 | licenses: ["MIT"], 40 | maintainers: ["Curiosum"], 41 | links: %{"GitHub" => @github_url}, 42 | files: ~w(lib mix.exs README.md LICENSE) 43 | ] 44 | end 45 | 46 | defp docs do 47 | [ 48 | main: "Contexted", 49 | source_ref: "v#{@version}", 50 | source_url: @github_url, 51 | groups_for_modules: [ 52 | Setup: [ 53 | Contexted 54 | ], 55 | Features: [ 56 | Contexted.Tracer, 57 | Contexted.Delegator, 58 | Contexted.CRUD 59 | ], 60 | Helpers: [ 61 | Contexted.ModuleAnalyzer 62 | ], 63 | Utils: [ 64 | Contexted.Utils 65 | ] 66 | ] 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 5 | "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 6 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 7 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 8 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 9 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 10 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 11 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 12 | "versioce": {:hex, :versioce, "2.0.0", "a31b5e7b744d0d4a3694dd6fe4c0ee403e969631789e73cbd2a3367246404948", [:mix], [{:git_cli, "~> 0.3.0", [hex: :git_cli, repo: "hexpm", optional: true]}], "hexpm", "b2112ce621cd40fe23ad957a3dd82bccfdfa33c9a7f1e710a44b75ae772186cc"}, 13 | } 14 | -------------------------------------------------------------------------------- /test/context_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ContextedTest do 2 | use ExUnit.Case 3 | doctest Contexted 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------