├── .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 | [](https://curiosum.com/contact)
8 | [](https://curiosum.com/services/elixir-software-development)
9 | [](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 | [](https://hex.pm/packages/contexted)
21 | [](https://github.com/curiosum-dev/contexted/actions)
22 | [](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 |
--------------------------------------------------------------------------------