diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d6e74..8caf0f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2025-01-28 +## [1.1.0] - 2025-08-27 + +### Added +- Support for external module callbacks with `@then {Module, :function}` syntax +- Comprehensive validation for callback formats with clear error messages +- Tests covering external module callbacks, mixed callback types, and error cases + +## [1.0.0] - 2025-08-26 ### Added - Initial release of `Then` library diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..5a5b01f --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-08-26 + +### Added +- Initial release of `Then` library +- `@then` attribute for post-execution callbacks +- Support for side-effect separation from main function logic +- Compile-time validation to prevent multiple `@then` attributes per function +- Compatibility with other function attributes (`@doc`, `@spec`, `@deprecated`) +- Support for private callback functions (`defp`) +- Comprehensive test suite +- Documentation with examples and limitations + +### Features +- Clean separation of concerns between pure functions and side effects +- Automatic callback invocation after function execution +- Function result preservation (callbacks don't modify return values) +- Multi-clause function support +- Macro-based implementation for zero runtime overhead diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..818a667 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,23 @@ +# License + +MIT License + +Copyright (c) 2025 bardoor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/then.md b/docs/then.md new file mode 100644 index 0000000..4c2ec05 --- /dev/null +++ b/docs/then.md @@ -0,0 +1,126 @@ +# Then + +Because sometimes you want to do something *after* a function, but don't want to clutter its code. + +Put `@then :callback` or `@then {Module, :callback}` above a function and it will automatically +call the callback after execution. Function result stays unchanged, callback is called for side effects. + +## Installation + +```elixir +def deps do + [{:then, "~> 1.1.0"}] +end +``` + +## Basic Usage + +### Simple Local Callbacks + +```elixir +defmodule MyModule do + use Then + + @then :log + def add(a, b) do + a + b + end + + def log(result) do + IO.puts("Got #{result}") + end +end + +MyModule.add(2, 3) +# Got 5 +# => 5 +``` + +### External Module Callbacks + +You can also call functions from other modules: + +```elixir +defmodule Calculator do + use Then + + @then {Logger, :info} + def multiply(a, b) do + a * b + end + + @then {MyAudit, :track_operation} + def divide(a, b) when b != 0 do + a / b + end +end + +defmodule MyAudit do + def track_operation(result) do + IO.puts("Operation completed with result: #{result}") + end +end +``` + +### Real-world Example + +```elixir +defmodule UserService do + use Then + + @then :audit_creation + def new_user(params) do + case params do + %{email: email, name: name} -> {:ok, %User{name: name, email: email}} + _ -> {:error, "Required fields are missing"} + end + end + + # side effects separately + def audit_creation({:ok, user}), do: Logger.info("User #{user.email} created") + def audit_creation({:error, reason}), do: Logger.warn("User wasn't created. #{reason}") +end +``` + +### Compatibility + +Works perfectly with other function attributes: + +```elixir +defmodule MyService do + use Then + + @doc "Gets age from params" + @spec get_age(map()) :: integer() + @then :log_term + def get_age(params) do + params[:age] || 0 + end + + defp log_term(term), do: IO.puts("[log-term] #{term}") +end +``` + +`@spec`, `@doc`, `@deprecated` and other attributes work as expected. +Callback functions can be private (`defp`). + +### Limitations + +- One `@then` per function (compilation error if you try to use multiple) +- Callback is not called if function raises an exception +- For functions with multiple clauses, `@then` applies to all clauses +- External module callbacks must be available at compile time + +### Callback Formats + +`@then` accepts two formats: +- `:function_name` - calls local function +- `{ModuleName, :function_name}` - calls function from external module + +### Why Use This? + +It's simple and clear. Move log and other side-effects out of your beautiful logic. + +## License + +`Then` is released under the MIT License - see the [LICENSE](license.html) file. diff --git a/lib/then.ex b/lib/then.ex index 1327740..2947982 100644 --- a/lib/then.ex +++ b/lib/then.ex @@ -1,76 +1,11 @@ defmodule Then do @moduledoc """ - Because sometimes you want to do something *after* a function, but don't want to clutter its code. + Simple way to set after-function callbacks. - Put `@then :callback` above a function and it will automatically call `callback(result)` after execution. - Function result stays unchanged, callback is called for side effects. + Put `@then :callback` or `@then {Module, :callback}` above a function and it will automatically + call the callback after execution. Function result stays unchanged, callback is called for side effects. - ## Basic Usage - - defmodule MyModule do - use Then - - @then :log - def add(a, b) do - a + b - end - - def log(result) do - IO.puts("Got \#{result}") - end - end - - MyModule.add(2, 3) - # Got 5 - # => 5 - - ## Real-world Example - - defmodule UserService do - use Then - - @then :audit_creation - def new_user(params) do - case params do - %{email: email, name: name} -> {:ok, %User{name: name, email: email}} - _ -> {:error, "Required fields are missing"} - end - end - - # side effects separately - def audit_creation({:ok, user}), do: Logger.info("User \#{user.email} created") - def audit_creation({:error, reason}), do: Logger.warn("User wasn't created. \#{reason}") - end - - ## Compatibility - - Works perfectly with other function attributes: - - defmodule MyService do - use Then - - @doc "Gets age from params" - @spec get_age(map()) :: integer() - @then :log_term - def get_age(params) do - params[:age] || 0 - end - - defp log_term(term), do: IO.puts("[log-term] \#{term}") - end - - `@spec`, `@doc`, `@deprecated` and other attributes work as expected. - Callback functions can be private (`defp`). - - ## Limitations - - - One `@then` per function (compilation error if you try to use multiple) - - Callback is not called if function raises an exception - - For functions with multiple clauses, `@then` applies to all clauses - - ## Why Use This? - - It's simple and clear. Move log and other side-effects out of your beautiful logic. + See the main documentation for detailed usage examples and API reference. """ defmacro __using__(_opts) do