-
-
Notifications
You must be signed in to change notification settings - Fork 35
improvement: within tests and docs #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# Writing Installers | ||
|
||
Generate the install task: | ||
``` | ||
mix igniter.gen.task my_dep.install | ||
``` | ||
All of the work will be done in the igniter call: | ||
|
||
```elixir | ||
@impl Igniter.Mix.Task | ||
def igniter(igniter) do | ||
# Do your work here and return an updated igniter | ||
igniter | ||
|> Igniter.add_warning("mix routex.install is not yet implemented") | ||
end | ||
``` | ||
|
||
## Fetching Dependency | ||
|
||
```elixir | ||
if Igniter.Project.Deps.has_dep?(igniter, :my_dep) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So you don't have to do this typically. I'd say let's remove this example. If your installer should install additional packages or add additional dependencies, that should be configured in the installer. i.e You don't need to add the specific dep that you are installing, as igniter handles that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔
No changes detected on Refering to this Am I missing something? :thinkies: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In tests we use |
||
igniter | ||
else | ||
igniter | ||
|> Igniter.Project.Deps.add_dep({:my_dep, "~> 1.0"}) | ||
|> Igniter.apply_and_fetch_dependencies(error_on_abort?: true, yes_to_deps: true) | ||
end | ||
``` | ||
|
||
Check does the dependency already exist and if not add it and apply it. | ||
|
||
|
||
## Creating a module | ||
|
||
```elixir | ||
Igniter.Project.Module.create_module(igniter, module, """ | ||
use MyDep.Module | ||
""") | ||
``` | ||
|
||
Create a new module that uses some module from your application. | ||
|
||
## Modifying a module | ||
|
||
For example: | ||
|
||
```elixir | ||
defmodule SomeModule do | ||
alias Some.Example | ||
|
||
def some_function(a, b) do | ||
Example.function(a, b) | ||
end | ||
end | ||
``` | ||
|
||
Find that module and update it: | ||
|
||
```elixir | ||
Igniter.Project.Module.find_and_update_module!(igniter, SomeModule, fn zipper -> | ||
{:ok, igniter} | ||
end) | ||
``` | ||
|
||
In the function block use [`within/2`](https://hexdocs.pm/igniter/Igniter.Code.Common.html#within/2) to do multiple modifications. Find the part you want to change and modify it. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this tip, but let's maybe put it in its own section called like "making multiple modifications to a zipper"? Something good to describe also is the general concept of: Igniter provides a high level API for working with a data structure called a "zipper". This data structure is defined by sourceror (link to sourceror), and at a certain level of generator complexity, you will inevitably end up working with zippers. We recommend reading through sourcerer's documentation. While we provide many tools for working with source code ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So much to learn 🧠 I should put this back to draft. ✔️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're doing great 😄 This is an important guide to get right and I'm grateful you're putting in the effort here 🙇 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I just realized we have a "Writing generators" guide: https://github.com/ash-project/igniter/blob/main/documentation/writing-generators.md Maybe lets merge these with the name "Writing installers and generators" 😄 |
||
|
||
For example: | ||
- replace alias with an import: | ||
```elixir | ||
Igniter.Code.Common.within(fn zipper -> | ||
pred = &match?(%Zipper{node: {:alias, _, _}}, &1) | ||
zipper = Common.remove(zipper, pred) | ||
line = "import Some.Example, only: [:function]" | ||
{:ok, Common.add_code(zipper, line, placement: :before)} | ||
end) | ||
``` | ||
|
||
- replace function block: | ||
```elixir | ||
Igniter.Code.Common.within(fn zipper -> | ||
{:ok, zipper} = move_to_function(zipper, :some_function) | ||
{:ok, zipper} = Common.move_to_do_block(zipper) | ||
line = "my_private_function!(function(a, b))" | ||
{:ok, Igniter.Code.Common.replace_code(zipper, line)} | ||
end) | ||
``` | ||
|
||
- add a block: | ||
```elixir | ||
Igniter.Code.Common.within(fn zipper -> | ||
block = """ | ||
defp private_function!({:ok, result}), do: result | ||
defp private_function!(_), do: raise "Something went wrong!" | ||
""" | ||
|
||
{:ok, Common.add_code(zipper, block, placement: :after)} | ||
end) | ||
``` | ||
|
||
You can chain within blocks using `with`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
defmodule Igniter.Code.WithinTest do | ||
alias Igniter.Code.Common | ||
alias Sourceror.Zipper | ||
use ExUnit.Case | ||
import Igniter.Test | ||
|
||
describe "with within" do | ||
test "performs multiple changes to some module" do | ||
test_project() | ||
|> Igniter.create_new_file("lib/some_module.ex", """ | ||
defmodule SomeModule do | ||
alias Some.Example | ||
|
||
def some_function(a, b) do | ||
Example.function(a, b) | ||
end | ||
end | ||
""") | ||
|> apply_igniter!() | ||
|> Igniter.Project.Module.find_and_update_module!(SomeModule, fn zipper -> | ||
zipper | ||
|> within!(fn zipper -> | ||
pred = &match?(%Zipper{node: {:alias, _, _}}, &1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We never really want to match on the zipper node if we can avoid it. For this example I'd pick something exactly matching for simplicity, like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, ignore that, use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even better: Igniter.Code.Function.function_call?(zipper, :alias, 1, &Igniter.Code.Function.argument_equals?(&1, 0, The.Module.To.Replace)
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your example using The way I would typically recommend doing it is instead of using remove and add, you would move to the location of the node you want to modify and then replace that node. case move_to_my_alias(zipper) do
{:ok, zipper} ->
{:ok, Igniter.Code.Common.replace_code(zipper, "import Some.Example, only: [:function]")}
:error ->
{:ok, zipper}
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I knew my way wasn't the right way 😂 But this way I'll learn the right way. 💯 |
||
zipper = Common.remove(zipper, pred) | ||
line = "import Some.Example, only: [:function]" | ||
{:ok, Common.add_code(zipper, line, placement: :before)} | ||
end) | ||
|> within!(fn zipper -> | ||
{:ok, zipper} = move_to_function(zipper, :some_function) | ||
{:ok, zipper} = Common.move_to_do_block(zipper) | ||
line = "my_private_function!(function(a, b))" | ||
{:ok, Igniter.Code.Common.replace_code(zipper, line)} | ||
end) | ||
|> within!(fn zipper -> | ||
block = """ | ||
defp private_function!({:ok, result}), do: result | ||
defp private_function!(_), do: raise "Something went wrong!" | ||
""" | ||
|
||
{:ok, Common.add_code(zipper, block, placement: :after)} | ||
end) | ||
|> then(fn zipper -> {:ok, zipper} end) | ||
end) | ||
|> Igniter.Refactors.Rename.rename_function( | ||
{SomeModule, :some_function}, | ||
{SomeModule, :some_function!}, | ||
arity: 2 | ||
) | ||
|> assert_has_patch("lib/some_module.ex", """ | ||
|defmodule SomeModule do | ||
- | alias Some.Example | ||
+ | import Some.Example, only: [:function] | ||
| | ||
- | def some_function(a, b) do | ||
- | Example.function(a, b) | ||
+ | def some_function!(a, b) do | ||
+ | my_private_function!(function(a, b)) | ||
| end | ||
+ | | ||
+ | defp private_function!({:ok, result}), | ||
+ | do: result | ||
+ | | ||
+ | defp private_function!(_), | ||
+ | do: raise("Something went wrong!") | ||
|end | ||
""") | ||
end | ||
end | ||
|
||
defp within!(zipper, function) do | ||
case Common.within(zipper, function) do | ||
{:ok, zipper} -> zipper | ||
:error -> raise "Error calling within" | ||
end | ||
end | ||
|
||
defp move_to_function(zipper, function) do | ||
Igniter.Code.Common.move_to(zipper, fn zipper -> | ||
Igniter.Code.Function.function_call?(zipper, function) | ||
end) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets call this guide "writing installers & generators".
The logic is essentially the same, the only thing is that if your task is called
<your_library>.install
then it will be invoked whenmix igniter.install
is called.