Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c345dab9c1 | ||
|
|
2d46eda619 | ||
|
|
beb9b1b3ef | ||
|
|
cb512a7f17 | ||
|
|
e4fdeb6b79 | ||
|
|
6b22171018 | ||
|
|
f0112289e6 | ||
|
|
72e18a0a0c | ||
|
|
e2be3f652e | ||
|
|
490bd32322 | ||
|
|
1bfcba133a | ||
|
|
c3d024c16c | ||
|
|
1530a761d5 | ||
|
|
5197b6ece6 |
@@ -1 +0,0 @@
|
||||
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
|
||||
+10
-12
@@ -1,13 +1,11 @@
|
||||
/.eunit
|
||||
/.release
|
||||
/docs
|
||||
/ebin
|
||||
/lib/*/ebin/*
|
||||
/lib/*/tmp
|
||||
/lib/elixir/src/elixir.app.src
|
||||
/lib/elixir/src/*_lexer.erl
|
||||
/lib/elixir/src/*_parser.erl
|
||||
/lib/elixir/test/ebin
|
||||
/rel/elixir
|
||||
.formatter.exs
|
||||
/_build/
|
||||
/cover/
|
||||
/deps/
|
||||
/doc/
|
||||
/.fetch
|
||||
erl_crash.dump
|
||||
.elixir.plt
|
||||
*.ez
|
||||
n8n_openai_adapter-*.tar
|
||||
/tmp/
|
||||
/result
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
language: erlang
|
||||
script: "make compile && rm -rf .git && make test"
|
||||
notifications:
|
||||
irc: "irc.freenode.org#elixir-lang"
|
||||
recipients:
|
||||
- jose.valim@plataformatec.com.br
|
||||
- eric.meadows.jonsson@gmail.com
|
||||
otp_release:
|
||||
- 17.0
|
||||
- 17.1
|
||||
- 17.3
|
||||
-1352
File diff suppressed because it is too large
Load Diff
-265
@@ -1,265 +0,0 @@
|
||||
# Contributing to Elixir
|
||||
|
||||
Please take a moment to review this document in order to make the contribution
|
||||
process easy and effective for everyone involved!
|
||||
|
||||
## Using the issue tracker
|
||||
|
||||
Use the issues tracker for:
|
||||
|
||||
* [bug reports](#bugs-reports)
|
||||
* [submitting pull requests](#pull-requests)
|
||||
|
||||
Please **do not** use the issue tracker for personal support requests nor feature requests. Support requests should be sent to:
|
||||
|
||||
* [the elixir-talk mailing list](http://groups.google.com/group/elixir-lang-talk)
|
||||
* [Stack Overflow](http://stackoverflow.com/questions/ask?tags=elixir)
|
||||
* [#elixir-lang](irc://chat.freenode.net/elixir-lang)
|
||||
|
||||
Feature requests can be discussed on [the elixir-core mailing list](http://groups.google.com/group/elixir-lang-core).
|
||||
|
||||
We do our best to keep the issue tracker tidy and organized, making it useful
|
||||
for everyone. For example, we classify open issues per application and perceived
|
||||
difficulty of the issue, making it easier for developers to
|
||||
[contribute to Elixir](#contributing).
|
||||
|
||||
## Bug reports
|
||||
|
||||
A bug is a _demonstrable problem_ that is caused by the code in the repository.
|
||||
Good bug reports are extremely helpful - thank you!
|
||||
|
||||
Guidelines for bug reports:
|
||||
|
||||
1. **Use the GitHub issue search** — check if the issue has already been
|
||||
reported.
|
||||
|
||||
2. **Check if the issue has been fixed** — try to reproduce it using the
|
||||
`master` branch in the repository.
|
||||
|
||||
3. **Isolate and report the problem** — ideally create a reduced test
|
||||
case.
|
||||
|
||||
Please try to be as detailed as possible in your report. Include information about
|
||||
your Operating System, your Erlang and Elixir versions. Please provide steps to
|
||||
reproduce the issue as well as the outcome you were expecting! All these details
|
||||
will help developers to fix any potential bugs.
|
||||
|
||||
Example:
|
||||
|
||||
> Short and descriptive example bug report title
|
||||
>
|
||||
> A summary of the issue and the environment in which it occurs. If suitable,
|
||||
> include the steps required to reproduce the bug.
|
||||
>
|
||||
> 1. This is the first step
|
||||
> 2. This is the second step
|
||||
> 3. Further steps, etc.
|
||||
>
|
||||
> `<url>` - a link to the reduced test case (e.g. a GitHub Gist)
|
||||
>
|
||||
> Any other information you want to share that is relevant to the issue being
|
||||
> reported. This might include the lines of code that you have identified as
|
||||
> causing the bug, and potential solutions (and your opinions on their
|
||||
> merits).
|
||||
|
||||
## Feature requests
|
||||
|
||||
Feature requests are welcome and should be discussed on [the elixir-core mailing list](http://groups.google.com/group/elixir-lang-core). But take a moment to find
|
||||
out whether your idea fits with the scope and aims of the project. It's up to *you*
|
||||
to make a strong case to convince the community of the merits of this feature.
|
||||
Please provide as much detail and context as possible.
|
||||
|
||||
## Contributing
|
||||
|
||||
We incentivize everyone to contribute to Elixir and help us tackle
|
||||
existing issues! To do so, there are a few things you need to know
|
||||
about the code. First, Elixir code is divided in applications inside
|
||||
the `lib` folder:
|
||||
|
||||
* `elixir` - Contains Elixir's kernel and stdlib
|
||||
|
||||
* `eex` - Template engine that allows you to embed Elixir
|
||||
|
||||
* `ex_unit` - Simple test framework that ships with Elixir
|
||||
|
||||
* `iex` — IEx, Elixir's interactive shell
|
||||
|
||||
* `mix` — Elixir's build tool
|
||||
|
||||
You can run all tests in the root directory with `make test` and you can
|
||||
also run tests for a specific framework `make test_#{NAME}`, for example,
|
||||
`make test_ex_unit`.
|
||||
|
||||
In case you are changing a single file, you can compile and run tests only
|
||||
for that particular file for fast development cycles. For example, if you
|
||||
are changing the String module, you can compile it and run its tests as:
|
||||
|
||||
$ bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
|
||||
$ bin/elixir lib/elixir/test/elixir/string_test.exs
|
||||
|
||||
After your changes are done, please remember to run the full suite with
|
||||
`make test`.
|
||||
|
||||
From time to time, your tests may fail in an existing Elixir checkout and
|
||||
may require a clean start by running `make clean compile`. You can always
|
||||
check [the official build status on Travis-CI](https://travis-ci.org/elixir-lang/elixir).
|
||||
|
||||
With tests running and passing, you are ready to contribute to Elixir and
|
||||
send your pull requests.
|
||||
|
||||
## Contributing Documentation
|
||||
|
||||
Code documentation (`@doc`, `@moduledoc`, `@typedoc`) has a special convention:
|
||||
the first paragraph is considered to be a short summary.
|
||||
|
||||
For functions, macros and callbacks say what it will do. For example write
|
||||
something like:
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
Returns only those elements for which `fun` is true.
|
||||
|
||||
...
|
||||
"""
|
||||
def filter(collection, fun) ...
|
||||
```
|
||||
|
||||
For modules, protocols and types say what it is. For example write
|
||||
something like:
|
||||
|
||||
```elixir
|
||||
defmodule File.Stat do
|
||||
@moduledoc """
|
||||
Information about a file.
|
||||
|
||||
...
|
||||
"""
|
||||
|
||||
defstruct [...]
|
||||
end
|
||||
```
|
||||
|
||||
Keep in mind that the first paragraph might show up in a summary somewhere, long
|
||||
texts in the first paragraph create very ugly summaries. As a rule of thumb
|
||||
anything longer than 80 characters is too long.
|
||||
|
||||
Try to keep unneccesary details out of the first paragraph, it's only there to
|
||||
give a user a quick idea of what the documented "thing" does/is. The rest of the
|
||||
documentation string can contain the details, for example when a value and when
|
||||
`nil` is returned.
|
||||
|
||||
If possible include examples, preferably in a form that works with doctests. For
|
||||
example:
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
Return only those elements for which `fun` is true.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.filter([1, 2, 3], fn(x) -> rem(x, 2) == 0 end)
|
||||
[2]
|
||||
|
||||
"""
|
||||
def filter(collection, fun) ...
|
||||
```
|
||||
|
||||
This makes it easy to test the examples so that they don't go stale and examples
|
||||
are often a great help in explaining what a function does.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Good pull requests - patches, improvements, new features - are a fantastic
|
||||
help. They should remain focused in scope and avoid containing unrelated
|
||||
commits.
|
||||
|
||||
**IMPORTANT**: By submitting a patch, you agree that your work will be
|
||||
licensed under the license used by the project.
|
||||
|
||||
If you have any large pull request in mind (e.g. implementing features,
|
||||
refactoring code, etc), **please ask first** otherwise you risk spending
|
||||
a lot of time working on something that the project's developers might
|
||||
not want to merge into the project.
|
||||
|
||||
Please adhere to the coding conventions in the project (indentation,
|
||||
accurate comments, etc.) and don't forget to add your own tests and
|
||||
documentation. When working with git, we recommend the following process
|
||||
in order to craft an excellent pull request:
|
||||
|
||||
1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
|
||||
and configure the remotes:
|
||||
|
||||
```bash
|
||||
# Clone your fork of the repo into the current directory
|
||||
git clone https://github.com/<your-username>/elixir
|
||||
# Navigate to the newly cloned directory
|
||||
cd elixir
|
||||
# Assign the original repo to a remote called "upstream"
|
||||
git remote add upstream https://github.com/elixir-lang/elixir
|
||||
```
|
||||
|
||||
2. If you cloned a while ago, get the latest changes from upstream:
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
```
|
||||
|
||||
3. Create a new topic branch (off of `master`) to contain your feature, change,
|
||||
or fix.
|
||||
|
||||
**IMPORTANT**: Making changes in `master` is discouraged. You should always
|
||||
keep your local `master` in sync with upstream `master` and make your
|
||||
changes in topic branches.
|
||||
|
||||
```bash
|
||||
git checkout -b <topic-branch-name>
|
||||
```
|
||||
|
||||
4. Commit your changes in logical chunks. Keep your commit messages organized,
|
||||
with a short description in the first line and more detailed information on
|
||||
the following lines. Feel free to use Git's
|
||||
[interactive rebase](https://help.github.com/articles/interactive-rebase)
|
||||
feature to tidy up your commits before making them public.
|
||||
|
||||
5. Make sure all the tests are still passing.
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
This command will compile the code in your branch and use that
|
||||
version of Elixir to run the tests. This is needed to ensure your changes can
|
||||
pass all the tests.
|
||||
|
||||
6. Push your topic branch up to your fork:
|
||||
|
||||
```bash
|
||||
git push origin <topic-branch-name>
|
||||
```
|
||||
|
||||
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
|
||||
with a clear title and description.
|
||||
|
||||
8. If you haven't updated your pull request for a while, you should consider
|
||||
rebasing on master and resolving any conflicts.
|
||||
|
||||
**IMPORTANT**: _Never ever_ merge upstream `master` into your branches. You
|
||||
should always `git rebase` on `master` to bring your changes up to date when
|
||||
necessary.
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
git checkout <your-topic-branch>
|
||||
git rebase master
|
||||
```
|
||||
|
||||
We have saved some excellent pull requests we have received in the past in case
|
||||
you are looking for some examples:
|
||||
|
||||
* https://github.com/elixir-lang/elixir/pull/992
|
||||
* https://github.com/elixir-lang/elixir/pull/1058
|
||||
* https://github.com/elixir-lang/elixir/pull/1059
|
||||
|
||||
Thank you for your contributions!
|
||||
@@ -1,8 +0,0 @@
|
||||
LEGAL NOTICE INFORMATION
|
||||
------------------------
|
||||
|
||||
All the files in this distribution are covered under either Elixir's
|
||||
license (see the file LICENSE) except the files mentioned below that
|
||||
contains sections that are under Erlang's License (EPL):
|
||||
|
||||
lib/elixir/src/elixir_parser.erl (generated by build scripts)
|
||||
@@ -1,13 +0,0 @@
|
||||
Copyright 2012-2013 Plataformatec.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,211 +0,0 @@
|
||||
REBAR := rebar
|
||||
DOCS := v1.0
|
||||
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
VERSION := $(strip $(shell cat VERSION))
|
||||
Q := @
|
||||
PREFIX := /usr/local
|
||||
LIBDIR := lib
|
||||
INSTALL = install
|
||||
INSTALL_DIR = $(INSTALL) -m755 -d
|
||||
INSTALL_DATA = $(INSTALL) -m644
|
||||
INSTALL_PROGRAM = $(INSTALL) -m755
|
||||
|
||||
.PHONY: install compile erlang elixir build_plt clean_plt dialyze test clean docs release_docs release_zip check_erlang_release
|
||||
.NOTPARALLEL: compile
|
||||
|
||||
#==> Functions
|
||||
|
||||
# This check should work for older versions like R16B
|
||||
# as well as new verions like 17.1 and 18
|
||||
define CHECK_ERLANG_RELEASE
|
||||
$(Q) erl -noshell -eval 'io:fwrite("~s", [erlang:system_info(otp_release)])' -s erlang halt | grep -q '^1[789]'; \
|
||||
if [ $$? != 0 ]; then \
|
||||
echo "At least Erlang 17.0 is required to build Elixir"; \
|
||||
exit 1; \
|
||||
fi;
|
||||
endef
|
||||
|
||||
define APP_TEMPLATE
|
||||
$(1): lib/$(1)/ebin/Elixir.$(2).beam lib/$(1)/ebin/$(1).app
|
||||
|
||||
lib/$(1)/ebin/$(1).app: lib/$(1)/mix.exs
|
||||
$(Q) mkdir -p lib/$(1)/_build/shared/lib/$(1)
|
||||
$(Q) cp -R lib/$(1)/ebin lib/$(1)/_build/shared/lib/$(1)/
|
||||
$(Q) cd lib/$(1) && ../../bin/elixir -e 'Mix.start(:permanent, [])' -r mix.exs -e 'Mix.Task.run("compile.app")'
|
||||
$(Q) cp lib/$(1)/_build/shared/lib/$(1)/ebin/$(1).app lib/$(1)/ebin/$(1).app
|
||||
$(Q) rm -rf lib/$(1)/_build
|
||||
|
||||
lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1)/lib/*/*.ex) $(wildcard lib/$(1)/lib/*/*/*.ex)
|
||||
@ echo "==> $(1) (compile)"
|
||||
@ rm -rf lib/$(1)/ebin
|
||||
$(Q) cd lib/$(1) && ../../$$(ELIXIRC) "lib/**/*.ex" -o ebin
|
||||
|
||||
test_$(1): $(1)
|
||||
@ echo "==> $(1) (exunit)"
|
||||
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/*_test.exs";
|
||||
endef
|
||||
|
||||
#==> Compilation tasks
|
||||
|
||||
KERNEL:=lib/elixir/ebin/Elixir.Kernel.beam
|
||||
UNICODE:=lib/elixir/ebin/Elixir.String.Unicode.beam
|
||||
|
||||
default: compile
|
||||
|
||||
compile: lib/elixir/src/elixir.app.src erlang elixir
|
||||
|
||||
lib/elixir/src/elixir.app.src: src/elixir.app.src
|
||||
$(Q) $(call CHECK_ERLANG_RELEASE)
|
||||
$(Q) rm -rf lib/elixir/src/elixir.app.src
|
||||
$(Q) echo "%% This file is automatically generated from <project_root>/src/elixir.app.src" \
|
||||
>lib/elixir/src/elixir.app.src
|
||||
$(Q) cat src/elixir.app.src >>lib/elixir/src/elixir.app.src
|
||||
|
||||
erlang:
|
||||
$(Q) cd lib/elixir && ../../$(REBAR) compile
|
||||
|
||||
# Since Mix depends on EEx and EEx depends on
|
||||
# Mix, we first compile EEx without the .app
|
||||
# file, then mix and then compile EEx fully
|
||||
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
|
||||
|
||||
stdlib: $(KERNEL) VERSION
|
||||
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
|
||||
$(Q) if [ ! -f $(KERNEL) ]; then \
|
||||
echo "==> bootstrap (compile)"; \
|
||||
$(ERL) -s elixir_compiler core -s erlang halt; \
|
||||
fi
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/kernel.ex" -o ebin;
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
|
||||
$(Q) $(MAKE) unicode
|
||||
$(Q) rm -rf lib/elixir/ebin/elixir.app
|
||||
$(Q) cd lib/elixir && ../../$(REBAR) compile
|
||||
|
||||
unicode: $(UNICODE)
|
||||
$(UNICODE): lib/elixir/unicode/*
|
||||
@ echo "==> unicode (compile)";
|
||||
@ echo "Embedding the Unicode database... (this may take a while)"
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) unicode/unicode.ex -o ebin;
|
||||
|
||||
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
|
||||
$(eval $(call APP_TEMPLATE,logger,Logger))
|
||||
$(eval $(call APP_TEMPLATE,eex,EEx))
|
||||
$(eval $(call APP_TEMPLATE,mix,Mix))
|
||||
$(eval $(call APP_TEMPLATE,iex,IEx))
|
||||
|
||||
install: compile
|
||||
@ echo "==> elixir (install)"
|
||||
$(Q) for dir in lib/*; do \
|
||||
$(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin"; \
|
||||
$(INSTALL_DATA) $$dir/ebin/* "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin"; \
|
||||
done
|
||||
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/bin"
|
||||
$(Q) $(INSTALL_PROGRAM) $(filter-out %.ps1, $(filter-out %.bat, $(wildcard bin/*))) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/bin"
|
||||
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/bin"
|
||||
$(Q) for file in "$(DESTDIR)$(PREFIX)"/$(LIBDIR)/elixir/bin/* ; do \
|
||||
ln -sf "../$(LIBDIR)/elixir/bin/$${file##*/}" "$(DESTDIR)$(PREFIX)/bin/" ; \
|
||||
done
|
||||
|
||||
clean:
|
||||
cd lib/elixir && ../../$(REBAR) clean
|
||||
rm -rf ebin
|
||||
rm -rf lib/*/ebin
|
||||
rm -rf lib/elixir/test/ebin
|
||||
rm -rf lib/*/tmp
|
||||
rm -rf lib/mix/test/fixtures/git_repo
|
||||
rm -rf lib/mix/test/fixtures/deps_on_git_repo
|
||||
rm -rf lib/mix/test/fixtures/git_rebar
|
||||
rm -rf lib/elixir/src/elixir.app.src
|
||||
|
||||
clean_exbeam:
|
||||
$(Q) rm -f lib/*/ebin/Elixir.*.beam
|
||||
|
||||
#==> Release tasks
|
||||
|
||||
SOURCE_REF = $(shell head="$$(git rev-parse HEAD)" tag="$$(git tag --points-at $$head | tail -1)" ; echo "$${tag:-$$head}\c")
|
||||
DOCS = bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" -m "$(3)" -u "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" -o docs/$(2) -p http://elixir-lang.org/docs.html
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc
|
||||
$(Q) rm -rf docs
|
||||
$(call DOCS,Elixir,elixir,Kernel)
|
||||
$(call DOCS,EEx,eex,EEx)
|
||||
$(call DOCS,Mix,mix,Mix)
|
||||
$(call DOCS,IEx,iex,IEx)
|
||||
$(call DOCS,ExUnit,ex_unit,ExUnit)
|
||||
$(call DOCS,Logger,logger,Logger)
|
||||
|
||||
../ex_doc/bin/ex_doc:
|
||||
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
|
||||
@ false
|
||||
|
||||
release_zip: compile
|
||||
rm -rf v$(VERSION).zip
|
||||
zip -9 -r v$(VERSION).zip bin CHANGELOG.md LEGAL lib/*/ebin LICENSE Makefile README.md VERSION
|
||||
|
||||
release_docs: docs
|
||||
cd ../docs
|
||||
rm -rf ../docs/$(DOCS)
|
||||
mv docs ../docs/$(DOCS)
|
||||
|
||||
# This task requires aws-cli to be installed and set up for access to s3.hex.pm
|
||||
# See: http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-set-up.html
|
||||
|
||||
publish_mix: compile
|
||||
cd lib/mix && MIX_ENV=prod mix escript.build
|
||||
aws s3 cp lib/mix/mix s3://s3.hex.pm/builds/mix/v$(VERSION)/mix --acl public-read
|
||||
aws s3 cp lib/mix/mix s3://s3.hex.pm/builds/mix/mix --acl public-read
|
||||
rm lib/mix/mix
|
||||
rm -rf lib/mix/_build
|
||||
|
||||
#==> Tests tasks
|
||||
|
||||
test: test_erlang test_elixir
|
||||
|
||||
TEST_ERL = lib/elixir/test/erlang
|
||||
TEST_EBIN = lib/elixir/test/ebin
|
||||
TEST_ERLS = $(addprefix $(TEST_EBIN)/, $(addsuffix .beam, $(basename $(notdir $(wildcard $(TEST_ERL)/*.erl)))))
|
||||
|
||||
test_erlang: compile $(TEST_ERLS)
|
||||
@ echo "==> elixir (eunit)"
|
||||
$(Q) $(ERL) -pa $(TEST_EBIN) -s test_helper test;
|
||||
@ echo ""
|
||||
|
||||
$(TEST_EBIN)/%.beam: $(TEST_ERL)/%.erl
|
||||
$(Q) mkdir -p $(TEST_EBIN)
|
||||
$(Q) $(ERLC) -o $(TEST_EBIN) $<
|
||||
|
||||
test_elixir: test_stdlib test_ex_unit test_logger test_doc_test test_mix test_eex test_iex
|
||||
|
||||
test_doc_test: compile
|
||||
@ echo "==> doctest (exunit)"
|
||||
$(Q) cd lib/elixir && ../../bin/elixir -r "test/doc_test.exs";
|
||||
|
||||
test_stdlib: compile
|
||||
@ echo "==> elixir (exunit)"
|
||||
$(Q) exec epmd & exit
|
||||
$(Q) if [ "$(OS)" = "Windows_NT" ]; then \
|
||||
cd lib/elixir && cmd //C call ../../bin/elixir.bat -r "test/elixir/test_helper.exs" -pr "test/elixir/**/*_test.exs"; \
|
||||
else \
|
||||
cd lib/elixir && ../../bin/elixir -r "test/elixir/test_helper.exs" -pr "test/elixir/**/*_test.exs"; \
|
||||
fi
|
||||
|
||||
#==> Dialyzer tasks
|
||||
|
||||
DIALYZER_OPTS = --no_check_plt --fullpath -Werror_handling -Wunmatched_returns -Wunderspecs
|
||||
PLT = .elixir.plt
|
||||
|
||||
$(PLT):
|
||||
@ echo "==> Building PLT with Elixir's dependencies..."
|
||||
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets
|
||||
|
||||
clean_plt:
|
||||
$(Q) rm -f $(PLT)
|
||||
|
||||
build_plt: clean_plt $(PLT)
|
||||
|
||||
dialyze: compile $(PLT)
|
||||
@ echo "==> Dialyzing Elixir..."
|
||||
$(Q) dialyzer --plt $(PLT) $(DIALYZER_OPTS) lib/*/ebin
|
||||
@@ -1,59 +1,94 @@
|
||||

|
||||
=========
|
||||
[](http://travis-ci.org/elixir-lang/elixir)
|
||||
# n8n-openai-adapter
|
||||
|
||||
For more about Elixir, installation and documentation, [check Elixir's website](http://elixir-lang.org/).
|
||||
An OpenAI-compatible HTTP adapter that exposes self-hosted **n8n chat agents**
|
||||
behind a standard `/v1/chat/completions` API, so any OpenAI client (Cursor,
|
||||
LibreChat, the `openai` SDK, a custom app) can talk to your n8n agents as if
|
||||
they were OpenAI models.
|
||||
|
||||
## Usage
|
||||
n8n itself does **not** ship an inbound OpenAI-compatible endpoint (its "AI
|
||||
Gateway" is an outbound proxy to n8n Cloud). This small Elixir service is the
|
||||
bridge: one `/v1/chat/completions` endpoint, routed to whichever n8n agent you
|
||||
name in the `model` field.
|
||||
|
||||
If you want to contribute to Elixir or run it from source, clone this repository to your machine, compile and test it:
|
||||
## How it works
|
||||
|
||||
$ git clone https://github.com/elixir-lang/elixir.git
|
||||
$ cd elixir
|
||||
$ make clean test
|
||||
```
|
||||
Your OpenAI client
|
||||
POST /v1/chat/completions {"model":"scholar-agent","thread_id":"abc","messages":[...]}
|
||||
|
|
||||
v
|
||||
n8n-openai-adapter (Plug + Bandit)
|
||||
- authorize (Bearer <ADAPTER_API_KEY>)
|
||||
- look up "scholar-agent" -> n8n chat webhook URL (AgentRegistry GenServer)
|
||||
- take the last user message
|
||||
- forward to the n8n webhook {sessionId: thread_id, action: sendMessage, chatInput}
|
||||
|
|
||||
v
|
||||
n8n agent (its MCP tools, memory, etc. run as usual)
|
||||
|
|
||||
v
|
||||
returns OpenAI-shaped {"choices":[{"message":{"role":"assistant","content":...}}]}
|
||||
```
|
||||
|
||||
> Note: if you are running on Windows, [this article includes important notes for compiling Elixir from source on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
|
||||
Multiple agents = multiple `model` names, each mapped to a different n8n webhook
|
||||
in the `AGENTS` env var.
|
||||
|
||||
If Elixir fails to build (specifically when pulling in a new version via git), be sure to remove any previous build artifacts by running `make clean`, then `make test`.
|
||||
## Configuration (env vars)
|
||||
|
||||
If tests pass, you are ready to move on to the [Getting Started guide][1] or to try Interactive Elixir by running: `bin/iex` in your terminal.
|
||||
| Var | Required | Purpose |
|
||||
|------------------|----------|---------------------------------------------------------------------|
|
||||
| `ADAPTER_API_KEY`| yes | Bearer key that OpenAI clients send. |
|
||||
| `ADMIN_API_KEY` | yes | Bearer key for the admin API / web admin page. |
|
||||
| `AGENTS_FILE` | no | Path to the JSON store (default `/var/lib/n8n-openai/agents.json`). |
|
||||
| `PORT` | no | HTTP port (default `8000`). |
|
||||
| `CHAT_WEBHOOK_BASIC` | no | `"user:password"` if your n8n Chat Trigger is Basic-auth protected. |
|
||||
|
||||
However, if tests fail, it is likely you have an outdated Erlang version (Elixir requires Erlang 17.0 or later). You can check your Erlang version by calling `erl` in the command line. You will see some information as follows:
|
||||
Agents are **not** configured via env — they're managed at runtime through the
|
||||
web admin page / admin API and persisted to `AGENTS_FILE`. The store starts
|
||||
empty; add agents after boot.
|
||||
|
||||
Erlang/OTP 17 [erts-6.0] [source-07b8f44] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
|
||||
## Admin API (manage agents at runtime)
|
||||
|
||||
If you have the correct version and tests still fail, feel free to [open an issue][2].
|
||||
Agents are persisted to `AGENTS_FILE` and can be added/removed without a
|
||||
redeploy, using the `ADMIN_API_KEY`:
|
||||
|
||||
## Building documentation
|
||||
```bash
|
||||
# list
|
||||
curl -H "Authorization: Bearer $ADMIN_API_KEY" https://openai.bueso.eu/admin/agents
|
||||
|
||||
Building the documentation requires [ex_doc](https://github.com/elixir-lang/ex_doc) to be installed and built in the same containing folder as elixir.
|
||||
# add / update an agent
|
||||
curl -X POST -H "Authorization: Bearer $ADMIN_API_KEY" -H "Content-Type: application/json" \
|
||||
-d '{"model":"media-agent","webhook":"https://n8n.bueso.eu/webhook/<id>/chat"}' \
|
||||
https://openai.bueso.eu/admin/agents
|
||||
|
||||
# After cloning and compiling Elixir
|
||||
$ git clone git://github.com/elixir-lang/ex_doc.git
|
||||
$ cd ex_doc && ../elixir/bin/mix compile
|
||||
$ cd ../elixir && make docs
|
||||
# remove
|
||||
curl -X DELETE -H "Authorization: Bearer $ADMIN_API_KEY" \
|
||||
https://openai.bueso.eu/admin/agents/media-agent
|
||||
```
|
||||
|
||||
## Contributing
|
||||
The store is authoritative and persists across restarts; no env config needed.
|
||||
|
||||
We appreciate any contribution to Elixir, so check out our [CONTRIBUTING.md](CONTRIBUTING.md) guide for more information. We usually keep a list of features and bugs [in the issue tracker][2].
|
||||
## Building & running
|
||||
|
||||
## Important links
|
||||
```bash
|
||||
mix deps.get
|
||||
mix compile
|
||||
ADAPTER_API_KEY=secret AGENTS='{"scholar-agent":"https://n8n.bueso.eu/webhook/<id>/chat"}' \
|
||||
PORT=8000 mix run --no-halt
|
||||
```
|
||||
|
||||
* \#elixir-lang on freenode IRC
|
||||
* [Website][1]
|
||||
* [Issue tracker][2]
|
||||
* [elixir-talk Mailing list (questions)][3]
|
||||
* [elixir-core Mailing list (development)][4]
|
||||
## Testing
|
||||
|
||||
[1]: http://elixir-lang.org
|
||||
[2]: https://github.com/elixir-lang/elixir/issues
|
||||
[3]: http://groups.google.com/group/elixir-lang-talk
|
||||
[4]: http://groups.google.com/group/elixir-lang-core
|
||||
```bash
|
||||
MIX_ENV=test mix test
|
||||
```
|
||||
|
||||
## License
|
||||
## Nix
|
||||
|
||||
"Elixir" and the Elixir logo are copyright (c) 2012 Plataformatec.
|
||||
The repo ships a `flake.nix` exporting `overlays.default` and a `packages.default`
|
||||
(the packaged BEAM release), so it can be consumed as a flake input from your
|
||||
NixOS config just like any other flake — e.g.:
|
||||
|
||||
Elixir source code is released under Apache 2 License with some parts under Erlang's license (EPL).
|
||||
|
||||
Check [LEGAL](LEGAL) and [LICENSE](LICENSE) files for more information.
|
||||
```nix
|
||||
inputs.n8n-openai-adapter.url = "git+https://gitea.bueso.eu/<owner>/n8n-openai-adapter";
|
||||
```
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
# Release process
|
||||
|
||||
## All releases
|
||||
|
||||
This document simply outlines the release process:
|
||||
|
||||
1. Ensure you are running on the oldest supported Erlang version
|
||||
|
||||
2. Remove all `-dev` extension from versions (see below for all files)
|
||||
|
||||
3. Ensure CHANGELOG is updated and add current date
|
||||
|
||||
4. Commit changes above with title "Release vVERSION" and generate new tag
|
||||
|
||||
5. Run `make clean test` to ensure all tests pass from scratch and the CI is green
|
||||
|
||||
6. Ensure minimum supported Hex works with new release (instructions upcoming)
|
||||
|
||||
7. Push master and the new tag
|
||||
|
||||
8. Release new docs with `make release_docs`, move docs to `docs/stable` if appropriate, and push
|
||||
|
||||
9. Release new zip with `make release_zip`, push `Precompiled.zip` to GitHub Releases
|
||||
|
||||
10. Add the release to `elixir.csv` file in `elixir-lang/elixir-lang.github.com`
|
||||
|
||||
11. Build and push standalone Mix with `make publish_mix` (requires AWS credentials)
|
||||
|
||||
## New vMAJOR.MINOR releases
|
||||
|
||||
11. Create a new branch "vMAJOR.MINOR"
|
||||
|
||||
12. Move docs generation to `docs/vOLD-MAJOR.OLD-MINOR` and copy them to `docs/stable`
|
||||
|
||||
13. Bump versions, start new CHANGELOG, add `-dev` back and commit "Start vVERSION+1"
|
||||
|
||||
14. `make release_docs` and push it to `elixir-lang/docs`
|
||||
|
||||
## Places where version is mentioned
|
||||
|
||||
* VERSION (make sure there is no newline in this file)
|
||||
* CHANGELOG.md
|
||||
* src/elixir.app.src (not lib/elixir/src/elixir.app.src)
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "Usage: `basename $0` [options] [.exs file] [data]
|
||||
|
||||
-v Prints version and exit
|
||||
-e \"command\" Evaluates the given command (*)
|
||||
-r \"file\" Requires the given files/patterns (*)
|
||||
-S \"script\" Finds and executes the given script
|
||||
-pr \"file\" Requires the given files/patterns in parallel (*)
|
||||
-pa \"path\" Prepends the given path to Erlang code path (*)
|
||||
-pz \"path\" Appends the given path to Erlang code path (*)
|
||||
--app \"app\" Start the given app and its dependencies (*)
|
||||
--erl \"switches\" Switches to be passed down to erlang (*)
|
||||
--name \"name\" Makes and assigns a name to the distributed node
|
||||
--sname \"name\" Makes and assigns a short name to the distributed node
|
||||
--cookie \"cookie\" Sets a cookie for this distributed node
|
||||
--hidden Makes a hidden node
|
||||
--detached Starts the Erlang VM detached from console
|
||||
--no-halt Does not halt the Erlang VM after execution
|
||||
|
||||
** Options marked with (*) can be given more than once
|
||||
** Options given after the .exs file or -- are passed down to the executed code
|
||||
** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTIONS or --erl" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readlink_f () {
|
||||
cd "$(dirname "$1")" > /dev/null
|
||||
filename="$(basename "$1")"
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f "$(readlink "$filename")"
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
MODE="elixir"
|
||||
ERL=""
|
||||
I=1
|
||||
|
||||
while [ $I -le $# ]; do
|
||||
S=1
|
||||
eval "PEEK=\${$I}"
|
||||
case "$PEEK" in
|
||||
+iex)
|
||||
MODE="iex"
|
||||
;;
|
||||
+elixirc)
|
||||
MODE="elixirc"
|
||||
;;
|
||||
-v|--compile|--no-halt)
|
||||
;;
|
||||
-e|-r|-pr|-pa|-pz|--remsh|--app)
|
||||
S=2
|
||||
;;
|
||||
--detached|--hidden)
|
||||
ERL="$ERL `echo $PEEK | cut -c 2-`"
|
||||
;;
|
||||
--cookie)
|
||||
I=$(expr $I + 1)
|
||||
eval "VAL=\${$I}"
|
||||
ERL="$ERL -setcookie "$VAL""
|
||||
;;
|
||||
--sname|--name)
|
||||
I=$(expr $I + 1)
|
||||
eval "VAL=\${$I}"
|
||||
ERL="$ERL `echo $PEEK | cut -c 2-` "$VAL""
|
||||
;;
|
||||
--erl)
|
||||
I=$(expr $I + 1)
|
||||
eval "VAL=\${$I}"
|
||||
ERL="$ERL "$VAL""
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
I=$(expr $I + $S)
|
||||
done
|
||||
|
||||
SELF=$(readlink_f "$0")
|
||||
SCRIPT_PATH=$(dirname "$SELF")
|
||||
|
||||
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
|
||||
if [ "$MODE" != "iex" ]; then ERL="-noshell -s elixir start_cli $ERL"; fi
|
||||
|
||||
# Check for terminal support
|
||||
if [ "$OS" != "Windows_NT" ]; then
|
||||
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
|
||||
fi
|
||||
|
||||
if [ -z "$ERL_PATH" ]; then
|
||||
if [ -f "$SCRIPT_PATH/../releases/RELEASES" ] && [ -f "$SCRIPT_PATH/erl" ]; then
|
||||
ERL_PATH="$SCRIPT_PATH"/erl
|
||||
else
|
||||
ERL_PATH=erl
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$ERL_PATH" -pa "$SCRIPT_PATH"/../lib/*/ebin $ELIXIR_ERL_OPTIONS $ERL -extra "$@"
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
@echo off
|
||||
setlocal
|
||||
if "%1"=="" goto documentation
|
||||
if "%1"=="--help" goto documentation
|
||||
if "%1"=="-h" goto documentation
|
||||
if "%1"=="/h" goto documentation
|
||||
goto parseopts
|
||||
|
||||
:documentation
|
||||
echo Usage: %~nx0 [options] [.exs file] [data]
|
||||
echo.
|
||||
echo -v Prints version and exit
|
||||
echo -e command Evaluates the given command (*)
|
||||
echo -r file Requires the given files/patterns (*)
|
||||
echo -S script Finds and executes the given script
|
||||
echo -pr file Requires the given files/patterns in parallel (*)
|
||||
echo -pa path Prepends the given path to Erlang code path (*)
|
||||
echo -pz path Appends the given path to Erlang code path (*)
|
||||
echo --app app Start the given app and its dependencies (*)
|
||||
echo --erl switches Switches to be passed down to erlang (*)
|
||||
echo --name name Makes and assigns a name to the distributed node
|
||||
echo --sname name Makes and assigns a short name to the distributed node
|
||||
echo --cookie cookie Sets a cookie for this distributed node
|
||||
echo --hidden Makes a hidden node
|
||||
echo --detached Starts the Erlang VM detached from console
|
||||
echo --werl Uses Erlang's Windows shell GUI
|
||||
echo --no-halt Does not halt the Erlang VM after execution
|
||||
echo.
|
||||
echo ** Options marked with (*) can be given more than once
|
||||
echo ** Options given after the .exs file or -- are passed down to the executed code
|
||||
echo ** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTIONS or --erl
|
||||
goto end
|
||||
|
||||
:parseopts
|
||||
|
||||
rem Parameters for Erlang
|
||||
set parsErlang=
|
||||
|
||||
rem Make sure we keep a copy of all parameters
|
||||
set allPars=%*
|
||||
|
||||
rem Get the original path name from the batch file
|
||||
set originPath=%~dp0
|
||||
|
||||
rem Optional parameters before the "-extra" parameter
|
||||
set beforeExtra=
|
||||
|
||||
rem Flag which determines whether or not to use werl vs erl
|
||||
set useWerl=0
|
||||
|
||||
rem Designates which mode / Elixir component to run as
|
||||
set runMode="elixir"
|
||||
|
||||
rem Recursive loop called for each parameter that parses the cmd line parameters
|
||||
:startloop
|
||||
set par="%1"
|
||||
shift
|
||||
if "%par%"=="" (
|
||||
rem if no parameters defined
|
||||
goto :expand_erl_libs
|
||||
)
|
||||
if "%par%"=="""" (
|
||||
rem if no parameters defined - special case for parameter that is already quoted
|
||||
goto :expand_erl_libs
|
||||
)
|
||||
rem ******* EXECUTION OPTIONS **********************
|
||||
IF "%par%"==""--werl"" (Set useWerl=1)
|
||||
IF "%par%"==""+iex"" (Set runMode="iex")
|
||||
rem ******* elixir parameters **********************
|
||||
rem Note: we don't have to do anything with options that don't take an argument
|
||||
IF """"=="%par:-e=%" (shift)
|
||||
IF """"=="%par:-r=%" (shift)
|
||||
IF """"=="%par:-pr=%" (shift)
|
||||
IF """"=="%par:-pa=%" (shift)
|
||||
IF """"=="%par:-pz=%" (shift)
|
||||
IF """"=="%par:--app=%" (shift)
|
||||
IF """"=="%par:--remsh=%" (shift)
|
||||
rem ******* ERLANG PARAMETERS **********************
|
||||
IF """"=="%par:--detached=%" (Set parsErlang=%parsErlang% -detached)
|
||||
IF """"=="%par:--hidden=%" (Set parsErlang=%parsErlang% -hidden)
|
||||
IF """"=="%par:--cookie=%" (Set parsErlang=%parsErlang% -setcookie %1 && shift)
|
||||
IF """"=="%par:--sname=%" (Set parsErlang=%parsErlang% -sname %1 && shift)
|
||||
IF """"=="%par:--name=%" (Set parsErlang=%parsErlang% -name %1 && shift)
|
||||
IF """"=="%par:--erl=%" (Set beforeExtra=%beforeExtra% %~1 && shift)
|
||||
goto:startloop
|
||||
|
||||
rem ******* assume all pre-params are parsed ********************
|
||||
:expand_erl_libs
|
||||
rem ******* expand all ebin paths as Windows does not support the ..\*\ebin wildcard ********************
|
||||
SETLOCAL enabledelayedexpansion
|
||||
set ext_libs=
|
||||
for /d %%d in ("%originPath%..\lib\*.") do (
|
||||
set ext_libs=!ext_libs! -pa "%%~fd\ebin"
|
||||
)
|
||||
SETLOCAL disabledelayedexpansion
|
||||
:run
|
||||
IF NOT %runMode% == "iex" (
|
||||
set beforeExtra=-s elixir start_cli %beforeExtra%
|
||||
)
|
||||
IF %useWerl% EQU 1 (
|
||||
start werl.exe %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
||||
) ELSE (
|
||||
erl.exe %ext_libs% -noshell %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
||||
)
|
||||
:end
|
||||
endlocal
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "Usage: `basename $0` [elixir switches] [compiler switches] [.ex files]
|
||||
|
||||
-o The directory to output compiled files
|
||||
--no-docs Do not attach documentation to compiled modules
|
||||
--no-debug-info Do not attach debug info to compiled modules
|
||||
--ignore-module-conflict
|
||||
--warnings-as-errors Treat warnings as errors and return non-zero exit code
|
||||
--verbose Print informational messages.
|
||||
|
||||
** Options given after -- are passed down to the executed code
|
||||
** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTIONS
|
||||
** Options can be passed to the erlang compiler using ERL_COMPILER_OPTIONS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readlink_f () {
|
||||
cd "$(dirname "$1")" > /dev/null
|
||||
filename="$(basename "$1")"
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f "$(readlink "$filename")"
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
SELF=$(readlink_f "$0")
|
||||
SCRIPT_PATH=$(dirname "$SELF")
|
||||
exec "$SCRIPT_PATH"/elixir +elixirc "$@"
|
||||
@@ -1,26 +0,0 @@
|
||||
@echo off
|
||||
set argc=0
|
||||
for %%A in (%*) do (
|
||||
if "%%A"=="--help" goto documentation
|
||||
if "%%A"=="-h" goto documentation
|
||||
if "%%A"=="/h" goto documentation
|
||||
set /A argc+=1
|
||||
)
|
||||
if %argc%==0 goto documentation
|
||||
goto run
|
||||
|
||||
:documentation
|
||||
echo Usage: %~nx0 [elixir switches] [compiler switches] [.ex files]
|
||||
echo.
|
||||
echo -o The directory to output compiled files
|
||||
echo --no-docs Do not attach documentation to compiled modules
|
||||
echo --no-debug-info Do not attach debug info to compiled modules
|
||||
echo --ignore-module-conflict
|
||||
echo --warnings-as-errors Treat warnings as errors and return non-zero exit code
|
||||
echo --verbose Print informational messages.
|
||||
echo.
|
||||
echo ** Options given after -- are passed down to the executed code
|
||||
echo ** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTIONS
|
||||
echo ** Options can be passed to the erlang compiler using ERL_COMPILER_OPTIONS >&2
|
||||
:run
|
||||
call "%~dp0\elixir.bat" +elixirc %*
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ $# -gt 0 ] && ([ "$1" = "--help" ] || [ "$1" = "-h" ]); then
|
||||
echo "Usage: `basename $0` [options] [.exs file] [data]
|
||||
|
||||
-v Prints version
|
||||
-e \"command\" Evaluates the given command (*)
|
||||
-r \"file\" Requires the given files/patterns (*)
|
||||
-S \"script\" Finds and executes the given script
|
||||
-pr \"file\" Requires the given files/patterns in parallel (*)
|
||||
-pa \"path\" Prepends the given path to Erlang code path (*)
|
||||
-pz \"path\" Appends the given path to Erlang code path (*)
|
||||
--app \"app\" Start the given app and its dependencies (*)
|
||||
--erl \"switches\" Switches to be passed down to erlang (*)
|
||||
--name \"name\" Makes and assigns a name to the distributed node
|
||||
--sname \"name\" Makes and assigns a short name to the distributed node
|
||||
--cookie \"cookie\" Sets a cookie for this distributed node
|
||||
--hidden Makes a hidden node
|
||||
--detached Starts the Erlang VM detached from console
|
||||
--remsh \"name\" Connects to a node using a remote shell
|
||||
--dot-iex \"path\" Overrides default .iex.exs file and uses path instead;
|
||||
path can be empty, then no file will be loaded
|
||||
|
||||
** Options marked with (*) can be given more than once
|
||||
** Options given after the .exs file or -- are passed down to the executed code
|
||||
** Options can be passed to the VM using ELIXIR_ERL_OPTIONS or --erl" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readlink_f () {
|
||||
cd "$(dirname "$1")" > /dev/null
|
||||
filename="$(basename "$1")"
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f "$(readlink "$filename")"
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
SELF=$(readlink_f "$0")
|
||||
SCRIPT_PATH=$(dirname "$SELF")
|
||||
|
||||
if [ "$OS" = "Windows_NT" ]; then NOSHELL="-noshell "; fi
|
||||
exec "$SCRIPT_PATH"/elixir --no-halt --erl "$NOSHELL -user Elixir.IEx.CLI" +iex "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
call "%~dp0\elixir.bat" +iex --erl "-user Elixir.IEx.CLI" --no-halt %*
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env elixir
|
||||
# Reminder: apply any changes in this file to bin\mix.bat
|
||||
Mix.start
|
||||
Mix.CLI.main
|
||||
@@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
call "%~dp0\elixir.bat" -e Mix.start -e Mix.CLI.main %*
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
# Store path to mix.bat as a FileInfo object
|
||||
$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat'))
|
||||
$newArgs = @()
|
||||
|
||||
for ($i = 0; $i -lt $args.length; $i++)
|
||||
{
|
||||
if ($args[$i] -is [array])
|
||||
{
|
||||
# Commas created the array so we need to reintroduce those commas
|
||||
for ($j = 0; $j -lt $args[$i].length - 1; $j++)
|
||||
{
|
||||
$newArgs += ($args[$i][$j] + ',')
|
||||
}
|
||||
$newArgs += $args[$i][-1]
|
||||
}
|
||||
else
|
||||
{
|
||||
$newArgs += $args[$i]
|
||||
}
|
||||
}
|
||||
|
||||
# Corrected arguments are ready to pass to batch file
|
||||
& $mixBatPath $newArgs
|
||||
@@ -0,0 +1,7 @@
|
||||
import Config
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
|
||||
if config_env() == :test do
|
||||
config :logger, level: :warning
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
import Config
|
||||
|
||||
# Dev: no special config — all runtime settings come from env vars.
|
||||
@@ -0,0 +1,5 @@
|
||||
import Config
|
||||
|
||||
# Production: no hardcoded values here. All runtime config (PORT, AGENTS,
|
||||
# ADAPTER_API_KEY, CHAT_WEBHOOK_BASIC) comes from the systemd EnvironmentFile
|
||||
# in the NixOS service module.
|
||||
@@ -0,0 +1,7 @@
|
||||
import Config
|
||||
|
||||
# Test environment: the app starts with an empty agent store (no AGENTS env
|
||||
# seeding — agents are managed via the admin API). ADAPTER_API_KEY /
|
||||
# ADMIN_API_KEY are set in test/test_helper.exs. AGENTS_FILE must be set HERE
|
||||
# (config loads before the app boots) to a writable tmp path.
|
||||
System.put_env("AGENTS_FILE", Path.join(System.tmp_dir!(), "n8n-openai-test-agents.json"))
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1788881743,
|
||||
"narHash": "sha256-2V9GZGvPfrNzxFozhI9dcqV+c3QdA8YZrvAAzqEB+dI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d6524aaca2ff07876657ae2b323f24be4874944b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
description = "OpenAI-compatible adapter exposing n8n chat agents behind /v1/chat/completions";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs, ... }:
|
||||
let
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
beamPackages = pkgs.beamPackages;
|
||||
in
|
||||
{
|
||||
default = beamPackages.mixRelease {
|
||||
pname = "n8n-openai-adapter";
|
||||
version = "0.1.0";
|
||||
src = self;
|
||||
mixFodDeps = beamPackages.fetchMixDeps {
|
||||
pname = "n8n-openai-adapter";
|
||||
version = "0.1.0";
|
||||
src = self;
|
||||
hash = "sha256-sdAhpZUeF33V9xjEa/z/aTmCllfetMjO/1XyfJfUNao=";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
overlays.default = final: prev: {
|
||||
n8n-openai-adapter = self.packages.${final.stdenv.system}.default;
|
||||
};
|
||||
|
||||
# Proper NixOS module: consume with
|
||||
# imports = [ inputs.n8n-openai-adapter.nixosModules.default ];
|
||||
# services.n8n-openai-adapter = { enable = true; domain = "..."; port = 8134; };
|
||||
nixosModules.default = import ./nixos-module.nix;
|
||||
};
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
defmodule EEx.SyntaxError do
|
||||
defexception [:message, :file, :line]
|
||||
|
||||
def message(exception) do
|
||||
"#{exception.file}:#{exception.line}: #{exception.message}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EEx do
|
||||
@moduledoc ~S"""
|
||||
EEx stands for Embedded Elixir. It allows you to embed
|
||||
Elixir code inside a string in a robust way:
|
||||
|
||||
iex> EEx.eval_string "foo <%= bar %>", [bar: "baz"]
|
||||
"foo baz"
|
||||
|
||||
## API
|
||||
|
||||
This module provides 3 main APIs for you to use:
|
||||
|
||||
1. Evaluate a string (`eval_string`) or a file (`eval_file`)
|
||||
directly. This is the simplest API to use but also the
|
||||
slowest, since the code is evaluated and not compiled before.
|
||||
|
||||
2. Define a function from a string (`function_from_string`)
|
||||
or a file (`function_from_file`). This allows you to embed
|
||||
the template as a function inside a module which will then
|
||||
be compiled. This is the preferred API if you have access
|
||||
to the template at compilation time.
|
||||
|
||||
3. Compile a string (`compile_string`) or a file (`compile_file`)
|
||||
into Elixir syntax tree. This is the API used by both functions
|
||||
above and is available to you if you want to provide your own
|
||||
ways of handling the compiled template.
|
||||
|
||||
## Options
|
||||
|
||||
All functions in this module accepts EEx-related options.
|
||||
They are:
|
||||
|
||||
* `:line` - the line to be used as the template start. Defaults to 1.
|
||||
* `:file` - the file to be used in the template. Defaults to the given
|
||||
file the template is read from or to "nofile" when compiling from a string.
|
||||
* `:engine` - the EEx engine to be used for compilation.
|
||||
|
||||
## Engine
|
||||
|
||||
EEx has the concept of engines which allows you to modify or
|
||||
transform the code extracted from the given string or file.
|
||||
|
||||
By default, `EEx` uses the `EEx.SmartEngine` that provides some
|
||||
conveniences on top of the simple `EEx.Engine`.
|
||||
|
||||
### Tags
|
||||
|
||||
`EEx.SmartEngine` supports the following tags:
|
||||
|
||||
<% Elixir expression - inline with output %>
|
||||
<%= Elixir expression - replace with result %>
|
||||
<%% EEx quotation - returns the contents inside %>
|
||||
<%# Comments - they are discarded from source %>
|
||||
|
||||
All expressions that output something to the template
|
||||
**must** use the equals sign (`=`). Since everything in
|
||||
Elixir is a macro, there are no exceptions for this rule.
|
||||
For example, while some template languages would special-
|
||||
case `if` clauses, they are treated the same in EEx and
|
||||
also require `=` in order to have their result printed:
|
||||
|
||||
<%= if true do %>
|
||||
It is obviously true
|
||||
<% else %>
|
||||
This will never appear
|
||||
<% end %>
|
||||
|
||||
Notice that different engines may have different rules
|
||||
for each tag. Other tags may be added in future versions.
|
||||
|
||||
### Macros
|
||||
|
||||
`EEx.SmartEngine` also adds some macros to your template.
|
||||
An example is the `@` macro which allows easy data access
|
||||
in a template:
|
||||
|
||||
iex> EEx.eval_string "<%= @foo %>", assigns: [foo: 1]
|
||||
"1"
|
||||
|
||||
In other words, `<%= @foo %>` is simply translated to:
|
||||
|
||||
<%= Dict.get assigns, :foo %>
|
||||
|
||||
The assigns extension is useful when the number of variables
|
||||
required by the template is not specified at compilation time.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the string.
|
||||
|
||||
The kind (`:def` or `:defp`) must be given, the
|
||||
function name, its arguments and the compilation options.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> defmodule Sample do
|
||||
...> require EEx
|
||||
...> EEx.function_from_string :def, :sample, "<%= a + b %>", [:a, :b]
|
||||
...> end
|
||||
iex> Sample.sample(1, 2)
|
||||
"3"
|
||||
|
||||
"""
|
||||
defmacro function_from_string(kind, name, source, args \\ [], options \\ []) do
|
||||
quote bind_quoted: binding do
|
||||
info = Keyword.merge [file: __ENV__.file, line: __ENV__.line], options
|
||||
args = Enum.map args, fn arg -> {arg, [line: info[:line]], nil} end
|
||||
compiled = EEx.compile_string(source, info)
|
||||
|
||||
case kind do
|
||||
:def -> def(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
|
||||
:defp -> defp(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the file contents.
|
||||
|
||||
The kind (`:def` or `:defp`) must be given, the
|
||||
function name, its arguments and the compilation options.
|
||||
|
||||
This function is useful in case you have templates but
|
||||
you want to precompile inside a module for speed.
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.eex
|
||||
<%= a + b %>
|
||||
|
||||
# sample.ex
|
||||
defmodule Sample do
|
||||
require EEx
|
||||
EEx.function_from_file :def, :sample, "sample.eex", [:a, :b]
|
||||
end
|
||||
|
||||
# iex
|
||||
Sample.sample(1, 2) #=> "3"
|
||||
|
||||
"""
|
||||
defmacro function_from_file(kind, name, file, args \\ [], options \\ []) do
|
||||
quote bind_quoted: binding do
|
||||
info = Keyword.merge options, [file: file, line: 1]
|
||||
args = Enum.map args, fn arg -> {arg, [line: 1], nil} end
|
||||
compiled = EEx.compile_file(file, info)
|
||||
|
||||
@external_resource file
|
||||
@file file
|
||||
case kind do
|
||||
:def -> def(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
|
||||
:defp -> defp(unquote(name)(unquote_splicing(args)), do: unquote(compiled))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a string `source` and generate a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
"""
|
||||
def compile_string(source, options \\ []) do
|
||||
EEx.Compiler.compile(source, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a `filename` and generate a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
"""
|
||||
def compile_file(filename, options \\ []) do
|
||||
options = Keyword.merge options, [file: filename, line: 1]
|
||||
compile_string(File.read!(filename), options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a string `source` and evaluate the values using the `bindings`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> EEx.eval_string "foo <%= bar %>", [bar: "baz"]
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
def eval_string(source, bindings \\ [], options \\ []) do
|
||||
compiled = compile_string(source, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a `filename` and evaluate the values using the `bindings`.
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.ex
|
||||
foo <%= bar %>
|
||||
|
||||
# iex
|
||||
EEx.eval_file "sample.ex", [bar: "baz"] #=> "foo baz"
|
||||
|
||||
"""
|
||||
def eval_file(filename, bindings \\ [], options \\ []) do
|
||||
options = Keyword.put options, :file, filename
|
||||
compiled = compile_file(filename, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
### Helpers
|
||||
|
||||
defp do_eval(compiled, bindings, options) do
|
||||
{result, _} = Code.eval_quoted(compiled, bindings, options)
|
||||
result
|
||||
end
|
||||
end
|
||||
@@ -1,121 +0,0 @@
|
||||
defmodule EEx.Compiler do
|
||||
@moduledoc false
|
||||
|
||||
# When changing this setting, don't forget to update the docs for EEx
|
||||
@default_engine EEx.SmartEngine
|
||||
|
||||
@doc """
|
||||
This is the compilation entry point. It glues the tokenizer
|
||||
and the engine together by handling the tokens and invoking
|
||||
the engine every time a full expression or text is received.
|
||||
"""
|
||||
def compile(source, opts) do
|
||||
file = opts[:file] || "nofile"
|
||||
line = opts[:line] || 1
|
||||
case EEx.Tokenizer.tokenize(source, line) do
|
||||
{:ok, tokens} ->
|
||||
state = %{engine: opts[:engine] || @default_engine,
|
||||
file: file, line: line, quoted: [], start_line: nil}
|
||||
generate_buffer(tokens, "", [], state)
|
||||
{:error, line, message} ->
|
||||
raise EEx.SyntaxError, line: line, file: file, message: message
|
||||
end
|
||||
end
|
||||
|
||||
# Generates the buffers by handling each expression from the tokenizer
|
||||
|
||||
defp generate_buffer([{:text, chars}|t], buffer, scope, state) do
|
||||
buffer = state.engine.handle_text(buffer, IO.chardata_to_string(chars))
|
||||
generate_buffer(t, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:expr, line, mark, chars}|t], buffer, scope, state) do
|
||||
expr = Code.string_to_quoted!(chars, [line: line, file: state.file])
|
||||
buffer = state.engine.handle_expr(buffer, mark, expr)
|
||||
generate_buffer(t, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:start_expr, start_line, mark, chars}|t], buffer, scope, state) do
|
||||
{contents, line, t} = look_ahead_text(t, start_line, chars)
|
||||
{contents, t} = generate_buffer(t, "", [contents|scope],
|
||||
%{state | quoted: [], line: line, start_line: start_line})
|
||||
buffer = state.engine.handle_expr(buffer, mark, contents)
|
||||
generate_buffer(t, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:middle_expr, line, _, chars}|t], buffer, [current|scope], state) do
|
||||
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
|
||||
generate_buffer(t, "", [wrapped|scope], %{state | line: line})
|
||||
end
|
||||
|
||||
defp generate_buffer([{:end_expr, line, _, chars}|t], buffer, [current|_], state) do
|
||||
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
|
||||
tuples = Code.string_to_quoted!(wrapped, [line: state.start_line, file: state.file])
|
||||
buffer = insert_quoted(tuples, state.quoted)
|
||||
{buffer, t}
|
||||
end
|
||||
|
||||
defp generate_buffer([{:end_expr, line, _, chars}|_], _buffer, [], state) do
|
||||
raise EEx.SyntaxError, message: "unexpected token #{inspect chars}", file: state.file, line: line
|
||||
end
|
||||
|
||||
defp generate_buffer([], buffer, [], state) do
|
||||
state.engine.handle_body(buffer)
|
||||
end
|
||||
|
||||
defp generate_buffer([], _buffer, _scope, state) do
|
||||
raise EEx.SyntaxError, message: "unexpected end of string, expected a closing '<% end %>'",
|
||||
file: state.file, line: state.line
|
||||
end
|
||||
|
||||
# Creates a placeholder and wrap it inside the expression block
|
||||
|
||||
defp wrap_expr(current, line, buffer, chars, state) do
|
||||
new_lines = List.duplicate(?\n, line - state.line)
|
||||
key = length(state.quoted)
|
||||
placeholder = '__EEX__(' ++ Integer.to_char_list(key) ++ ');'
|
||||
{current ++ placeholder ++ new_lines ++ chars,
|
||||
%{state | quoted: [{key, buffer}|state.quoted]}}
|
||||
end
|
||||
|
||||
# Look text ahead on expressions
|
||||
|
||||
defp look_ahead_text([{:text, text}, {:middle_expr, line, _, chars}|t]=list, start, contents) do
|
||||
if only_spaces?(text) do
|
||||
{contents ++ text ++ chars, line, t}
|
||||
else
|
||||
{contents, start, list}
|
||||
end
|
||||
end
|
||||
|
||||
defp look_ahead_text(t, start, contents) do
|
||||
{contents, start, t}
|
||||
end
|
||||
|
||||
defp only_spaces?(chars) do
|
||||
Enum.all?(chars, &(&1 in [?\s, ?\t, ?\r, ?\n]))
|
||||
end
|
||||
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||
{^key, value} = List.keyfind quoted, key, 0
|
||||
value
|
||||
end
|
||||
|
||||
defp insert_quoted({left, line, right}, quoted) do
|
||||
{insert_quoted(left, quoted), line, insert_quoted(right, quoted)}
|
||||
end
|
||||
|
||||
defp insert_quoted({left, right}, quoted) do
|
||||
{insert_quoted(left, quoted), insert_quoted(right, quoted)}
|
||||
end
|
||||
|
||||
defp insert_quoted(list, quoted) when is_list(list) do
|
||||
Enum.map list, &insert_quoted(&1, quoted)
|
||||
end
|
||||
|
||||
defp insert_quoted(other, _quoted) do
|
||||
other
|
||||
end
|
||||
end
|
||||
@@ -1,113 +0,0 @@
|
||||
defmodule EEx.Engine do
|
||||
@moduledoc ~S"""
|
||||
Basic EEx engine that ships with Elixir.
|
||||
|
||||
An engine needs to implement three functions:
|
||||
|
||||
* `handle_body(quoted)` - receives the final built quoted
|
||||
expression, should do final post-processing and return a
|
||||
quoted expression.
|
||||
|
||||
* `handle_text(buffer, text)` - it receives the buffer,
|
||||
the text and must return a new quoted expression.
|
||||
|
||||
* `handle_expr(buffer, marker, expr)` - it receives the buffer,
|
||||
the marker, the expr and must return a new quoted expression.
|
||||
|
||||
The marker is what follows exactly after `<%`. For example,
|
||||
`<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
|
||||
as marker. The allowed markers so far are: `""` and `"="`.
|
||||
|
||||
Read `handle_expr/3` below for more information about the markers
|
||||
implemented by default by this engine.
|
||||
|
||||
`EEx.Engine` can be used directly if one desires to use the
|
||||
default implementations for the functions above.
|
||||
"""
|
||||
|
||||
use Behaviour
|
||||
|
||||
defcallback handle_body(Macro.t) :: Macro.t
|
||||
defcallback handle_text(Macro.t, binary) :: Macro.t
|
||||
defcallback handle_expr(Macro.t, binary, Macro.t) :: Macro.t
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
@behaviour EEx.Engine
|
||||
|
||||
def handle_body(body) do
|
||||
EEx.Engine.handle_body(body)
|
||||
end
|
||||
|
||||
def handle_text(buffer, text) do
|
||||
EEx.Engine.handle_text(buffer, text)
|
||||
end
|
||||
|
||||
def handle_expr(buffer, mark, expr) do
|
||||
EEx.Engine.handle_expr(buffer, mark, expr)
|
||||
end
|
||||
|
||||
defoverridable [handle_body: 1, handle_expr: 3, handle_text: 2]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles assigns in quoted expressions.
|
||||
|
||||
This can be added to any custom engine by invoking
|
||||
`handle_assign/3` with `Macro.prewalk/1`:
|
||||
|
||||
def handle_expr(buffer, token, expr) do
|
||||
expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
|
||||
EEx.Engine.handle_expr(buffer, token, expr)
|
||||
end
|
||||
|
||||
"""
|
||||
def handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
|
||||
line = meta[:line] || 0
|
||||
quote line: line, do: Dict.get(var!(assigns), unquote(name))
|
||||
end
|
||||
|
||||
def handle_assign(arg) do
|
||||
arg
|
||||
end
|
||||
|
||||
@doc """
|
||||
The default implementation implementation simply returns the
|
||||
given expression.
|
||||
"""
|
||||
def handle_body(quoted) do
|
||||
quoted
|
||||
end
|
||||
|
||||
@doc """
|
||||
The default implementation simply concatenates text to the buffer.
|
||||
"""
|
||||
def handle_text(buffer, text) do
|
||||
quote do: unquote(buffer) <> unquote(text)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Implements expressions according to the markers.
|
||||
|
||||
<% Elixir expression - inline with output %>
|
||||
<%= Elixir expression - replace with result %>
|
||||
|
||||
All other markers are not implemented by this engine.
|
||||
"""
|
||||
def handle_expr(buffer, "=", expr) do
|
||||
quote do
|
||||
tmp = unquote(buffer)
|
||||
tmp <> String.Chars.to_string(unquote(expr))
|
||||
end
|
||||
end
|
||||
|
||||
def handle_expr(buffer, "", expr) do
|
||||
quote do
|
||||
tmp = unquote(buffer)
|
||||
unquote(expr)
|
||||
tmp
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
defmodule EEx.SmartEngine do
|
||||
@moduledoc """
|
||||
The default engine used by EEx.
|
||||
|
||||
It includes assigns (like `@foo`) and possibly other
|
||||
conveniences in the future.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> EEx.eval_string("<%= @foo %>", assigns: [foo: 1])
|
||||
"1"
|
||||
|
||||
In the example above, we can access the value `foo` under
|
||||
the binding `assigns` using `@foo`. This is useful when
|
||||
a template, after compiled, may receive different assigns
|
||||
and the developer don't want to recompile it for each
|
||||
variable set.
|
||||
|
||||
Assigns can also be used when compiled to a function:
|
||||
|
||||
# sample.eex
|
||||
<%= @a + @b %>
|
||||
|
||||
# sample.ex
|
||||
defmodule Sample do
|
||||
require EEx
|
||||
EEx.function_from_file :def, :sample, "sample.eex", [:assigns]
|
||||
end
|
||||
|
||||
# iex
|
||||
Sample.sample(a: 1, b: 2) #=> "3"
|
||||
|
||||
"""
|
||||
|
||||
use EEx.Engine
|
||||
|
||||
def handle_expr(buffer, mark, expr) do
|
||||
expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
|
||||
super(buffer, mark, expr)
|
||||
end
|
||||
end
|
||||
@@ -1,172 +0,0 @@
|
||||
defmodule EEx.Tokenizer do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Tokenizes the given char list or binary.
|
||||
|
||||
It returns {:ok, list} with the following tokens:
|
||||
|
||||
* `{:text, contents}`
|
||||
* `{:expr, line, marker, contents}`
|
||||
* `{:start_expr, line, marker, contents}`
|
||||
* `{:middle_expr, line, marker, contents}`
|
||||
* `{:end_expr, line, marker, contents}`
|
||||
|
||||
Or `{:error, line, error}` in case of errors.
|
||||
"""
|
||||
def tokenize(bin, line) when is_binary(bin) do
|
||||
tokenize(String.to_char_list(bin), line)
|
||||
end
|
||||
|
||||
def tokenize(list, line) do
|
||||
tokenize(list, line, [], [])
|
||||
end
|
||||
|
||||
defp tokenize('<%%' ++ t, line, buffer, acc) do
|
||||
case expr(t, line, [?%, ?<|buffer]) do
|
||||
{:error, _, _} = error -> error
|
||||
{:ok, buffer, new_line, rest} ->
|
||||
tokenize rest, new_line, [?>, ?%|buffer], acc
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('<%#' ++ t, line, buffer, acc) do
|
||||
case expr(t, line, []) do
|
||||
{:error, _, _} = error -> error
|
||||
{:ok, _, new_line, rest} ->
|
||||
tokenize rest, new_line, buffer, acc
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('<%' ++ t, line, buffer, acc) do
|
||||
{marker, t} = retrieve_marker(t)
|
||||
|
||||
case expr(t, line, []) do
|
||||
{:error, _, _} = error -> error
|
||||
{:ok, expr, new_line, rest} ->
|
||||
token = token_name(expr)
|
||||
acc = tokenize_text(buffer, acc)
|
||||
final = {token, line, marker, Enum.reverse(expr)}
|
||||
tokenize rest, new_line, [], [final | acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('\n' ++ t, line, buffer, acc) do
|
||||
tokenize t, line + 1, [?\n|buffer], acc
|
||||
end
|
||||
|
||||
defp tokenize([h|t], line, buffer, acc) do
|
||||
tokenize t, line, [h|buffer], acc
|
||||
end
|
||||
|
||||
defp tokenize([], _line, buffer, acc) do
|
||||
{:ok, Enum.reverse(tokenize_text(buffer, acc))}
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
|
||||
defp retrieve_marker('=' ++ t) do
|
||||
{"=", t}
|
||||
end
|
||||
|
||||
defp retrieve_marker(t) do
|
||||
{"", t}
|
||||
end
|
||||
|
||||
# Tokenize an expression until we find %>
|
||||
|
||||
defp expr([?%, ?>|t], line, buffer) do
|
||||
{:ok, buffer, line, t}
|
||||
end
|
||||
|
||||
defp expr('\n' ++ t, line, buffer) do
|
||||
expr t, line + 1, [?\n|buffer]
|
||||
end
|
||||
|
||||
defp expr([h|t], line, buffer) do
|
||||
expr t, line, [h|buffer]
|
||||
end
|
||||
|
||||
defp expr([], line, _buffer) do
|
||||
{:error, line, "missing token '%>'"}
|
||||
end
|
||||
|
||||
# Receive an expression content and check
|
||||
# if it is a start, middle or an end token.
|
||||
#
|
||||
# Start tokens finish with `do` and `fn ->`
|
||||
# Middle tokens are marked with `->` or keywords
|
||||
# End tokens contain only the end word
|
||||
|
||||
defp token_name([h|t]) when h in [?\s, ?\t] do
|
||||
token_name(t)
|
||||
end
|
||||
|
||||
defp token_name('od' ++ [h|_]) when h in [?\s, ?\t, ?)] do
|
||||
:start_expr
|
||||
end
|
||||
|
||||
defp token_name('>-' ++ rest) do
|
||||
rest = Enum.reverse(rest)
|
||||
|
||||
# Tokenize the remaining passing check_terminators as
|
||||
# false, which relax the tokenizer to not error on
|
||||
# unmatched pairs. Then, we check if there is a "fn"
|
||||
# token and, if so, it is not followed by an "end"
|
||||
# token. If this is the case, we are on a start expr.
|
||||
case :elixir_tokenizer.tokenize(rest, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, _line, tokens} ->
|
||||
tokens = Enum.reverse(tokens)
|
||||
fn_index = fn_index(tokens)
|
||||
|
||||
if fn_index && end_index(tokens) > fn_index do
|
||||
:start_expr
|
||||
else
|
||||
:middle_expr
|
||||
end
|
||||
_error ->
|
||||
:middle_expr
|
||||
end
|
||||
end
|
||||
|
||||
defp token_name('esle' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('retfa' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('hctac' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('eucser' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('dne' ++ t), do: check_spaces(t, :end_expr)
|
||||
|
||||
defp token_name(_) do
|
||||
:expr
|
||||
end
|
||||
|
||||
defp fn_index(tokens) do
|
||||
Enum.find_index tokens, fn
|
||||
{:fn_paren, _} -> true
|
||||
{:fn, _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp end_index(tokens) do
|
||||
Enum.find_index(tokens, &match?({:end, _}, &1)) || :infinity
|
||||
end
|
||||
|
||||
defp check_spaces(string, token) do
|
||||
if Enum.all?(string, &(&1 in [?\s, ?\t])) do
|
||||
token
|
||||
else
|
||||
:expr
|
||||
end
|
||||
end
|
||||
|
||||
# Tokenize the buffered text by appending
|
||||
# it to the given accumulator.
|
||||
|
||||
defp tokenize_text([], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp tokenize_text(buffer, acc) do
|
||||
[{:text, Enum.reverse(buffer)} | acc]
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
defmodule EEx.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :eex,
|
||||
version: System.version,
|
||||
build_per_environment: false]
|
||||
end
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
Code.require_file "../test_helper.exs", __DIR__
|
||||
|
||||
defmodule EEx.SmartEngineTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "evaluates simple string" do
|
||||
assert_eval "foo bar", "foo bar"
|
||||
end
|
||||
|
||||
test "evaluates with assigns as keywords" do
|
||||
assert_eval "1", "<%= @foo %>", assigns: [foo: 1]
|
||||
end
|
||||
|
||||
test "evaluates with assigns as a map" do
|
||||
assert_eval "1", "<%= @foo %>", assigns: %{foo: 1}
|
||||
end
|
||||
|
||||
test "evaluates with loops" do
|
||||
assert_eval "1\n2\n3\n", "<%= for x <- [1, 2, 3] do %><%= x %>\n<% end %>"
|
||||
end
|
||||
|
||||
test "compiled preserved line numbers" do
|
||||
result = EEx.compile_string("<%= @hello %>", engine: EEx.SmartEngine)
|
||||
Macro.prewalk(result, fn
|
||||
{_left, meta, _right} ->
|
||||
assert Keyword.get(meta, :line, 0) in [0, 1]
|
||||
_ ->
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file)
|
||||
assert result == expected
|
||||
end
|
||||
end
|
||||
@@ -1,106 +0,0 @@
|
||||
Code.require_file "../test_helper.exs", __DIR__
|
||||
|
||||
defmodule EEx.TokenizerTest do
|
||||
use ExUnit.Case, async: true
|
||||
require EEx.Tokenizer, as: T
|
||||
|
||||
test "simple chars lists" do
|
||||
assert T.tokenize('foo', 1) == {:ok, [{:text, 'foo'}]}
|
||||
end
|
||||
|
||||
test "simple strings" do
|
||||
assert T.tokenize("foo", 1) == {:ok, [{:text, 'foo'}]}
|
||||
end
|
||||
|
||||
test "strings with embedded code" do
|
||||
assert T.tokenize('foo <% bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, "", ' bar '}]}
|
||||
end
|
||||
|
||||
test "strings with embedded equals code" do
|
||||
assert T.tokenize('foo <%= bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, "=", ' bar '}]}
|
||||
end
|
||||
|
||||
test "strings with more than one line" do
|
||||
assert T.tokenize('foo\n<%= bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo\n'}, {:expr, 2, "=", ' bar '}]}
|
||||
end
|
||||
|
||||
test "strings with more than one line and expression with more than one line" do
|
||||
string = '''
|
||||
foo <%= bar
|
||||
|
||||
baz %>
|
||||
<% foo %>
|
||||
'''
|
||||
|
||||
assert T.tokenize(string, 1) == {:ok, [
|
||||
{:text, 'foo '},
|
||||
{:expr, 1, "=", ' bar\n\nbaz '},
|
||||
{:text, '\n'},
|
||||
{:expr, 4, "", ' foo '},
|
||||
{:text, '\n'}
|
||||
]}
|
||||
end
|
||||
|
||||
test "quotation" do
|
||||
assert T.tokenize('foo <%% true %>', 1) == {:ok, [
|
||||
{:text, 'foo <% true %>'}
|
||||
]}
|
||||
end
|
||||
|
||||
test "quotation with do/end" do
|
||||
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1) == {:ok, [
|
||||
{:text, 'foo <% true do %>bar<% end %>'}
|
||||
]}
|
||||
end
|
||||
|
||||
test "comments" do
|
||||
assert T.tokenize('foo <%# true %>', 1) == {:ok, [
|
||||
{:text, 'foo '}
|
||||
]}
|
||||
end
|
||||
|
||||
test "comments with do/end" do
|
||||
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1) == {:ok, [
|
||||
{:text, 'foo bar'}
|
||||
]}
|
||||
end
|
||||
|
||||
test "strings with embedded do end" do
|
||||
assert T.tokenize('foo <% if true do %>bar<% end %>', 1) == {:ok, [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, "", ' if true do '},
|
||||
{:text, 'bar'},
|
||||
{:end_expr, 1, "", ' end '}
|
||||
]}
|
||||
end
|
||||
|
||||
test "strings with embedded -> end" do
|
||||
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1) == {:ok, [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, "", ' cond do '},
|
||||
{:middle_expr, 1, "", ' false -> '},
|
||||
{:text, 'bar'},
|
||||
{:middle_expr, 1, "", ' true -> '},
|
||||
{:text, 'baz'},
|
||||
{:end_expr, 1, "", ' end '}
|
||||
]}
|
||||
end
|
||||
|
||||
test "strings with embedded keywords blocks" do
|
||||
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1) == {:ok, [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, "", ' if true do '},
|
||||
{:text, 'bar'},
|
||||
{:middle_expr, 1, "", ' else '},
|
||||
{:text, 'baz'},
|
||||
{:end_expr, 1, "", ' end '}
|
||||
]}
|
||||
end
|
||||
|
||||
test "raise syntax error when there is start mark and no end mark" do
|
||||
assert T.tokenize('foo <% :bar', 1) == {:error, 1, "missing token '%>'"}
|
||||
end
|
||||
end
|
||||
@@ -1,394 +0,0 @@
|
||||
Code.require_file "test_helper.exs", __DIR__
|
||||
|
||||
require EEx
|
||||
|
||||
defmodule EExTest.Compiled do
|
||||
def before_compile do
|
||||
fill_in_stacktrace
|
||||
{__ENV__.line, hd(tl(System.stacktrace))}
|
||||
end
|
||||
|
||||
EEx.function_from_string :def, :string_sample, "<%= a + b %>", [:a, :b]
|
||||
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
||||
EEx.function_from_file :defp, :private_file_sample, filename, [:bar]
|
||||
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
||||
EEx.function_from_file :def, :public_file_sample, filename, [:bar]
|
||||
|
||||
def file_sample(arg), do: private_file_sample(arg)
|
||||
|
||||
def after_compile do
|
||||
fill_in_stacktrace
|
||||
{__ENV__.line, hd(tl(System.stacktrace))}
|
||||
end
|
||||
|
||||
@file "unknown"
|
||||
def unknown do
|
||||
fill_in_stacktrace
|
||||
{__ENV__.line, hd(tl(System.stacktrace))}
|
||||
end
|
||||
|
||||
defp fill_in_stacktrace do
|
||||
try do
|
||||
:erlang.error "failed"
|
||||
catch
|
||||
:error, _ -> System.stacktrace
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Clause do
|
||||
defmacro defclause(expr, block) do
|
||||
quote do
|
||||
def unquote(expr), unquote(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EExTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
doctest EEx
|
||||
doctest EEx.Engine
|
||||
doctest EEx.SmartEngine
|
||||
|
||||
test "evaluates simple string" do
|
||||
assert_eval "foo bar", "foo bar"
|
||||
end
|
||||
|
||||
test "evaluates with embedded" do
|
||||
assert_eval "foo bar", "foo <%= :bar %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded and the binding" do
|
||||
assert EEx.eval_string("foo <%= bar %>", [bar: 1]) == "foo 1"
|
||||
end
|
||||
|
||||
test "evaluates with embedded do end" do
|
||||
assert_eval "foo bar", "foo <%= if true do %>bar<% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded do end and eval the expression" do
|
||||
assert_eval "foo ", "foo <%= if false do %>bar<% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded do end and nested print expression" do
|
||||
assert_eval "foo bar", "foo <%= if true do %><%= :bar %><% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded do end and nested expressions" do
|
||||
assert_eval "foo bar baz", "foo <%= if true do %>bar <% Process.put(:eex_text, 1) %><%= :baz %><% end %>"
|
||||
assert Process.get(:eex_text) == 1
|
||||
end
|
||||
|
||||
test "evaluates with embedded middle expression" do
|
||||
assert_eval "foo bar", "foo <%= if true do %>bar<% else %>baz<% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with embedded middle expression and eval the expression" do
|
||||
assert_eval "foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with nested start expression" do
|
||||
assert_eval "foo bar", "foo <%= if true do %><%= if true do %>bar<% end %><% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with nested middle expression" do
|
||||
assert_eval "foo baz", "foo <%= if true do %><%= if false do %>bar<% else %>baz<% end %><% end %>"
|
||||
end
|
||||
|
||||
test "evaluates with defined variable" do
|
||||
assert_eval "foo 1", "foo <% bar = 1 %><%= bar %>"
|
||||
end
|
||||
|
||||
test "evaluates with require code" do
|
||||
assert_eval "foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>"
|
||||
end
|
||||
|
||||
test "evaluates with end of token" do
|
||||
assert_eval "foo bar %>", "foo bar %>"
|
||||
end
|
||||
|
||||
test "raises a syntax error when the token is invalid" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1: missing token '%>'", fn ->
|
||||
EEx.compile_string "foo <%= bar"
|
||||
end
|
||||
end
|
||||
|
||||
test "raises a syntax error when end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1: unexpected token ' end '", fn ->
|
||||
EEx.compile_string "foo <% end %>"
|
||||
end
|
||||
end
|
||||
|
||||
test "raises a syntax error when start expression is found without an end expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:2: unexpected end of string, expected a closing '<% end %>'", fn ->
|
||||
EEx.compile_string "foo\n<% if true do %>"
|
||||
end
|
||||
end
|
||||
|
||||
test "raises a syntax error when nested end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1: unexpected token ' end '", fn ->
|
||||
EEx.compile_string "foo <% if true do %><% end %><% end %>"
|
||||
end
|
||||
end
|
||||
|
||||
test "respects line numbers" do
|
||||
expected = """
|
||||
foo
|
||||
2
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "respects line numbers inside nested expressions" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
3
|
||||
|
||||
5
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= if true do %>
|
||||
<%= __ENV__.line %>
|
||||
<% end %>
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "respects line numbers inside start expression" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
true
|
||||
|
||||
5
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= if __ENV__.line == 2 do %>
|
||||
<%= true %>
|
||||
<% end %>
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "respects line numbers inside middle expression with ->" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
true
|
||||
|
||||
7
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= cond do %>
|
||||
<% false -> %> false
|
||||
<% __ENV__.line == 4 -> %>
|
||||
<%= true %>
|
||||
<% end %>
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "respects line number inside middle expressions with keywords" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
5
|
||||
|
||||
7
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= if false do %>
|
||||
<%= __ENV__.line %>
|
||||
<% else %>
|
||||
<%= __ENV__.line %>
|
||||
<% end %>
|
||||
<%= __ENV__.line %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "properly handle functions" do
|
||||
expected = """
|
||||
|
||||
Number 1
|
||||
|
||||
Number 2
|
||||
|
||||
Number 3
|
||||
|
||||
"""
|
||||
|
||||
string = """
|
||||
<%= Enum.map [1, 2, 3], fn x -> %>
|
||||
Number <%= x %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "do not consider already finished functions" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
true
|
||||
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= cond do %>
|
||||
<% false -> %> false
|
||||
<% fn -> 1 end -> %>
|
||||
<%= true %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval expected, string
|
||||
end
|
||||
|
||||
test "evaluates nested do expressions" do
|
||||
string = """
|
||||
<% y = ["a", "b", "c"] %>
|
||||
<%= cond do %>
|
||||
<% "a" in y -> %>
|
||||
Good
|
||||
<% true -> %>
|
||||
<% if true do %>true<% else %>false<% end %>
|
||||
Bad
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval "\n\n Good\n \n", string
|
||||
end
|
||||
|
||||
test "for comprehensions" do
|
||||
string = """
|
||||
<%= for _name <- packages || [] do %>
|
||||
<% end %>
|
||||
<%= all || :done %>
|
||||
"""
|
||||
assert_eval "\ndone\n", string, packages: nil, all: nil
|
||||
end
|
||||
|
||||
test "unicode" do
|
||||
template = """
|
||||
• <%= "•" %> •
|
||||
<%= "Jößé Vâlìm" %> Jößé Vâlìm
|
||||
"""
|
||||
result = EEx.eval_string(template)
|
||||
assert result == " • • •\n Jößé Vâlìm Jößé Vâlìm\n"
|
||||
end
|
||||
|
||||
test "evaluates the source from a given file" do
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template.eex")
|
||||
result = EEx.eval_file(filename)
|
||||
assert result == "foo bar.\n"
|
||||
end
|
||||
|
||||
test "evaluates the source from a given file with bindings" do
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
||||
result = EEx.eval_file(filename, [bar: 1])
|
||||
assert result == "foo 1\n"
|
||||
end
|
||||
|
||||
test "raises an Exception when there's an error with the given file" do
|
||||
assert_raise File.Error, "could not read file non-existent.eex: no such file or directory", fn ->
|
||||
filename = "non-existent.eex"
|
||||
EEx.compile_file(filename)
|
||||
end
|
||||
end
|
||||
|
||||
test "sets external resource attribute" do
|
||||
assert EExTest.Compiled.__info__(:attributes)[:external_resource] ==
|
||||
[Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")]
|
||||
end
|
||||
|
||||
test "defined from string" do
|
||||
assert EExTest.Compiled.string_sample(1, 2) == "3"
|
||||
end
|
||||
|
||||
test "defined from file" do
|
||||
assert EExTest.Compiled.file_sample(1) == "foo 1\n"
|
||||
assert EExTest.Compiled.public_file_sample(1) == "foo 1\n"
|
||||
end
|
||||
|
||||
test "defined from file do not affect backtrace" do
|
||||
assert EExTest.Compiled.before_compile ==
|
||||
{8,
|
||||
{EExTest.Compiled,
|
||||
:before_compile,
|
||||
0,
|
||||
[file: to_char_list(Path.relative_to_cwd(__ENV__.file)), line: 7]
|
||||
}
|
||||
}
|
||||
|
||||
assert EExTest.Compiled.after_compile ==
|
||||
{23,
|
||||
{EExTest.Compiled,
|
||||
:after_compile,
|
||||
0,
|
||||
[file: to_char_list(Path.relative_to_cwd(__ENV__.file)), line: 22]
|
||||
}
|
||||
}
|
||||
|
||||
assert EExTest.Compiled.unknown ==
|
||||
{29,
|
||||
{EExTest.Compiled,
|
||||
:unknown,
|
||||
0,
|
||||
[file: 'unknown', line: 28]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defmodule TestEngine do
|
||||
@behaviour EEx.Engine
|
||||
|
||||
def handle_body(body) do
|
||||
{:wrapped, body}
|
||||
end
|
||||
|
||||
def handle_text(buffer, text) do
|
||||
EEx.Engine.handle_text(buffer, text)
|
||||
end
|
||||
|
||||
def handle_expr(buffer, mark, expr) do
|
||||
EEx.Engine.handle_expr(buffer, mark, expr)
|
||||
end
|
||||
end
|
||||
|
||||
test "calls handle_body" do
|
||||
assert {:wrapped, "foo"} = EEx.eval_string("foo", [], engine: TestEngine)
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.Engine)
|
||||
assert result == expected
|
||||
end
|
||||
end
|
||||
-1
@@ -1 +0,0 @@
|
||||
foo <%= if true do %>bar.<% end %>
|
||||
@@ -1 +0,0 @@
|
||||
foo <%= bar %>
|
||||
@@ -1 +0,0 @@
|
||||
ExUnit.start [trace: "--trace" in System.argv]
|
||||
@@ -1,150 +0,0 @@
|
||||
defprotocol Access do
|
||||
@moduledoc """
|
||||
The Access protocol is used by `foo[bar]` and also
|
||||
empowers the nested update functions in Kernel.
|
||||
|
||||
For instance, `foo[bar]` translates `Access.get(foo, bar)`.
|
||||
`Kernel.get_in/2`, `Kernel.put_in/3`, `Kernel.update_in/3` and
|
||||
`Kernel.get_and_update_in/3` are also all powered by the Access
|
||||
protocol.
|
||||
|
||||
This protocol is implemented by default for keywords, maps
|
||||
and dictionary like types:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:a]
|
||||
1
|
||||
|
||||
iex> map = %{a: 1, b: 2}
|
||||
iex> map[:a]
|
||||
1
|
||||
|
||||
iex> star_ratings = %{1.0 => "★", 1.5 => "★☆", 2.0 => "★★"}
|
||||
iex> star_ratings[1.5]
|
||||
"★☆"
|
||||
|
||||
The key comparison must be implemented using the `===` operator.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Accesses the given key in the container.
|
||||
"""
|
||||
@spec get(t, term) :: t
|
||||
def get(container, key)
|
||||
|
||||
@doc """
|
||||
Gets a value and updates the given `key` in one pass.
|
||||
|
||||
The function must receive the value for the given `key`
|
||||
(or `nil` if the key doesn't exist in `container`) and
|
||||
the function must return a tuple containing the `get`
|
||||
value and the new value to be stored in the `container`.
|
||||
"""
|
||||
@spec get_and_update(t, term, (term -> {get, term})) :: {get, t} when get: var
|
||||
def get_and_update(container, key, fun)
|
||||
end
|
||||
|
||||
defimpl Access, for: List do
|
||||
def get(dict, key) when is_atom(key) do
|
||||
case :lists.keyfind(key, 1, dict) do
|
||||
{^key, value} -> value
|
||||
false -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def get(_dict, key) do
|
||||
raise ArgumentError,
|
||||
"the access protocol for lists expect the key to be an atom, got: #{inspect key}"
|
||||
end
|
||||
|
||||
def get_and_update(dict, key, fun) when is_atom(key) do
|
||||
get_and_update(dict, [], key, fun)
|
||||
end
|
||||
|
||||
defp get_and_update([{key, value}|t], acc, key, fun) do
|
||||
{get, update} = fun.(value)
|
||||
{get, :lists.reverse(acc, [{key, update}|t])}
|
||||
end
|
||||
|
||||
defp get_and_update([h|t], acc, key, fun) do
|
||||
get_and_update(t, [h|acc], key, fun)
|
||||
end
|
||||
|
||||
defp get_and_update([], acc, key, fun) do
|
||||
{get, update} = fun.(nil)
|
||||
{get, [{key, update}|:lists.reverse(acc)]}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Access, for: Map do
|
||||
def get(map, key) do
|
||||
case :maps.find(key, map) do
|
||||
{:ok, value} -> value
|
||||
:error -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_and_update(map, key, fun) do
|
||||
value =
|
||||
case :maps.find(key, map) do
|
||||
{:ok, value} -> value
|
||||
:error -> nil
|
||||
end
|
||||
|
||||
{get, update} = fun.(value)
|
||||
{get, :maps.put(key, update, map)}
|
||||
end
|
||||
|
||||
def get!(%{} = map, key) do
|
||||
case :maps.find(key, map) do
|
||||
{:ok, value} -> value
|
||||
:error -> raise KeyError, key: key, term: map
|
||||
end
|
||||
end
|
||||
|
||||
def get!(other, key) do
|
||||
raise ArgumentError,
|
||||
"could not get key #{inspect key}. Expected map/struct, got: #{inspect other}"
|
||||
end
|
||||
|
||||
def get_and_update!(%{} = map, key, fun) do
|
||||
case :maps.find(key, map) do
|
||||
{:ok, value} ->
|
||||
{get, update} = fun.(value)
|
||||
{get, :maps.put(key, update, map)}
|
||||
:error ->
|
||||
raise KeyError, key: key, term: map
|
||||
end
|
||||
end
|
||||
|
||||
def get_and_update!(other, key, _fun) do
|
||||
raise ArgumentError,
|
||||
"could not put/update key #{inspect key}. Expected map/struct, got: #{inspect other}"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Access, for: Atom do
|
||||
def get(nil, _) do
|
||||
nil
|
||||
end
|
||||
|
||||
def get(atom, _) do
|
||||
undefined(atom)
|
||||
end
|
||||
|
||||
def get_and_update(nil, key, _fun) do
|
||||
raise ArgumentError,
|
||||
"could not put/update key #{inspect key} on a nil value"
|
||||
end
|
||||
|
||||
def get_and_update(atom, _key, _fun) do
|
||||
undefined(atom)
|
||||
end
|
||||
|
||||
defp undefined(atom) do
|
||||
raise Protocol.UndefinedError,
|
||||
protocol: @protocol,
|
||||
value: atom,
|
||||
description: "only the nil atom is supported"
|
||||
end
|
||||
end
|
||||
@@ -1,299 +0,0 @@
|
||||
defmodule Agent do
|
||||
@moduledoc """
|
||||
Agents are a simple abstraction around state.
|
||||
|
||||
Often in Elixir there is a need to share or store state that
|
||||
must be accessed from different processes or by the same process
|
||||
at different points in time.
|
||||
|
||||
The Agent module provides a basic server implementation that
|
||||
allows state to be retrieved and updated via a simple API.
|
||||
|
||||
## Examples
|
||||
|
||||
For example, in the Mix tool that ships with Elixir, we need
|
||||
to keep a set of all tasks executed by a given project. Since
|
||||
this set is shared, we can implement it with an Agent:
|
||||
|
||||
defmodule Mix.TasksServer do
|
||||
def start_link do
|
||||
Agent.start_link(fn -> HashSet.new end, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc "Checks if the task has already executed"
|
||||
def executed?(task, project) do
|
||||
item = {task, project}
|
||||
Agent.get(__MODULE__, fn set ->
|
||||
item in set
|
||||
end)
|
||||
end
|
||||
|
||||
@doc "Marks a task as executed"
|
||||
def put_task(task, project) do
|
||||
item = {task, project}
|
||||
Agent.update(__MODULE__, &Set.put(&1, item))
|
||||
end
|
||||
end
|
||||
|
||||
Note that agents still provide a segregation between the
|
||||
client and server APIs, as seen in GenServers. In particular,
|
||||
all code inside the function passed to the agent is executed
|
||||
by the agent. This distinction is important because you may
|
||||
want to avoid expensive operations inside the agent, as it will
|
||||
effectively block the agent until the request is fulfilled.
|
||||
|
||||
Consider these two examples:
|
||||
|
||||
# Compute in the agent/server
|
||||
def get_something(agent) do
|
||||
Agent.get(agent, fn state -> do_something_expensive(state) end)
|
||||
end
|
||||
|
||||
# Compute in the agent/client
|
||||
def get_something(agent) do
|
||||
Agent.get(agent, &(&1)) |> do_something_expensive()
|
||||
end
|
||||
|
||||
The first one blocks the agent while the second one copies
|
||||
all the state to the client and executes the operation in the client.
|
||||
The trade-off here is exactly if the data is small enough to be
|
||||
sent to the client cheaply or large enough to require processing on
|
||||
the server (or at least some initial processing).
|
||||
|
||||
## Name Registration
|
||||
|
||||
An Agent is bound to the same name registration rules as GenServers.
|
||||
Read more about it in the `GenServer` docs.
|
||||
|
||||
## A word on distributed agents
|
||||
|
||||
It is important to consider the limitations of distributed agents. Agents
|
||||
provides two APIs, one that works with anonymous functions and another
|
||||
that expects explicit module, function and arguments.
|
||||
|
||||
In a distributed setup with multiple nodes, the API that accepts anonymous
|
||||
functions only works if the caller (client) and the agent have the same
|
||||
version of the caller module.
|
||||
|
||||
Keep in mind this issue also shows up when performing "rolling upgrades"
|
||||
with agents. By rolling upgrades we mean the following situation: you wish
|
||||
to deploy a new version of your software by *shutting down* some of your
|
||||
nodes and replacing them with nodes running a new version of the software.
|
||||
In this setup, part of your environment will have one version of a given
|
||||
module and the other part another version (the newer one) of the same module.
|
||||
|
||||
The best solution is to simply use the explicit module, function and arguments
|
||||
APIs when working with distributed agents.
|
||||
|
||||
## Hot code swapping
|
||||
|
||||
An agent can have its code hot swapped live by simply passing a module,
|
||||
function and args tuple to the update instruction. For example, imagine
|
||||
you have an agent named `:sample` and you want to convert its inner state
|
||||
from some dict structure to a map. It can be done with the following
|
||||
instruction:
|
||||
|
||||
{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}
|
||||
|
||||
The agent's state will be added to the given list as the first argument.
|
||||
"""
|
||||
|
||||
@typedoc "Return values of `start*` functions"
|
||||
@type on_start :: {:ok, pid} | {:error, {:already_started, pid} | term}
|
||||
|
||||
@typedoc "The agent name"
|
||||
@type name :: atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@typedoc "The agent reference"
|
||||
@type agent :: pid | {atom, node} | name
|
||||
|
||||
@typedoc "The agent state"
|
||||
@type state :: term
|
||||
|
||||
@doc """
|
||||
Starts an agent linked to the current process with the given function.
|
||||
|
||||
This is often used to start the agent as part of a supervision tree.
|
||||
|
||||
Once the agent is spawned, the given function is invoked and its return
|
||||
value is used as the agent state. Note that `start_link` does not return
|
||||
until the given function has returned.
|
||||
|
||||
## Options
|
||||
|
||||
The `:name` option is used for registration as described in the module
|
||||
documentation.
|
||||
|
||||
If the `:timeout` option is present, the agent is allowed to spend at most
|
||||
the given number of milliseconds on initialization or it will be terminated
|
||||
and the start function will return `{:error, :timeout}`.
|
||||
|
||||
If the `:debug` option is present, the corresponding function in the
|
||||
[`:sys` module](http://www.erlang.org/doc/man/sys.html) will be invoked.
|
||||
|
||||
If the `:spawn_opt` option is present, its value will be passed as options
|
||||
to the underlying process as in `Process.spawn/4`.
|
||||
|
||||
## Return values
|
||||
|
||||
If the server is successfully created and initialized, the function returns
|
||||
`{:ok, pid}`, where `pid` is the pid of the server. If there already exists
|
||||
an agent with the specified name, the function returns
|
||||
`{:error, {:already_started, pid}}` with the pid of that process.
|
||||
|
||||
If the given function callback fails with `reason`, the function returns
|
||||
`{:error, reason}`.
|
||||
"""
|
||||
@spec start_link((() -> term), GenServer.options) :: on_start
|
||||
def start_link(fun, options \\ []) when is_function(fun, 0) do
|
||||
GenServer.start_link(Agent.Server, fun, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts an agent linked to the current process with the given module
|
||||
function and arguments.
|
||||
|
||||
Same as `start_link/2` but a module, function and args are expected
|
||||
instead of an anonymous function.
|
||||
"""
|
||||
@spec start_link(module, atom, [any], GenServer.options) :: on_start
|
||||
def start_link(module, fun, args, options \\ []) do
|
||||
GenServer.start_link(Agent.Server, {module, fun, args}, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts an agent process without links (outside of a supervision tree).
|
||||
|
||||
See `start_link/2` for more information.
|
||||
"""
|
||||
@spec start((() -> term), GenServer.options) :: on_start
|
||||
def start(fun, options \\ []) when is_function(fun, 0) do
|
||||
GenServer.start(Agent.Server, fun, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts an agent with the given module function and arguments.
|
||||
|
||||
Similar to `start/2` but a module, function and args are expected
|
||||
instead of an anonymous function.
|
||||
"""
|
||||
@spec start(module, atom, [any], GenServer.options) :: on_start
|
||||
def start(module, fun, args, options \\ []) do
|
||||
GenServer.start(Agent.Server, {module, fun, args}, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an agent value via the given function.
|
||||
|
||||
The function `fun` is sent to the `agent` which invokes the function
|
||||
passing the agent state. The result of the function invocation is
|
||||
returned.
|
||||
|
||||
A timeout can also be specified (it has a default value of 5000).
|
||||
"""
|
||||
@spec get(agent, (state -> a), timeout) :: a when a: var
|
||||
def get(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
|
||||
GenServer.call(agent, {:get, fun}, timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an agent value via the given function.
|
||||
|
||||
Same as `get/3` but a module, function and args are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of args.
|
||||
"""
|
||||
@spec get(agent, module, atom, [term], timeout) :: any
|
||||
def get(agent, module, fun, args, timeout \\ 5000) do
|
||||
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets and updates the agent state in one operation.
|
||||
|
||||
The function `fun` is sent to the `agent` which invokes the function
|
||||
passing the agent state. The function must return a tuple with two
|
||||
elements, the first being the value to return (i.e. the `get` value)
|
||||
and the second one is the new state.
|
||||
|
||||
A timeout can also be specified (it has a default value of 5000).
|
||||
"""
|
||||
@spec get_and_update(agent, (state -> {a, state}), timeout) :: a when a: var
|
||||
def get_and_update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
|
||||
GenServer.call(agent, {:get_and_update, fun}, timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets and updates the agent state in one operation.
|
||||
|
||||
Same as `get_and_update/3` but a module, function and args are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of args.
|
||||
"""
|
||||
@spec get_and_update(agent, module, atom, [term], timeout) :: any
|
||||
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
|
||||
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the agent state.
|
||||
|
||||
The function `fun` is sent to the `agent` which invokes the function
|
||||
passing the agent state. The function must return the new state.
|
||||
|
||||
A timeout can also be specified (it has a default value of 5000).
|
||||
This function always returns `:ok`.
|
||||
"""
|
||||
@spec update(agent, (state -> state), timeout) :: :ok
|
||||
def update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
|
||||
GenServer.call(agent, {:update, fun}, timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the agent state.
|
||||
|
||||
Same as `update/3` but a module, function and args are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of args.
|
||||
"""
|
||||
@spec update(agent, module, atom, [term], timeout) :: :ok
|
||||
def update(agent, module, fun, args, timeout \\ 5000) do
|
||||
GenServer.call(agent, {:update, {module, fun, args}}, timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a cast (fire and forget) operation on the agent state.
|
||||
|
||||
The function `fun` is sent to the `agent` which invokes the function
|
||||
passing the agent state. The function must return the new state.
|
||||
|
||||
Note that `cast` returns `:ok` immediately, regardless of whether the
|
||||
destination node or agent exists.
|
||||
"""
|
||||
@spec cast(agent, (state -> state)) :: :ok
|
||||
def cast(agent, fun) when is_function(fun, 1) do
|
||||
GenServer.cast(agent, {:cast, fun})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a cast (fire and forget) operation on the agent state.
|
||||
|
||||
Same as `cast/2` but a module, function and args are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of args.
|
||||
"""
|
||||
@spec cast(agent, module, atom, [term]) :: :ok
|
||||
def cast(agent, module, fun, args) do
|
||||
GenServer.cast(agent, {:cast, {module, fun, args}})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the agent.
|
||||
|
||||
Returns `:ok` if the agent is stopped within the given `timeout`.
|
||||
"""
|
||||
@spec stop(agent, timeout) :: :ok
|
||||
def stop(agent, timeout \\ 5000) do
|
||||
GenServer.call(agent, :stop, timeout)
|
||||
end
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
defmodule Agent.Server do
|
||||
@moduledoc false
|
||||
|
||||
use GenServer
|
||||
|
||||
def init(fun) do
|
||||
{:ok, run(fun, [])}
|
||||
end
|
||||
|
||||
def handle_call({:get, fun}, _from, state) do
|
||||
{:reply, run(fun, [state]), state}
|
||||
end
|
||||
|
||||
def handle_call({:get_and_update, fun}, _from, state) do
|
||||
{reply, state} = run(fun, [state])
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
def handle_call({:update, fun}, _from, state) do
|
||||
{:reply, :ok, run(fun, [state])}
|
||||
end
|
||||
|
||||
def handle_call(:stop, _from, state) do
|
||||
{:stop, :normal, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call(msg, from, state) do
|
||||
super(msg, from, state)
|
||||
end
|
||||
|
||||
def handle_cast({:cast, fun}, state) do
|
||||
{:noreply, run(fun, [state])}
|
||||
end
|
||||
|
||||
def handle_cast(msg, state) do
|
||||
super(msg, state)
|
||||
end
|
||||
|
||||
def code_change(_old, state, fun) do
|
||||
{:ok, run(fun, [state])}
|
||||
end
|
||||
|
||||
def terminate(_reason, _state) do
|
||||
# There is a race condition if the agent is
|
||||
# restarted too fast and it is registered.
|
||||
try do
|
||||
self |> :erlang.process_info(:registered_name) |> elem(1) |> Process.unregister
|
||||
rescue
|
||||
_ -> :ok
|
||||
end
|
||||
:ok
|
||||
end
|
||||
|
||||
defp run({m, f, a}, extra), do: apply(m, f, extra ++ a)
|
||||
defp run(fun, extra), do: apply(fun, extra)
|
||||
end
|
||||
@@ -1,405 +0,0 @@
|
||||
defmodule Application do
|
||||
@moduledoc """
|
||||
A module for working with applications and defining application callbacks.
|
||||
|
||||
In Elixir (actually, in Erlang/OTP), an application is a component
|
||||
implementing some specific functionality, that can be started and stopped
|
||||
as a unit, and which can be re-used in other systems as well.
|
||||
|
||||
Applications are defined with an application file named `APP.app` where
|
||||
`APP` is the APP name, usually in `underscore_case` convention. The
|
||||
application file must reside in the same `ebin` directory as the
|
||||
application's modules bytecode.
|
||||
|
||||
In Elixir, Mix is responsible for compiling your source code and
|
||||
generating your application `.app` file. Furthermore, Mix is also
|
||||
responsible for configuring, starting and stopping your application
|
||||
and its dependencies. For this reason, this documentation will focus
|
||||
on the remaining aspects of your application: the application environment,
|
||||
and the application callback module.
|
||||
|
||||
You can learn more about Mix compilation of `.app` files by typing
|
||||
`mix help compile.app`.
|
||||
|
||||
## Application environment
|
||||
|
||||
Once an application is started, OTP provides an application environment
|
||||
that can be used to configure applications.
|
||||
|
||||
Assuming you are inside a Mix project, you can edit your application
|
||||
function in the `mix.exs` file to the following:
|
||||
|
||||
def application do
|
||||
[env: [hello: :world]]
|
||||
end
|
||||
|
||||
In the application function, we can define the default environment values
|
||||
for our application. By starting your application with `iex -S mix`, you
|
||||
can access the default value:
|
||||
|
||||
Application.get_env(:APP_NAME, :hello)
|
||||
#=> {:ok, :hello}
|
||||
|
||||
It is also possible to put and delete values from the application value,
|
||||
including new values that are not defined in the environment file (although
|
||||
those should be avoided).
|
||||
|
||||
In the future, we plan to support configuration files which allows
|
||||
developers to configure the environment of their dependencies.
|
||||
|
||||
Keep in mind that each application is responsible for its environment.
|
||||
Do not use the functions in this module for directly access or modify
|
||||
the environment of other application (as it may lead to inconsistent
|
||||
data in the application environment).
|
||||
|
||||
## Application module callback
|
||||
|
||||
Often times, an application defines a supervision tree that must be started
|
||||
and stopped when the application starts and stops. For such, we need to
|
||||
define an application module callback. The first step is to define the
|
||||
module callback in the application definition in the `mix.exs` file:
|
||||
|
||||
def application do
|
||||
[mod: {MyApp, []}]
|
||||
end
|
||||
|
||||
Our application now requires the `MyApp` module to provide an application
|
||||
callback. This can be done by invoking `use Application` in that module
|
||||
and defining a `start/2` callback, for example:
|
||||
|
||||
defmodule MyApp do
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
MyApp.Supervisor.start_link()
|
||||
end
|
||||
end
|
||||
|
||||
`start/2` most commonly returns `{:ok, pid}` or `{:ok, pid, state}` where
|
||||
`pid` identifies the supervision tree and the state is the application state.
|
||||
`args` is second element of the tuple given to the `:mod` option.
|
||||
|
||||
The `type` passed into `start/2` is usually `:normal` unless in a distributed
|
||||
setup where applications takeover and failovers are configured. This particular
|
||||
aspect of applications can be read with more detail in the OTP documentation:
|
||||
|
||||
* http://www.erlang.org/doc/man/application.html
|
||||
* http://www.erlang.org/doc/design_principles/applications.html
|
||||
|
||||
A developer may also implement the `stop/1` callback (automatically defined
|
||||
by `use Application`) which does any application cleanup. It receives the
|
||||
application state and can return any value. Notice that shutting down the
|
||||
supervisor is automatically handled by the VM;
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
@behaviour :application
|
||||
|
||||
@doc false
|
||||
def stop(_state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defoverridable [stop: 1]
|
||||
end
|
||||
end
|
||||
|
||||
@type app :: atom
|
||||
@type key :: atom
|
||||
@type value :: term
|
||||
@type start_type :: :permanent | :transient | :temporary
|
||||
|
||||
@doc """
|
||||
Returns all key-value pairs for `app`.
|
||||
"""
|
||||
@spec get_all_env(app) :: [{key,value}]
|
||||
def get_all_env(app) do
|
||||
:application.get_all_env(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value for `key` in `app`'s environment.
|
||||
|
||||
If the specified application is not loaded, or the configuration parameter
|
||||
does not exist, the function returns the `default` value.
|
||||
"""
|
||||
@spec get_env(app, key, value) :: value
|
||||
def get_env(app, key, default \\ nil) do
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, value} -> value
|
||||
:undefined -> default
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value for `key` in `app`'s environment in a tuple.
|
||||
|
||||
If the specified application is not loaded, or the configuration parameter
|
||||
does not exist, the function returns `:error`.
|
||||
"""
|
||||
@spec fetch_env(app, key) :: {:ok, value} | :error
|
||||
def fetch_env(app, key) do
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, value} -> {:ok, value}
|
||||
:undefined -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the `value` in `key` for the given `app`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:timeout` - the timeout for the change (defaults to 5000ms)
|
||||
* `:persistent` - persists the given value on application load and reloads
|
||||
|
||||
If `put_env/4` is called before the application is loaded, the application
|
||||
environment values specified in the `.app` file will override the ones
|
||||
previously set.
|
||||
|
||||
The persistent option can be set to true when there is a need to guarantee
|
||||
parameters set with this function will not be overridden by the ones defined
|
||||
in the application resource file on load. This means persistent values will
|
||||
stick after the application is loaded and also on application reload.
|
||||
"""
|
||||
@spec put_env(app, key, value, [timeout: timeout, persistent: boolean]) :: :ok
|
||||
def put_env(app, key, value, opts \\ []) do
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the `key` from the given `app` environment.
|
||||
|
||||
See `put_env/4` for a description of the options.
|
||||
"""
|
||||
@spec delete_env(app, key, [timeout: timeout, persistent: boolean]) :: :ok
|
||||
def delete_env(app, key, opts \\ []) do
|
||||
:application.unset_env(app, key, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given `app` is started.
|
||||
|
||||
Same as `start/2` but returns `:ok` if the application was already
|
||||
started. This is useful in scripts and in test setup, where test
|
||||
applications need to be explicitly started:
|
||||
|
||||
:ok = Application.ensure_started(:my_test_dep)
|
||||
|
||||
"""
|
||||
@spec ensure_started(app, start_type) :: :ok | {:error, term}
|
||||
def ensure_started(app, type \\ :temporary) when is_atom(app) do
|
||||
:application.ensure_started(app, type)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given `app` and its applications are started.
|
||||
|
||||
Same as `start/2` but also starts the applications listed under
|
||||
`:applications` in the `.app` file in case they were not previously
|
||||
started.
|
||||
"""
|
||||
@spec ensure_all_started(app, start_type) :: {:ok, [app]} | {:error, {app, term}}
|
||||
def ensure_all_started(app, type \\ :temporary) when is_atom(app) do
|
||||
:application.ensure_all_started(app, type)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts the given `app`.
|
||||
|
||||
If the `app` is not loaded, the application will first be loaded using `load/1`.
|
||||
Any included application, defined in the `:included_applications` key of the
|
||||
`.app` file will also be loaded, but they won't be started.
|
||||
|
||||
Furthermore, all applications listed in the `:applications` key must be explicitly
|
||||
started before this application is. If not, `{:error, {:not_started, app}}` is
|
||||
returned, where `app` is the name of the missing application.
|
||||
|
||||
In case you want to automatically load **and start** all of `app`'s dependencies,
|
||||
see `ensure_all_started/2`.
|
||||
|
||||
The `type` argument specifies the type of the application:
|
||||
|
||||
* `:permanent` - if `app` terminates, all other applications and the entire
|
||||
node are also terminated.
|
||||
|
||||
* `:transient` - if `app` terminates with `:normal` reason, it is reported
|
||||
but no other applications are terminated. If a transient application
|
||||
terminates abnormally, all other applications and the entire node are
|
||||
also terminated.
|
||||
|
||||
* `:temporary` - if `app` terminates, it is reported but no other
|
||||
applications are terminated (the default).
|
||||
|
||||
Note that it is always possible to stop an application explicitly by calling
|
||||
`stop/1`. Regardless of the type of the application, no other applications will
|
||||
be affected.
|
||||
|
||||
Note also that the `:transient` type is of little practical use, since when a
|
||||
supervision tree terminates, the reason is set to `:shutdown`, not `:normal`.
|
||||
"""
|
||||
@spec start(app, start_type) :: :ok | {:error, term}
|
||||
def start(app, type \\ :temporary) when is_atom(app) do
|
||||
:application.start(app, type)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the given `app`.
|
||||
|
||||
When stopped, the application is still loaded.
|
||||
"""
|
||||
@spec stop(app) :: :ok | {:error, term}
|
||||
def stop(app) do
|
||||
:application.stop(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads the given `app`.
|
||||
|
||||
In order to be loaded, an `.app` file must be in the load paths.
|
||||
All `:included_applications` will also be loaded.
|
||||
|
||||
Loading the application does not start it nor load its modules, but
|
||||
it does load its environment.
|
||||
"""
|
||||
@spec load(app) :: :ok | {:error, term}
|
||||
def load(app) when is_atom(app) do
|
||||
:application.load(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unloads the given `app`.
|
||||
|
||||
It will also unload all `:included_applications`.
|
||||
Note that the function does not purge the application modules.
|
||||
"""
|
||||
@spec unload(app) :: :ok | {:error, term}
|
||||
def unload(app) when is_atom(app) do
|
||||
:application.unload(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the directory for app.
|
||||
|
||||
This information is returned based on the code path. Here is an
|
||||
example:
|
||||
|
||||
File.mkdir_p!("foo/ebin")
|
||||
Code.prepend_path("foo/ebin")
|
||||
Application.app_dir(:foo)
|
||||
#=> "foo"
|
||||
|
||||
Even though the directory is empty and there is no `.app` file
|
||||
it is considered the application directory based on the name
|
||||
"foo/ebin". The name may contain a dash `-` which is considered
|
||||
to be the app version and it is removed for the lookup purposes:
|
||||
|
||||
File.mkdir_p!("bar-123/ebin")
|
||||
Code.prepend_path("bar-123/ebin")
|
||||
Application.app_dir(:bar)
|
||||
#=> "bar-123"
|
||||
|
||||
For more information on code paths, check the `Code` module in
|
||||
Elixir and also Erlang's `:code` module.
|
||||
"""
|
||||
@spec app_dir(app) :: String.t
|
||||
def app_dir(app) when is_atom(app) do
|
||||
case :code.lib_dir(app) do
|
||||
lib when is_list(lib) -> IO.chardata_to_string(lib)
|
||||
{:error, :bad_name} -> raise ArgumentError, "unknown application: #{inspect app}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the given path inside `app_dir/1`.
|
||||
"""
|
||||
@spec app_dir(app, String.t) :: String.t
|
||||
def app_dir(app, path) when is_binary(path) do
|
||||
Path.join(app_dir(app), path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the error reason returned by `start/2`,
|
||||
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
|
||||
returns a string.
|
||||
"""
|
||||
@spec format_error(any) :: String.t
|
||||
def format_error(reason) do
|
||||
try do
|
||||
impl_format_error(reason)
|
||||
catch
|
||||
# A user could create an error that looks like a builtin one
|
||||
# causing an error.
|
||||
:error, _ ->
|
||||
inspect(reason)
|
||||
end
|
||||
end
|
||||
|
||||
# exit(:normal) call is special cased, undo the special case.
|
||||
defp impl_format_error({{:EXIT, :normal}, {mod, :start, args}}) do
|
||||
Exception.format_exit({:normal, {mod, :start, args}})
|
||||
end
|
||||
|
||||
# {:error, reason} return value
|
||||
defp impl_format_error({reason, {mod, :start, args}}) do
|
||||
Exception.format_mfa(mod, :start, args) <> " returned an error: " <>
|
||||
Exception.format_exit(reason)
|
||||
end
|
||||
|
||||
# error or exit(reason) call, use exit reason as reason.
|
||||
defp impl_format_error({:bad_return, {{mod, :start, args}, {:EXIT, reason}}}) do
|
||||
Exception.format_exit({reason, {mod, :start, args}})
|
||||
end
|
||||
|
||||
# bad return value
|
||||
defp impl_format_error({:bad_return, {{mod, :start, args}, return}}) do
|
||||
Exception.format_mfa(mod, :start, args) <>
|
||||
" returned a bad value: " <> inspect(return)
|
||||
end
|
||||
|
||||
defp impl_format_error({:already_started, app}) when is_atom(app) do
|
||||
"already started application #{app}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:not_started, app}) when is_atom(app) do
|
||||
"not started application #{app}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:bad_application, app}) do
|
||||
"bad application: #{inspect(app)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:already_loaded, app}) when is_atom(app) do
|
||||
"already loaded application #{app}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:not_loaded, app}) when is_atom(app) do
|
||||
"not loaded application #{app}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:invalid_restart_type, restart}) do
|
||||
"invalid application restart type: #{inspect(restart)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:invalid_name, name}) do
|
||||
"invalid application name: #{inspect(name)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:invalid_options, opts}) do
|
||||
"invalid application options: #{inspect(opts)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({:badstartspec, spec}) do
|
||||
"bad application start specs: #{inspect(spec)}"
|
||||
end
|
||||
|
||||
defp impl_format_error({'no such file or directory', file}) do
|
||||
"could not find application file: #{file}"
|
||||
end
|
||||
|
||||
defp impl_format_error(reason) do
|
||||
Exception.format_exit(reason)
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
defmodule Atom do
|
||||
@doc """
|
||||
Convenience functions for working with atoms.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Converts an atom to string.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec to_string(atom) :: String.t
|
||||
def to_string(atom) do
|
||||
:erlang.atom_to_binary(atom, :utf8)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an atom to a char list.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec to_char_list(atom) :: char_list
|
||||
def to_char_list(atom) do
|
||||
:erlang.atom_to_list(atom)
|
||||
end
|
||||
end
|
||||
@@ -1,592 +0,0 @@
|
||||
defmodule Base do
|
||||
import Bitwise
|
||||
|
||||
@moduledoc """
|
||||
This module provides data encoding and decoding functions
|
||||
according to [RFC 4648](http://tools.ietf.org/html/rfc4648).
|
||||
|
||||
This document defines the commonly used base 16, base 32, and base
|
||||
64 encoding schemes.
|
||||
|
||||
## Base 16 alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| 0| 4| 4| 8| 8| 12| C|
|
||||
| 1| 1| 5| 5| 9| 9| 13| D|
|
||||
| 2| 2| 6| 6| 10| A| 14| E|
|
||||
| 3| 3| 7| 7| 11| B| 15| F|
|
||||
|
||||
## Base 32 alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 9| J| 18| S| 27| 3|
|
||||
| 1| B| 10| K| 19| T| 28| 4|
|
||||
| 2| C| 11| L| 20| U| 29| 5|
|
||||
| 3| D| 12| M| 21| V| 30| 6|
|
||||
| 4| E| 13| N| 22| W| 31| 7|
|
||||
| 5| F| 14| O| 23| X| | |
|
||||
| 6| G| 15| P| 24| Y| (pad)| =|
|
||||
| 7| H| 16| Q| 25| Z| | |
|
||||
| 8| I| 17| R| 26| 2| | |
|
||||
|
||||
|
||||
## Base 32 (extended hex) alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| 0| 9| 9| 18| I| 27| R|
|
||||
| 1| 1| 10| A| 19| J| 28| S|
|
||||
| 2| 2| 11| B| 20| K| 29| T|
|
||||
| 3| 3| 12| C| 21| L| 30| U|
|
||||
| 4| 4| 13| D| 22| M| 31| V|
|
||||
| 5| 5| 14| E| 23| N| | |
|
||||
| 6| 6| 15| F| 24| O| (pad)| =|
|
||||
| 7| 7| 16| G| 25| P| | |
|
||||
| 8| 8| 17| H| 26| Q| | |
|
||||
|
||||
## Base 64 alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 17| R| 34| i| 51| z|
|
||||
| 1| B| 18| S| 35| j| 52| 0|
|
||||
| 2| C| 19| T| 36| k| 53| 1|
|
||||
| 3| D| 20| U| 37| l| 54| 2|
|
||||
| 4| E| 21| V| 38| m| 55| 3|
|
||||
| 5| F| 22| W| 39| n| 56| 4|
|
||||
| 6| G| 23| X| 40| o| 57| 5|
|
||||
| 7| H| 24| Y| 41| p| 58| 6|
|
||||
| 8| I| 25| Z| 42| q| 59| 7|
|
||||
| 9| J| 26| a| 43| r| 60| 8|
|
||||
| 10| K| 27| b| 44| s| 61| 9|
|
||||
| 11| L| 28| c| 45| t| 62| +|
|
||||
| 12| M| 29| d| 46| u| 63| /|
|
||||
| 13| N| 30| e| 47| v| | |
|
||||
| 14| O| 31| f| 48| w| (pad)| =|
|
||||
| 15| P| 32| g| 49| x| | |
|
||||
| 16| Q| 33| h| 50| y| | |
|
||||
|
||||
## Base 64 (URL and filename safe) alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 17| R| 34| i| 51| z|
|
||||
| 1| B| 18| S| 35| j| 52| 0|
|
||||
| 2| C| 19| T| 36| k| 53| 1|
|
||||
| 3| D| 20| U| 37| l| 54| 2|
|
||||
| 4| E| 21| V| 38| m| 55| 3|
|
||||
| 5| F| 22| W| 39| n| 56| 4|
|
||||
| 6| G| 23| X| 40| o| 57| 5|
|
||||
| 7| H| 24| Y| 41| p| 58| 6|
|
||||
| 8| I| 25| Z| 42| q| 59| 7|
|
||||
| 9| J| 26| a| 43| r| 60| 8|
|
||||
| 10| K| 27| b| 44| s| 61| 9|
|
||||
| 11| L| 28| c| 45| t| 62| -|
|
||||
| 12| M| 29| d| 46| u| 63| _|
|
||||
| 13| N| 30| e| 47| v| | |
|
||||
| 14| O| 31| f| 48| w| (pad)| =|
|
||||
| 15| P| 32| g| 49| x| | |
|
||||
| 16| Q| 33| h| 50| y| | |
|
||||
|
||||
"""
|
||||
|
||||
b16_alphabet = Enum.with_index '0123456789ABCDEF'
|
||||
b64_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
b64url_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
||||
b32_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
||||
b32hex_alphabet = Enum.with_index '0123456789ABCDEFGHIJKLMNOPQRSTUV'
|
||||
|
||||
Enum.each [ {:enc16, :dec16, b16_alphabet},
|
||||
{:enc64, :dec64, b64_alphabet},
|
||||
{:enc32, :dec32, b32_alphabet},
|
||||
{:enc64url, :dec64url, b64url_alphabet},
|
||||
{:enc32hex, :dec32hex, b32hex_alphabet} ], fn({enc, dec, alphabet}) ->
|
||||
for {encoding, value} <- alphabet do
|
||||
defp unquote(enc)(unquote(value)), do: unquote(encoding)
|
||||
defp unquote(dec)(unquote(encoding)), do: unquote(value)
|
||||
end
|
||||
defp unquote(dec)(c) do
|
||||
raise ArgumentError, "non-alphabet digit found: #{<<c>>}"
|
||||
end
|
||||
end
|
||||
|
||||
defp encode_case(:upper, func),
|
||||
do: func
|
||||
defp encode_case(:lower, func),
|
||||
do: &to_lower(func.(&1))
|
||||
|
||||
defp decode_case(:upper, func),
|
||||
do: func
|
||||
defp decode_case(:lower, func),
|
||||
do: &func.(from_lower(&1))
|
||||
defp decode_case(:mixed, func),
|
||||
do: &func.(from_mixed(&1))
|
||||
|
||||
defp to_lower(char) when char in ?A..?Z,
|
||||
do: char + (?a - ?A)
|
||||
defp to_lower(char),
|
||||
do: char
|
||||
|
||||
defp from_lower(char) when char in ?a..?z,
|
||||
do: char - (?a - ?A)
|
||||
defp from_lower(char) when not char in ?A..?Z,
|
||||
do: char
|
||||
defp from_lower(char),
|
||||
do: raise(ArgumentError, "non-alphabet digit found: #{<<char>>}")
|
||||
|
||||
defp from_mixed(char) when char in ?a..?z,
|
||||
do: char - (?a - ?A)
|
||||
defp from_mixed(char),
|
||||
do: char
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 16 encoded string.
|
||||
|
||||
Accepts an atom `:upper` (default) for encoding to upper case characters or
|
||||
`:lower` for lower case characters.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.encode16("foobar")
|
||||
"666F6F626172"
|
||||
|
||||
iex> Base.encode16("foobar", case: :lower)
|
||||
"666f6f626172"
|
||||
|
||||
"""
|
||||
@spec encode16(binary) :: binary
|
||||
@spec encode16(binary, Keyword.t) :: binary
|
||||
def encode16(data, opts \\ []) when is_binary(data) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
do_encode16(data, encode_case(case, &enc16/1))
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
Decodes a base 16 encoded string into a binary string.
|
||||
|
||||
Accepts an atom `:upper` (default) for decoding from upper case characters or
|
||||
`:lower` for lower case characters. `:mixed` can be given for mixed case
|
||||
characters.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode16("666F6F626172")
|
||||
{:ok, "foobar"}
|
||||
|
||||
iex> Base.decode16("666f6f626172", case: :lower)
|
||||
{:ok, "foobar"}
|
||||
|
||||
iex> Base.decode16("666f6F626172", case: :mixed)
|
||||
{:ok, "foobar"}
|
||||
|
||||
"""
|
||||
@spec decode16(binary) :: {:ok, binary} | :error
|
||||
@spec decode16(binary, Keyword.t) :: {:ok, binary} | :error
|
||||
def decode16(string, opts \\ []) when is_binary(string) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
{:ok, do_decode16(string, decode_case(case, &dec16/1))}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 16 encoded string into a binary string.
|
||||
|
||||
Accepts an atom `:upper` (default) for decoding from upper case characters or
|
||||
`:lower` for lower case characters. `:mixed` can be given for mixed case
|
||||
characters.
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode16!("666F6F626172")
|
||||
"foobar"
|
||||
|
||||
iex> Base.decode16!("666f6f626172", case: :lower)
|
||||
"foobar"
|
||||
|
||||
iex> Base.decode16!("666f6F626172", case: :mixed)
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
@spec decode16!(binary) :: binary
|
||||
@spec decode16!(binary, Keyword.t) :: binary
|
||||
def decode16!(string, opts \\ []) when is_binary(string) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
do_decode16(string, decode_case(case, &dec16/1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 64 encoded string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.encode64("foobar")
|
||||
"Zm9vYmFy"
|
||||
|
||||
"""
|
||||
@spec encode64(binary) :: binary
|
||||
def encode64(data) when is_binary(data) do
|
||||
do_encode64(data, &enc64/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 64 encoded string into a binary string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode64("Zm9vYmFy")
|
||||
{:ok, "foobar"}
|
||||
|
||||
"""
|
||||
@spec decode64(binary) :: {:ok, binary} | :error
|
||||
def decode64(string) when is_binary(string) do
|
||||
{:ok, do_decode64(string, &dec64/1)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 64 encoded string into a binary string.
|
||||
|
||||
The following alphabet is used both for encoding and decoding:
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode64!("Zm9vYmFy")
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
@spec decode64!(binary) :: binary
|
||||
def decode64!(string) when is_binary(string) do
|
||||
do_decode64(string, &dec64/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 64 encoded string with URL and filename
|
||||
safe alphabet.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.url_encode64(<<255,127,254,252>>)
|
||||
"_3_-_A=="
|
||||
|
||||
"""
|
||||
@spec url_encode64(binary) :: binary
|
||||
def url_encode64(data) when is_binary(data) do
|
||||
do_encode64(data, &enc64url/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 64 encoded string with URL and filename safe alphabet
|
||||
into a binary string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.url_decode64("_3_-_A==")
|
||||
{:ok, <<255,127,254,252>>}
|
||||
|
||||
"""
|
||||
@spec url_decode64(binary) :: {:ok, binary} | :error
|
||||
def url_decode64(string) when is_binary(string) do
|
||||
{:ok, do_decode64(string, &dec64url/1)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 64 encoded string with URL and filename safe alphabet
|
||||
into a binary string.
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.url_decode64!("_3_-_A==")
|
||||
<<255,127,254,252>>
|
||||
|
||||
"""
|
||||
@spec url_decode64!(binary) :: binary
|
||||
def url_decode64!(string) when is_binary(string) do
|
||||
do_decode64(string, &dec64url/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 32 encoded string.
|
||||
|
||||
Accepts an atom `:upper` (default) for encoding to upper case characters or
|
||||
`:lower` for lower case characters.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.encode32("foobar")
|
||||
"MZXW6YTBOI======"
|
||||
|
||||
iex> Base.encode32("foobar", case: :lower)
|
||||
"mzxw6ytboi======"
|
||||
|
||||
"""
|
||||
@spec encode32(binary) :: binary
|
||||
@spec encode32(binary, Keyword.t) :: binary
|
||||
def encode32(data, opts \\ []) when is_binary(data) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
do_encode32(data, encode_case(case, &enc32/1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 32 encoded string into a binary string.
|
||||
|
||||
Accepts an atom `:upper` (default) for decoding from upper case characters or
|
||||
`:lower` for lower case characters. `:mixed` can be given for mixed case
|
||||
characters.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode32("MZXW6YTBOI======")
|
||||
{:ok, "foobar"}
|
||||
|
||||
iex> Base.decode32("mzxw6ytboi======", case: :lower)
|
||||
{:ok, "foobar"}
|
||||
|
||||
iex> Base.decode32("mzXW6ytBOi======", case: :mixed)
|
||||
{:ok, "foobar"}
|
||||
|
||||
"""
|
||||
@spec decode32(binary) :: {:ok, binary} | :error
|
||||
@spec decode32(binary, Keyword.t) :: {:ok, binary} | :error
|
||||
def decode32(string, opts \\ []) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
{:ok, do_decode32(string, decode_case(case, &dec32/1))}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 32 encoded string into a binary string.
|
||||
|
||||
Accepts an atom `:upper` (default) for decoding from upper case characters or
|
||||
`:lower` for lower case characters. `:mixed` can be given for mixed case
|
||||
characters.
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.decode32!("MZXW6YTBOI======")
|
||||
"foobar"
|
||||
|
||||
iex> Base.decode32!("mzxw6ytboi======", case: :lower)
|
||||
"foobar"
|
||||
|
||||
iex> Base.decode32!("mzXW6ytBOi======", case: :mixed)
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
@spec decode32!(binary) :: binary
|
||||
@spec decode32!(binary, Keyword.t) :: binary
|
||||
def decode32!(string, opts \\ []) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
do_decode32(string, decode_case(case, &dec32/1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 32 encoded string with an
|
||||
extended hexadecimal alphabet.
|
||||
|
||||
Accepts an atom `:upper` (default) for encoding to upper case characters or
|
||||
`:lower` for lower case characters.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.hex_encode32("foobar")
|
||||
"CPNMUOJ1E8======"
|
||||
|
||||
iex> Base.hex_encode32("foobar", case: :lower)
|
||||
"cpnmuoj1e8======"
|
||||
|
||||
"""
|
||||
@spec hex_encode32(binary) :: binary
|
||||
@spec hex_encode32(binary, Keyword.t) :: binary
|
||||
def hex_encode32(data, opts \\ []) when is_binary(data) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
do_encode32(data, encode_case(case, &enc32hex/1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 32 encoded string with extended hexadecimal alphabet
|
||||
into a binary string.
|
||||
|
||||
Accepts an atom `:upper` (default) for decoding from upper case characters or
|
||||
`:lower` for lower case characters. `:mixed` can be given for mixed case
|
||||
characters.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.hex_decode32("CPNMUOJ1E8======")
|
||||
{:ok, "foobar"}
|
||||
|
||||
iex> Base.hex_decode32("cpnmuoj1e8======", case: :lower)
|
||||
{:ok, "foobar"}
|
||||
|
||||
iex> Base.hex_decode32("cpnMuOJ1E8======", case: :mixed)
|
||||
{:ok, "foobar"}
|
||||
|
||||
"""
|
||||
@spec hex_decode32(binary) :: {:ok, binary} | :error
|
||||
@spec hex_decode32(binary, Keyword.t) :: {:ok, binary} | :error
|
||||
def hex_decode32(string, opts \\ []) when is_binary(string) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
{:ok, do_decode32(string, decode_case(case, &dec32hex/1))}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes a base 32 encoded string with extended hexadecimal alphabet
|
||||
into a binary string.
|
||||
|
||||
Accepts an atom `:upper` (default) for decoding from upper case characters or
|
||||
`:lower` for lower case characters. `:mixed` can be given for mixed case
|
||||
characters.
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Base.hex_decode32!("CPNMUOJ1E8======")
|
||||
"foobar"
|
||||
|
||||
iex> Base.hex_decode32!("cpnmuoj1e8======", case: :lower)
|
||||
"foobar"
|
||||
|
||||
iex> Base.hex_decode32!("cpnMuOJ1E8======", case: :mixed)
|
||||
"foobar"
|
||||
|
||||
"""
|
||||
@spec hex_decode32!(binary) :: binary
|
||||
@spec hex_decode32!(binary, Keyword.t) :: binary
|
||||
def hex_decode32!(string, opts \\ []) when is_binary(string) do
|
||||
case = Keyword.get(opts, :case, :upper)
|
||||
do_decode32(string, decode_case(case, &dec32hex/1))
|
||||
end
|
||||
|
||||
defp do_encode16(<<>>, _), do: <<>>
|
||||
defp do_encode16(data, enc) do
|
||||
for <<c::4 <- data>>, into: <<>>, do: <<enc.(c)::8>>
|
||||
end
|
||||
|
||||
defp do_decode16(<<>>, _), do: <<>>
|
||||
defp do_decode16(string, dec) when rem(byte_size(string), 2) == 0 do
|
||||
for <<c1::8, c2::8 <- string>>, into: <<>> do
|
||||
<<dec.(c1)::4, dec.(c2)::4>>
|
||||
end
|
||||
end
|
||||
defp do_decode16(_, _) do
|
||||
raise ArgumentError, "odd-length string"
|
||||
end
|
||||
|
||||
defp do_encode64(<<>>, _), do: <<>>
|
||||
defp do_encode64(data, enc) do
|
||||
split = 3 * div(byte_size(data), 3)
|
||||
<<main::size(split)-binary, rest::binary>> = data
|
||||
main = for <<c::6 <- main>>, into: <<>>, do: <<enc.(c)::8>>
|
||||
case rest do
|
||||
<<c1::6, c2::6, c3::4>> ->
|
||||
<<main::binary, enc.(c1)::8, enc.(c2)::8, enc.(bsl(c3, 2))::8, ?=>>
|
||||
<<c1::6, c2::2>> ->
|
||||
<<main::binary, enc.(c1)::8, enc.(bsl(c2, 4))::8, ?=, ?=>>
|
||||
<<>> ->
|
||||
main
|
||||
end
|
||||
end
|
||||
|
||||
defp do_decode64(<<>>, _), do: <<>>
|
||||
defp do_decode64(string, dec) when rem(byte_size(string), 4) == 0 do
|
||||
split = byte_size(string) - 4
|
||||
<<main::size(split)-binary, rest::binary>> = string
|
||||
main = for <<c::8 <- main>>, into: <<>>, do: <<dec.(c)::6>>
|
||||
case rest do
|
||||
<<c1::8, c2::8, ?=, ?=>> ->
|
||||
<<main::binary, dec.(c1)::6, bsr(dec.(c2), 4)::2>>
|
||||
<<c1::8, c2::8, c3::8, ?=>> ->
|
||||
<<main::binary, dec.(c1)::6, dec.(c2)::6, bsr(dec.(c3), 2)::4>>
|
||||
<<c1::8, c2::8, c3::8, c4::8>> ->
|
||||
<<main::binary, dec.(c1)::6, dec.(c2)::6, dec.(c3)::6, dec.(c4)::6>>
|
||||
<<>> ->
|
||||
main
|
||||
end
|
||||
end
|
||||
defp do_decode64(_, _) do
|
||||
raise ArgumentError, "incorrect padding"
|
||||
end
|
||||
|
||||
defp do_encode32(<<>>, _), do: <<>>
|
||||
defp do_encode32(data, enc) do
|
||||
split = 5 * div(byte_size(data), 5)
|
||||
<<main::size(split)-binary, rest::binary>> = data
|
||||
main = for <<c::5 <- main>>, into: <<>>, do: <<enc.(c)::8>>
|
||||
case rest do
|
||||
<<c1::5, c2::5, c3::5, c4::5, c5::5, c6::5, c7::2>> ->
|
||||
<<main::binary,
|
||||
enc.(c1)::8, enc.(c2)::8, enc.(c3)::8, enc.(c4)::8,
|
||||
enc.(c5)::8, enc.(c6)::8, enc.(bsl(c7, 3))::8, ?=>>
|
||||
<<c1::5, c2::5, c3::5, c4::5, c5::4>> ->
|
||||
<<main::binary,
|
||||
enc.(c1)::8, enc.(c2)::8, enc.(c3)::8, enc.(c4)::8,
|
||||
enc.(bsl(c5, 1))::8, ?=, ?=, ?=>>
|
||||
<<c1::5, c2::5, c3::5, c4::1>> ->
|
||||
<<main::binary,
|
||||
enc.(c1)::8, enc.(c2)::8, enc.(c3)::8, enc.(bsl(c4, 4))::8,
|
||||
?=, ?=, ?=, ?=>>
|
||||
<<c1::5, c2::3>> ->
|
||||
<<main::binary,
|
||||
enc.(c1)::8, enc.(bsl(c2, 2))::8, ?=, ?=,
|
||||
?=, ?=, ?=, ?=>>
|
||||
<<>> ->
|
||||
main
|
||||
end
|
||||
end
|
||||
|
||||
defp do_decode32(<<>>, _), do: <<>>
|
||||
defp do_decode32(string, dec) when rem(byte_size(string), 8) == 0 do
|
||||
split = byte_size(string) - 8
|
||||
<<main::size(split)-binary, rest::binary>> = string
|
||||
main = for <<c::8 <- main>>, into: <<>>, do: <<dec.(c)::5>>
|
||||
case rest do
|
||||
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
|
||||
<<main::binary, dec.(c1)::5, bsr(dec.(c2), 2)::3>>
|
||||
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
|
||||
<<main::binary,
|
||||
dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, bsr(dec.(c4), 4)::1>>
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
|
||||
<<main::binary,
|
||||
dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
|
||||
bsr(dec.(c5), 1)::4>>
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
|
||||
<<main::binary,
|
||||
dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
|
||||
dec.(c5)::5, dec.(c6)::5, bsr(dec.(c7), 3)::2>>
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
|
||||
<<main::binary,
|
||||
dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
|
||||
dec.(c5)::5, dec.(c6)::5, dec.(c7)::5, dec.(c8)::5>>
|
||||
<<>> ->
|
||||
main
|
||||
end
|
||||
end
|
||||
defp do_decode32(_, _) do
|
||||
raise ArgumentError, "incorrect padding"
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,152 +0,0 @@
|
||||
defmodule Behaviour do
|
||||
@moduledoc """
|
||||
Utilities for defining behaviour interfaces.
|
||||
|
||||
Behaviours can be referenced by other modules
|
||||
to ensure they implement required callbacks.
|
||||
|
||||
For example, you can specify the `URI.Parser`
|
||||
behaviour as follows:
|
||||
|
||||
defmodule URI.Parser do
|
||||
use Behaviour
|
||||
|
||||
@doc "Parses the given URL"
|
||||
defcallback parse(uri_info :: URI.t) :: URI.t
|
||||
|
||||
@doc "Defines a default port"
|
||||
defcallback default_port() :: integer
|
||||
end
|
||||
|
||||
And then a module may use it as:
|
||||
|
||||
defmodule URI.HTTP do
|
||||
@behaviour URI.Parser
|
||||
def default_port(), do: 80
|
||||
def parse(info), do: info
|
||||
end
|
||||
|
||||
If the behaviour changes or `URI.HTTP` does
|
||||
not implement one of the callbacks, a warning
|
||||
will be raised.
|
||||
|
||||
## Implementation
|
||||
|
||||
Since Erlang R15, behaviours must be defined via
|
||||
`@callback` attributes. `defcallback` is a simple
|
||||
mechanism that defines the `@callback` attribute
|
||||
according to the given type specification. `defcallback` allows
|
||||
documentation to be created for the callback and defines
|
||||
a custom function signature.
|
||||
|
||||
The callbacks and their documentation can be retrieved
|
||||
via the `__behaviour__` callback function.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Define a function callback according to the given type specification.
|
||||
"""
|
||||
defmacro defcallback(spec) do
|
||||
do_defcallback(split_spec(spec, quote(do: term)), __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Define a macro callback according to the given type specification.
|
||||
"""
|
||||
defmacro defmacrocallback(spec) do
|
||||
do_defmacrocallback(split_spec(spec, quote(do: Macro.t)), __CALLER__)
|
||||
end
|
||||
|
||||
defp split_spec({:when, _, [{:::, _, [spec, return]}, guard]}, _default) do
|
||||
{spec, return, guard}
|
||||
end
|
||||
|
||||
defp split_spec({:when, _, [spec, guard]}, default) do
|
||||
{spec, default, guard}
|
||||
end
|
||||
|
||||
defp split_spec({:::, _, [spec, return]}, _default) do
|
||||
{spec, return, []}
|
||||
end
|
||||
|
||||
defp split_spec(spec, default) do
|
||||
{spec, default, []}
|
||||
end
|
||||
|
||||
defp do_defcallback({spec, return, guards}, caller) do
|
||||
case Macro.decompose_call(spec) do
|
||||
{name, args} ->
|
||||
do_callback(:def, name, args, name, length(args), args, return, guards, caller)
|
||||
_ ->
|
||||
raise ArgumentError, "invalid syntax in defcallback #{Macro.to_string(spec)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_defmacrocallback({spec, return, guards}, caller) do
|
||||
case Macro.decompose_call(spec) do
|
||||
{name, args} ->
|
||||
do_callback(:defmacro, :"MACRO-#{name}", [quote(do: env :: Macro.Env.t)|args],
|
||||
name, length(args), args, return, guards, caller)
|
||||
_ ->
|
||||
raise ArgumentError, "invalid syntax in defmacrocallback #{Macro.to_string(spec)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_callback(kind, name, args, docs_name, docs_arity, _docs_args, return, guards, caller) do
|
||||
Enum.each args, fn
|
||||
{:::, _, [left, right]} ->
|
||||
ensure_not_default(left)
|
||||
ensure_not_default(right)
|
||||
left
|
||||
other ->
|
||||
ensure_not_default(other)
|
||||
other
|
||||
end
|
||||
|
||||
quote do
|
||||
@callback unquote(name)(unquote_splicing(args)) :: unquote(return) when unquote(guards)
|
||||
Behaviour.store_docs(__MODULE__, unquote(caller.line), unquote(kind),
|
||||
unquote(docs_name), unquote(docs_arity))
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_not_default({:\\, _, [_, _]}) do
|
||||
raise ArgumentError, "default arguments \\\\ not supported in defcallback/defmacrocallback"
|
||||
end
|
||||
|
||||
defp ensure_not_default(_), do: :ok
|
||||
|
||||
@doc false
|
||||
def store_docs(module, line, kind, name, arity) do
|
||||
doc = Module.get_attribute module, :doc
|
||||
Module.delete_attribute module, :doc
|
||||
Module.put_attribute module, :behaviour_docs, {{name, arity}, line, kind, doc}
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
Module.register_attribute(__MODULE__, :behaviour_docs, accumulate: true)
|
||||
@before_compile unquote(__MODULE__)
|
||||
import unquote(__MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __before_compile__(env) do
|
||||
docs = if Code.compiler_options[:docs] do
|
||||
Enum.reverse Module.get_attribute(env.module, :behaviour_docs)
|
||||
end
|
||||
|
||||
quote do
|
||||
@doc false
|
||||
def __behaviour__(:callbacks) do
|
||||
__MODULE__.behaviour_info(:callbacks)
|
||||
end
|
||||
|
||||
def __behaviour__(:docs) do
|
||||
unquote(Macro.escape(docs))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,128 +0,0 @@
|
||||
defmodule Bitwise do
|
||||
@moduledoc """
|
||||
This module provides macros and operators for bitwise operators.
|
||||
These macros can be used in guards.
|
||||
|
||||
The easiest way to use is to simply import them into
|
||||
your module:
|
||||
|
||||
iex> use Bitwise
|
||||
iex> bnot 1
|
||||
-2
|
||||
iex> 1 &&& 1
|
||||
1
|
||||
|
||||
You can select to include only or skip operators by passing options:
|
||||
|
||||
iex> use Bitwise, only_operators: true
|
||||
iex> 1 &&& 1
|
||||
1
|
||||
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Allow a developer to use this module in their programs with
|
||||
the following options:
|
||||
|
||||
* `:only_operators` - include only operators
|
||||
* `:skip_operators` - skip operators
|
||||
|
||||
"""
|
||||
defmacro __using__(options) do
|
||||
except = cond do
|
||||
Keyword.get(options, :only_operators) ->
|
||||
[bnot: 1, band: 2, bor: 2, bxor: 2, bsl: 2, bsr: 2]
|
||||
Keyword.get(options, :skip_operators) ->
|
||||
[~~~: 1, &&&: 2, |||: 2, ^^^: 2, <<<: 2, >>>: 2]
|
||||
true -> []
|
||||
end
|
||||
|
||||
quote do
|
||||
import Bitwise, except: unquote(except)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise not.
|
||||
"""
|
||||
defmacro bnot(expr) do
|
||||
quote do: :erlang.bnot(unquote(expr))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise not as operator.
|
||||
"""
|
||||
defmacro ~~~expr do
|
||||
quote do: :erlang.bnot(unquote(expr))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise and.
|
||||
"""
|
||||
defmacro band(left, right) do
|
||||
quote do: :erlang.band(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise and as operator.
|
||||
"""
|
||||
defmacro left &&& right do
|
||||
quote do: :erlang.band(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise or.
|
||||
"""
|
||||
defmacro bor(left, right) do
|
||||
quote do: :erlang.bor(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise or as operator.
|
||||
"""
|
||||
defmacro left ||| right do
|
||||
quote do: :erlang.bor(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise xor.
|
||||
"""
|
||||
defmacro bxor(left, right) do
|
||||
quote do: :erlang.bxor(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise xor as operator.
|
||||
"""
|
||||
defmacro left ^^^ right do
|
||||
quote do: :erlang.bxor(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift left.
|
||||
"""
|
||||
defmacro bsl(left, right) do
|
||||
quote do: :erlang.bsl(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift left as operator.
|
||||
"""
|
||||
defmacro left <<< right do
|
||||
quote do: :erlang.bsl(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift right.
|
||||
"""
|
||||
defmacro bsr(left, right) do
|
||||
quote do: :erlang.bsr(unquote(left), unquote(right))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift right as operator.
|
||||
"""
|
||||
defmacro left >>> right do
|
||||
quote do: :erlang.bsr(unquote(left), unquote(right))
|
||||
end
|
||||
end
|
||||
@@ -1,558 +0,0 @@
|
||||
defmodule Code do
|
||||
@moduledoc """
|
||||
Utilities for managing code compilation, code evaluation and code loading.
|
||||
|
||||
This module complements [Erlang's code module](http://www.erlang.org/doc/man/code.html)
|
||||
to add behaviour which is specific to Elixir.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
List all loaded files.
|
||||
"""
|
||||
def loaded_files do
|
||||
:elixir_code_server.call :loaded
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove files from the loaded files list.
|
||||
|
||||
The modules defined in the file are not removed;
|
||||
calling this function only removes them from the list,
|
||||
allowing them to be required again.
|
||||
"""
|
||||
def unload_files(files) do
|
||||
:elixir_code_server.cast {:unload_files, files}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Append a path to the Erlang VM code path.
|
||||
|
||||
The path is expanded with `Path.expand/1` before being appended.
|
||||
"""
|
||||
def append_path(path) do
|
||||
:code.add_pathz(to_char_list(Path.expand path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prepend a path to the Erlang VM code path.
|
||||
|
||||
The path is expanded with `Path.expand/1` before being prepended.
|
||||
"""
|
||||
def prepend_path(path) do
|
||||
:code.add_patha(to_char_list(Path.expand path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a path from the Erlang VM code path.
|
||||
|
||||
The path is expanded with `Path.expand/1` before being deleted.
|
||||
"""
|
||||
def delete_path(path) do
|
||||
:code.del_path(to_char_list(Path.expand path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Re-add paths given to the command line to keep their position
|
||||
on the overall code path.
|
||||
|
||||
Some tools may change the code path by prepending new items but
|
||||
still want the paths given by the user to have higher priority.
|
||||
Calling this function guarantees the paths are re-added on
|
||||
top of the user given ones.
|
||||
"""
|
||||
@spec readd_paths() :: :ok
|
||||
def readd_paths() do
|
||||
{pa, pz} = :elixir_code_server.call(:paths)
|
||||
:code.add_pathsa(pa)
|
||||
:code.add_pathsz(pz)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluate the contents given by `string`.
|
||||
|
||||
The `binding` argument is a keyword list of variable bindings.
|
||||
The `opts` argument is a keyword list of environment options.
|
||||
|
||||
Those options can be:
|
||||
|
||||
* `:file` - the file to be considered in the evaluation
|
||||
* `:line` - the line on which the script starts
|
||||
* `:delegate_locals_to` - delegate local calls to the given module,
|
||||
the default is to not delegate
|
||||
|
||||
Additionally, the following scope values can be configured:
|
||||
|
||||
* `:aliases` - a list of tuples with the alias and its target
|
||||
|
||||
* `:requires` - a list of modules required
|
||||
|
||||
* `:functions` - a list of tuples where the first element is a module
|
||||
and the second a list of imported function names and arity; the list
|
||||
of function names and arity must be sorted
|
||||
|
||||
* `:macros` - a list of tuples where the first element is a module
|
||||
and the second a list of imported macro names and arity; the list
|
||||
of function names and arity must be sorted
|
||||
|
||||
Notice that setting any of the values above overrides Elixir's default
|
||||
values. For example, setting `:requires` to `[]`, will no longer
|
||||
automatically require the `Kernel` module; in the same way setting
|
||||
`:macros` will no longer auto-import `Kernel` macros like `if`, `case`,
|
||||
etc.
|
||||
|
||||
Returns a tuple of the form `{value, binding}`,
|
||||
where `value` is the value returned from evaluating `string`.
|
||||
If an error occurs while evaluating `string` an exception will be raised.
|
||||
|
||||
`binding` is a keyword list with the value of all variable bindings
|
||||
after evaluating `string`. The binding key is usually an atom, but it
|
||||
may be a tuple for variables defined in a different context.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Code.eval_string("a + b", [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
iex> Code.eval_string("c = a + b", [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2, c: 3]}
|
||||
|
||||
iex> Code.eval_string("a = a + b", [a: 1, b: 2])
|
||||
{3, [a: 3, b: 2]}
|
||||
|
||||
For convenience, you can pass `__ENV__` as the `opts` argument and
|
||||
all imports, requires and aliases defined in the current environment
|
||||
will be automatically carried over:
|
||||
|
||||
iex> Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
"""
|
||||
def eval_string(string, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_string(string, binding, %Macro.Env{} = env) do
|
||||
{value, binding, _env, _scope} = :elixir.eval to_char_list(string), binding, Map.to_list(env)
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
def eval_string(string, binding, opts) when is_list(opts) do
|
||||
validate_eval_opts(opts)
|
||||
{value, binding, _env, _scope} = :elixir.eval to_char_list(string), binding, opts
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluate the quoted contents.
|
||||
|
||||
See `eval_string/3` for a description of arguments and return values.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> contents = quote(do: var!(a) + var!(b))
|
||||
iex> Code.eval_quoted(contents, [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
For convenience, you can pass `__ENV__` as the `opts` argument and
|
||||
all options will be automatically extracted from the current environment:
|
||||
|
||||
iex> contents = quote(do: var!(a) + var!(b))
|
||||
iex> Code.eval_quoted(contents, [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
"""
|
||||
def eval_quoted(quoted, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
|
||||
{value, binding, _env, _scope} = :elixir.eval_quoted quoted, binding, Map.to_list(env)
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
def eval_quoted(quoted, binding, opts) when is_list(opts) do
|
||||
validate_eval_opts(opts)
|
||||
{value, binding, _env, _scope} = :elixir.eval_quoted quoted, binding, opts
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
defp validate_eval_opts(opts) do
|
||||
if f = opts[:functions], do: validate_imports(:functions, f)
|
||||
if m = opts[:macros], do: validate_imports(:macros, m)
|
||||
if a = opts[:aliases], do: validate_aliases(:aliases, a)
|
||||
if r = opts[:requires], do: validate_requires(:requires, r)
|
||||
end
|
||||
|
||||
defp validate_requires(kind, requires) do
|
||||
valid = is_list(requires) and Enum.all?(requires, &is_atom(&1))
|
||||
|
||||
unless valid do
|
||||
raise ArgumentError, "expected :#{kind} option given to eval in the format: [module]"
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_aliases(kind, aliases) do
|
||||
valid = is_list(aliases) and Enum.all?(aliases, fn {k, v} ->
|
||||
is_atom(k) and is_atom(v)
|
||||
end)
|
||||
|
||||
unless valid do
|
||||
raise ArgumentError, "expected :#{kind} option given to eval in the format: [{module, module}]"
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_imports(kind, imports) do
|
||||
valid = is_list(imports) and Enum.all?(imports, fn {k, v} ->
|
||||
is_atom(k) and is_list(v) and Enum.all?(v, fn {name, arity} ->
|
||||
is_atom(name) and is_integer(arity)
|
||||
end)
|
||||
end)
|
||||
|
||||
unless valid do
|
||||
raise ArgumentError, "expected :#{kind} option given to eval in the format: [{module, [{name, arity}]}]"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert the given string to its quoted form.
|
||||
|
||||
Returns `{:ok, quoted_form}`
|
||||
if it succeeds, `{:error, {line, error, token}}` otherwise.
|
||||
|
||||
## Options
|
||||
|
||||
* `:file` - the filename to be used in stacktraces
|
||||
and the file reported in the `__ENV__` variable
|
||||
|
||||
* `:line` - the line reported in the `__ENV__` variable
|
||||
|
||||
* `:existing_atoms_only` - when `true`, raises an error
|
||||
when non-existing atoms are found by the tokenizer
|
||||
|
||||
## Macro.to_string/2
|
||||
|
||||
The opposite of converting a string to its quoted form is
|
||||
`Macro.to_string/2`, which converts a quoted form to a string/binary
|
||||
representation.
|
||||
"""
|
||||
def string_to_quoted(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get opts, :file, "nofile"
|
||||
line = Keyword.get opts, :line, 1
|
||||
:elixir.string_to_quoted(to_char_list(string), line, file, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert the given string to its quoted form.
|
||||
|
||||
It returns the ast if it succeeds,
|
||||
raises an exception otherwise. The exception is a `TokenMissingError`
|
||||
in case a token is missing (usually because the expression is incomplete),
|
||||
`SyntaxError` otherwise.
|
||||
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get opts, :file, "nofile"
|
||||
line = Keyword.get opts, :line, 1
|
||||
:elixir.string_to_quoted!(to_char_list(string), line, file, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evals the given file.
|
||||
|
||||
Accepts `relative_to` as an argument to tell where the file is located.
|
||||
|
||||
While `load_file` loads a file and returns the loaded modules and their
|
||||
byte code, `eval_file` simply evaluates the file contents and returns the
|
||||
evaluation result and its bindings.
|
||||
"""
|
||||
def eval_file(file, relative_to \\ nil) do
|
||||
file = find_file(file, relative_to)
|
||||
eval_string File.read!(file), [], [file: file, line: 1]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Load the given file.
|
||||
|
||||
Accepts `relative_to` as an argument to tell where the file is located.
|
||||
If the file was already required/loaded, loads it again.
|
||||
|
||||
It returns a list of tuples `{ModuleName, <<byte_code>>}`, one tuple for
|
||||
each module defined in the file.
|
||||
|
||||
Notice that if `load_file` is invoked by different processes concurrently,
|
||||
the target file will be loaded concurrently many times. Check `require_file/2`
|
||||
if you don't want a file to be loaded concurrently.
|
||||
"""
|
||||
def load_file(file, relative_to \\ nil) when is_binary(file) do
|
||||
file = find_file(file, relative_to)
|
||||
:elixir_code_server.call {:acquire, file}
|
||||
loaded = :elixir_compiler.file file
|
||||
:elixir_code_server.cast {:loaded, file}
|
||||
loaded
|
||||
end
|
||||
|
||||
@doc """
|
||||
Requires the given `file`.
|
||||
|
||||
Accepts `relative_to` as an argument to tell where the file is located.
|
||||
The return value is the same as that of `load_file/2`. If the file was already
|
||||
required/loaded, doesn't do anything and returns `nil`.
|
||||
|
||||
Notice that if `require_file` is invoked by different processes concurrently,
|
||||
the first process to invoke `require_file` acquires a lock and the remaining
|
||||
ones will block until the file is available. I.e. if `require_file` is called
|
||||
N times with a given file, it will be loaded only once. The first process to
|
||||
call `require_file` will get the list of loaded modules, others will get `nil`.
|
||||
|
||||
Check `load_file/2` if you want a file to be loaded multiple times.
|
||||
"""
|
||||
def require_file(file, relative_to \\ nil) when is_binary(file) do
|
||||
file = find_file(file, relative_to)
|
||||
|
||||
case :elixir_code_server.call({:acquire, file}) do
|
||||
:loaded ->
|
||||
nil
|
||||
{:queued, ref} ->
|
||||
receive do {:elixir_code_server, ^ref, :loaded} -> nil end
|
||||
:proceed ->
|
||||
loaded = :elixir_compiler.file file
|
||||
:elixir_code_server.cast {:loaded, file}
|
||||
loaded
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the compilation options from the code server.
|
||||
|
||||
Check `compiler_options/1` for more information.
|
||||
"""
|
||||
def compiler_options do
|
||||
:elixir_code_server.call :compiler_options
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with the available compiler options.
|
||||
|
||||
See `Code.compiler_options/1` for more info.
|
||||
"""
|
||||
def available_compiler_options do
|
||||
[:docs, :debug_info, :ignore_module_conflict, :warnings_as_errors]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets compilation options.
|
||||
|
||||
These options are global since they are stored by Elixir's Code Server.
|
||||
|
||||
Available options are:
|
||||
|
||||
* `:docs` - when `true`, retain documentation in the compiled module,
|
||||
`true` by default
|
||||
|
||||
* `:debug_info` - when `true`, retain debug information in the compiled
|
||||
module; this allows a developer to reconstruct the original source
|
||||
code, `false` by default
|
||||
|
||||
* `:ignore_module_conflict` - when `true`, override modules that were
|
||||
already defined without raising errors, `false` by default
|
||||
|
||||
* `:warnings_as_errors` - cause compilation to fail when warnings are
|
||||
generated
|
||||
|
||||
"""
|
||||
def compiler_options(opts) do
|
||||
{opts, bad} = Keyword.split(opts, available_compiler_options)
|
||||
if bad != [] do
|
||||
bad = bad |> Keyword.keys |> Enum.join(", ")
|
||||
raise ArgumentError, message: "unknown compiler options: #{bad}"
|
||||
end
|
||||
:elixir_code_server.cast {:compiler_options, opts}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given string.
|
||||
|
||||
Returns a list of tuples where the first element is the module name
|
||||
and the second one is its byte code (as a binary).
|
||||
|
||||
For compiling many files at once, check `Kernel.ParallelCompiler.files/2`.
|
||||
"""
|
||||
def compile_string(string, file \\ "nofile") when is_binary(file) do
|
||||
:elixir_compiler.string to_char_list(string), file
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the quoted expression.
|
||||
|
||||
Returns a list of tuples where the first element is the module name and
|
||||
the second one is its byte code (as a binary).
|
||||
"""
|
||||
def compile_quoted(quoted, file \\ "nofile") when is_binary(file) do
|
||||
:elixir_compiler.quoted quoted, file
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is loaded.
|
||||
|
||||
If the module is already loaded, this works as no-op. If the module
|
||||
was not yet loaded, it tries to load it.
|
||||
|
||||
If it succeeds loading the module, it returns `{:module, module}`.
|
||||
If not, returns `{:error, reason}` with the error reason.
|
||||
|
||||
## Code loading on the Erlang VM
|
||||
|
||||
Erlang has two modes to load code: interactive and embedded.
|
||||
|
||||
By default, the Erlang VM runs in interactive mode, where modules
|
||||
are loaded as needed. In embedded mode the opposite happens, as all
|
||||
modules need to be loaded upfront or explicitly.
|
||||
|
||||
Therefore, this function is used to check if a module is loaded
|
||||
before using it and allows one to react accordingly. For example, the `URI`
|
||||
module uses this function to check if a specific parser exists for a given
|
||||
URI scheme.
|
||||
|
||||
## `Code.ensure_compiled/1`
|
||||
|
||||
Elixir also contains an `ensure_compiled/1` function that is a
|
||||
superset of `ensure_loaded/1`.
|
||||
|
||||
Since Elixir's compilation happens in parallel, in some situations
|
||||
you may need to use a module that was not yet compiled, therefore
|
||||
it can't even be loaded.
|
||||
|
||||
`ensure_compiled/1` halts the current process until the
|
||||
module we are depending on is available.
|
||||
|
||||
In most cases, `ensure_loaded/1` is enough. `ensure_compiled/1`
|
||||
must be used in rare cases, usually involving macros that need to
|
||||
invoke a module for callback information.
|
||||
"""
|
||||
def ensure_loaded(module) when is_atom(module) do
|
||||
:code.ensure_loaded(module)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is loaded.
|
||||
|
||||
Similar to `ensure_loaded/1`, but returns `true` if the module
|
||||
is already loaded or was successfully loaded. Returns `false`
|
||||
otherwise.
|
||||
"""
|
||||
def ensure_loaded?(module) do
|
||||
match?({:module, ^module}, ensure_loaded(module))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is compiled and loaded.
|
||||
|
||||
If the module is already loaded, it works as no-op. If the module was
|
||||
not loaded yet, it checks if it needs to be compiled first and then
|
||||
tries to load it.
|
||||
|
||||
If it succeeds loading the module, it returns `{:module, module}`.
|
||||
If not, returns `{:error, reason}` with the error reason.
|
||||
|
||||
Check `ensure_loaded/1` for more information on module loading
|
||||
and when to use `ensure_loaded/1` or `ensure_compiled/1`.
|
||||
"""
|
||||
def ensure_compiled(module) when is_atom(module) do
|
||||
case :code.ensure_loaded(module) do
|
||||
{:error, :nofile} = error ->
|
||||
case :erlang.get(:elixir_ensure_compiled) do
|
||||
:undefined -> error
|
||||
_ ->
|
||||
try do
|
||||
module.__info__(:module)
|
||||
{:module, module}
|
||||
rescue
|
||||
UndefinedFunctionError -> error
|
||||
end
|
||||
end
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is compiled and loaded.
|
||||
|
||||
Similar to `ensure_compiled/1`, but returns `true` if the module
|
||||
is already loaded or was successfully loaded and compiled.
|
||||
Returns `false` otherwise.
|
||||
"""
|
||||
def ensure_compiled?(module) do
|
||||
match?({:module, ^module}, ensure_compiled(module))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the docs for the given module.
|
||||
|
||||
When given a module name, it finds its BEAM code and reads the docs from it.
|
||||
|
||||
When given a path to a .beam file, it will load the docs directly from that
|
||||
file.
|
||||
|
||||
The return value depends on the `kind` value:
|
||||
|
||||
* `:docs` - list of all docstrings attached to functions and macros
|
||||
using the `@doc` attribute
|
||||
|
||||
* `:moduledoc` - tuple `{<line>, <doc>}` where `line` is the line on
|
||||
which module definition starts and `doc` is the string
|
||||
attached to the module using the `@moduledoc` attribute
|
||||
|
||||
* `:all` - a keyword list with both `:docs` and `:moduledoc`
|
||||
|
||||
"""
|
||||
def get_docs(module, kind) when is_atom(module) do
|
||||
case :code.get_object_code(module) do
|
||||
{_module, bin, _beam_path} ->
|
||||
do_get_docs(bin, kind)
|
||||
|
||||
:error -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_docs(binpath, kind) when is_binary(binpath) do
|
||||
do_get_docs(String.to_char_list(binpath), kind)
|
||||
end
|
||||
|
||||
@docs_chunk 'ExDc'
|
||||
|
||||
defp do_get_docs(bin_or_path, kind) do
|
||||
case :beam_lib.chunks(bin_or_path, [@docs_chunk]) do
|
||||
{:ok, {_module, [{@docs_chunk, bin}]}} ->
|
||||
lookup_docs(:erlang.binary_to_term(bin), kind)
|
||||
|
||||
{:error, :beam_lib, {:missing_chunk, _, @docs_chunk}} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp lookup_docs({:elixir_docs_v1, docs}, kind),
|
||||
do: do_lookup_docs(docs, kind)
|
||||
|
||||
# unsupported chunk version
|
||||
defp lookup_docs(_, _), do: nil
|
||||
|
||||
defp do_lookup_docs(docs, :all), do: docs
|
||||
defp do_lookup_docs(docs, kind) when kind in [:docs, :moduledoc],
|
||||
do: Keyword.get(docs, kind)
|
||||
|
||||
## Helpers
|
||||
|
||||
# Finds the file given the relative_to path.
|
||||
#
|
||||
# If the file is found, returns its path in binary, fails otherwise.
|
||||
defp find_file(file, relative_to) do
|
||||
file = if relative_to do
|
||||
Path.expand(file, relative_to)
|
||||
else
|
||||
Path.expand(file)
|
||||
end
|
||||
|
||||
if File.regular?(file) do
|
||||
file
|
||||
else
|
||||
raise Code.LoadError, file: file
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
defprotocol Collectable do
|
||||
@moduledoc """
|
||||
A protocol to traverse data structures.
|
||||
|
||||
The `Enum.into/2` function uses this protocol to insert an
|
||||
enumerable into a collection:
|
||||
|
||||
iex> Enum.into([a: 1, b: 2], %{})
|
||||
%{a: 1, b: 2}
|
||||
|
||||
## Why Collectable?
|
||||
|
||||
The `Enumerable` protocol is useful to take values out of a collection.
|
||||
In order to support a wide range of values, the functions provided by
|
||||
the `Enumerable` protocol do not keep shape. For example, passing a
|
||||
dictionary to `Enum.map/2` always returns a list.
|
||||
|
||||
This design is intentional. `Enumerable` was designed to support infinite
|
||||
collections, resources and other structures with fixed shape. For example,
|
||||
it doesn't make sense to insert values into a range, as it has a fixed
|
||||
shape where just the range limits are stored.
|
||||
|
||||
The `Collectable` module was designed to fill the gap left by the
|
||||
`Enumerable` protocol. `into/1` can be seen as the opposite of
|
||||
`Enumerable.reduce/3`. If `Enumerable` is about taking values out,
|
||||
`Collectable.into/1` is about collecting those values into a structure.
|
||||
"""
|
||||
|
||||
@type command :: {:cont, term} | :done | :halt
|
||||
|
||||
@doc """
|
||||
Returns a function that collects values alongside
|
||||
the initial accumulation value.
|
||||
|
||||
The returned function receives a collectable and injects a given
|
||||
value into it for every `{:cont, term}` instruction.
|
||||
|
||||
`:done` is passed when no further values will be injected, useful
|
||||
for closing resources and normalizing values. A collectable must
|
||||
be returned on `:done`.
|
||||
|
||||
If injection is suddenly interrupted, `:halt` is passed and it can
|
||||
return any value, as it won't be used.
|
||||
"""
|
||||
@spec into(t) :: {term, (term, command -> t | term)}
|
||||
def into(collectable)
|
||||
end
|
||||
|
||||
defimpl Collectable, for: List do
|
||||
def into(original) do
|
||||
{[], fn
|
||||
list, {:cont, x} -> [x|list]
|
||||
list, :done -> original ++ :lists.reverse(list)
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: BitString do
|
||||
def into(original) do
|
||||
{original, fn
|
||||
acc, {:cont, x} when is_bitstring(x) -> [acc|x]
|
||||
acc, :done -> IO.iodata_to_binary(acc)
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: Map do
|
||||
def into(original) do
|
||||
{original, fn
|
||||
map, {:cont, {k, v}} -> :maps.put(k, v, map)
|
||||
map, :done -> map
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
@@ -1,693 +0,0 @@
|
||||
defmodule Dict do
|
||||
@moduledoc ~S"""
|
||||
This module specifies the Dict API expected to be
|
||||
implemented by different dictionaries. It also provides
|
||||
functions that redirect to the underlying Dict, allowing
|
||||
a developer to work with different Dict implementations
|
||||
using one API.
|
||||
|
||||
To create a new dict, use the `new` functions defined
|
||||
by each dict type:
|
||||
|
||||
HashDict.new #=> creates an empty HashDict
|
||||
|
||||
In the examples below, `dict_impl` means a specific
|
||||
`Dict` implementation, for example `HashDict` or `Map`.
|
||||
|
||||
## Protocols
|
||||
|
||||
Besides implementing the functions in this module, all
|
||||
dictionaries are required to implement the `Access`
|
||||
protocol:
|
||||
|
||||
iex> dict = dict_impl.new
|
||||
iex> dict = Dict.put(dict, :hello, :world)
|
||||
iex> dict[:hello]
|
||||
:world
|
||||
|
||||
As well as the `Enumerable` and `Collectable` protocols.
|
||||
|
||||
## Match
|
||||
|
||||
Dictionaries are required to implement all operations
|
||||
using the match (`===`) operator.
|
||||
|
||||
## Default implementation
|
||||
|
||||
Default implementations for some functions in the `Dict` module
|
||||
are provided via `use Dict`.
|
||||
|
||||
For example:
|
||||
|
||||
defmodule MyDict do
|
||||
use Dict
|
||||
|
||||
# implement required functions (see below)
|
||||
# override default implementations if optimization
|
||||
# is needed
|
||||
end
|
||||
|
||||
The client module must contain the following functions:
|
||||
|
||||
* `delete/2`
|
||||
* `fetch/2`
|
||||
* `put/3`
|
||||
* `reduce/3`
|
||||
* `size/1`
|
||||
|
||||
All functions, except `reduce/3`, are required by the Dict behaviour.
|
||||
`reduce/3` must be implemtented as per the Enumerable protocol.
|
||||
|
||||
Based on these functions, `Dict` generates default implementations
|
||||
for the following functions:
|
||||
|
||||
* `drop/2`
|
||||
* `equal?/2`
|
||||
* `fetch!/2`
|
||||
* `get/2`
|
||||
* `get/3`
|
||||
* `has_key?/2`
|
||||
* `keys/1`
|
||||
* `merge/2`
|
||||
* `merge/3`
|
||||
* `pop/2`
|
||||
* `pop/3`
|
||||
* `put_new/3`
|
||||
* `split/2`
|
||||
* `take/2`
|
||||
* `to_list/1`
|
||||
* `update/4`
|
||||
* `update!/3`
|
||||
* `values/1`
|
||||
|
||||
All of these functions are defined as overridable, so you can provide
|
||||
your own implementation if needed.
|
||||
|
||||
Note you can also test your custom module via `Dict`'s doctests:
|
||||
|
||||
defmodule MyDict do
|
||||
# ...
|
||||
end
|
||||
|
||||
defmodule MyTests do
|
||||
use ExUnit.Case
|
||||
doctest Dict
|
||||
defp dict_impl, do: MyDict
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
use Behaviour
|
||||
|
||||
@type key :: any
|
||||
@type value :: any
|
||||
@type t :: list | map
|
||||
|
||||
defcallback new :: t
|
||||
defcallback delete(t, key) :: t
|
||||
defcallback drop(t, Enum.t) :: t
|
||||
defcallback equal?(t, t) :: boolean
|
||||
defcallback get(t, key) :: value
|
||||
defcallback get(t, key, value) :: value
|
||||
defcallback fetch(t, key) :: {:ok, value} | :error
|
||||
defcallback fetch!(t, key) :: value | no_return
|
||||
defcallback has_key?(t, key) :: boolean
|
||||
defcallback keys(t) :: [key]
|
||||
defcallback merge(t, t) :: t
|
||||
defcallback merge(t, t, (key, value, value -> value)) :: t
|
||||
defcallback pop(t, key) :: {value, t}
|
||||
defcallback pop(t, key, value) :: {value, t}
|
||||
defcallback put(t, key, value) :: t
|
||||
defcallback put_new(t, key, value) :: t
|
||||
defcallback size(t) :: non_neg_integer()
|
||||
defcallback split(t, Enum.t) :: {t, t}
|
||||
defcallback take(t, Enum.t) :: t
|
||||
defcallback to_list(t) :: list()
|
||||
defcallback update(t, key, value, (value -> value)) :: t
|
||||
defcallback update!(t, key, (value -> value)) :: t | no_return
|
||||
defcallback values(t) :: list(value)
|
||||
|
||||
defmacro __using__(_) do
|
||||
# Use this import to guarantee proper code expansion
|
||||
import Kernel, except: [size: 1]
|
||||
|
||||
quote do
|
||||
@behaviour Dict
|
||||
|
||||
def get(dict, key, default \\ nil) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
def fetch!(dict, key) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> raise KeyError, key: key, term: dict
|
||||
end
|
||||
end
|
||||
|
||||
def has_key?(dict, key) do
|
||||
match? {:ok, _}, fetch(dict, key)
|
||||
end
|
||||
|
||||
def put_new(dict, key, value) do
|
||||
case has_key?(dict, key) do
|
||||
true -> dict
|
||||
false -> put(dict, key, value)
|
||||
end
|
||||
end
|
||||
|
||||
def drop(dict, keys) do
|
||||
Enum.reduce(keys, dict, &delete(&2, &1))
|
||||
end
|
||||
|
||||
def take(dict, keys) do
|
||||
Enum.reduce(keys, new, fn key, acc ->
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> put(acc, key, value)
|
||||
:error -> acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def to_list(dict) do
|
||||
reduce(dict, {:cont, []}, fn
|
||||
kv, acc -> {:cont, [kv|acc]}
|
||||
end) |> elem(1) |> :lists.reverse
|
||||
end
|
||||
|
||||
def keys(dict) do
|
||||
reduce(dict, {:cont, []}, fn
|
||||
{k, _}, acc -> {:cont, [k|acc]}
|
||||
end) |> elem(1) |> :lists.reverse
|
||||
end
|
||||
|
||||
def values(dict) do
|
||||
reduce(dict, {:cont, []}, fn
|
||||
{_, v}, acc -> {:cont, [v|acc]}
|
||||
end) |> elem(1) |> :lists.reverse
|
||||
end
|
||||
|
||||
def equal?(dict1, dict2) do
|
||||
# Use this import to avoid conflicts in the user code
|
||||
import Kernel, except: [size: 1]
|
||||
|
||||
case size(dict1) == size(dict2) do
|
||||
false -> false
|
||||
true ->
|
||||
reduce(dict1, {:cont, true}, fn({k, v}, _acc) ->
|
||||
case fetch(dict2, k) do
|
||||
{:ok, ^v} -> {:cont, true}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
def merge(dict1, dict2, fun \\ fn(_k, _v1, v2) -> v2 end) do
|
||||
# Use this import to avoid conflicts in the user code
|
||||
import Kernel, except: [size: 1]
|
||||
|
||||
if size(dict1) < size(dict2) do
|
||||
reduce(dict1, {:cont, dict2}, fn {k, v1}, acc ->
|
||||
{:cont, update(acc, k, v1, &fun.(k, v1, &1))}
|
||||
end)
|
||||
else
|
||||
reduce(dict2, {:cont, dict1}, fn {k, v2}, acc ->
|
||||
{:cont, update(acc, k, v2, &fun.(k, &1, v2))}
|
||||
end)
|
||||
end |> elem(1)
|
||||
end
|
||||
|
||||
def update(dict, key, initial, fun) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} ->
|
||||
put(dict, key, fun.(value))
|
||||
:error ->
|
||||
put(dict, key, initial)
|
||||
end
|
||||
end
|
||||
|
||||
def update!(dict, key, fun) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} ->
|
||||
put(dict, key, fun.(value))
|
||||
:error ->
|
||||
raise KeyError, key: key, term: dict
|
||||
end
|
||||
end
|
||||
|
||||
def pop(dict, key, default \\ nil) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} ->
|
||||
{value, delete(dict, key)}
|
||||
:error ->
|
||||
{default, dict}
|
||||
end
|
||||
end
|
||||
|
||||
def split(dict, keys) do
|
||||
Enum.reduce(keys, {new, dict}, fn key, {inc, exc} = acc ->
|
||||
case fetch(exc, key) do
|
||||
{:ok, value} ->
|
||||
{put(inc, key, value), delete(exc, key)}
|
||||
:error ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defoverridable merge: 2, merge: 3, equal?: 2, to_list: 1, keys: 1,
|
||||
values: 1, take: 2, drop: 2, get: 2, get: 3, fetch!: 2,
|
||||
has_key?: 2, put_new: 3, pop: 2, pop: 3, split: 2,
|
||||
update: 4, update!: 3
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
defmacrop target(dict) do
|
||||
quote do
|
||||
case unquote(dict) do
|
||||
%{__struct__: x} when is_atom(x) ->
|
||||
x
|
||||
%{} ->
|
||||
Map
|
||||
x when is_list(x) ->
|
||||
Keyword
|
||||
x ->
|
||||
unsupported_dict(x)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all keys in `dict`.
|
||||
The keys are not guaranteed to be in any order.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> Enum.sort(Dict.keys(dict))
|
||||
[:a,:b]
|
||||
|
||||
"""
|
||||
@spec keys(t) :: [key]
|
||||
def keys(dict) do
|
||||
target(dict).keys(dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all values in `dict`.
|
||||
The values are not guaranteed to be in any order.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> Enum.sort(Dict.values(dict))
|
||||
[1,2]
|
||||
|
||||
"""
|
||||
@spec values(t) :: [value]
|
||||
def values(dict) do
|
||||
target(dict).values(dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of elements in `dict`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> Dict.size(dict)
|
||||
2
|
||||
|
||||
"""
|
||||
@spec size(t) :: non_neg_integer
|
||||
def size(dict) do
|
||||
target(dict).size(dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether the given `key` exists in the given `dict`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> Dict.has_key?(dict, :a)
|
||||
true
|
||||
iex> Dict.has_key?(dict, :b)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec has_key?(t, key) :: boolean
|
||||
def has_key?(dict, key) do
|
||||
target(dict).has_key?(dict, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value associated with `key` in `dict`. If `dict` does not
|
||||
contain `key`, returns `default` (or `nil` if not provided).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> Dict.get(dict, :a)
|
||||
1
|
||||
iex> Dict.get(dict, :b)
|
||||
nil
|
||||
iex> Dict.get(dict, :b, 3)
|
||||
3
|
||||
"""
|
||||
@spec get(t, key, value) :: value
|
||||
def get(dict, key, default \\ nil) do
|
||||
target(dict).get(dict, key, default)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `{:ok, value}` associated with `key` in `dict`.
|
||||
If `dict` does not contain `key`, returns `:error`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> Dict.fetch(dict, :a)
|
||||
{:ok, 1}
|
||||
iex> Dict.fetch(dict, :b)
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec fetch(t, key) :: value
|
||||
def fetch(dict, key) do
|
||||
target(dict).fetch(dict, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value associated with `key` in `dict`. If `dict` does not
|
||||
contain `key`, it raises `KeyError`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> Dict.fetch!(dict, :a)
|
||||
1
|
||||
|
||||
"""
|
||||
@spec fetch!(t, key) :: value | no_return
|
||||
def fetch!(dict, key) do
|
||||
target(dict).fetch!(dict, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores the given `value` under `key` in `dict`.
|
||||
If `dict` already has `key`, the stored value is replaced by the new one.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict = Dict.put(dict, :a, 3)
|
||||
iex> Dict.get(dict, :a)
|
||||
3
|
||||
|
||||
"""
|
||||
@spec put(t, key, value) :: t
|
||||
def put(dict, key, val) do
|
||||
target(dict).put(dict, key, val)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key` in `dict` unless `key` already exists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict = Dict.put_new(dict, :a, 3)
|
||||
iex> Dict.get(dict, :a)
|
||||
1
|
||||
|
||||
"""
|
||||
@spec put_new(t, key, value) :: t
|
||||
def put_new(dict, key, val) do
|
||||
target(dict).put_new(dict, key, val)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the entry stored under the given `key` from `dict`.
|
||||
If `dict` does not contain `key`, returns the dictionary unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict = Dict.delete(dict, :a)
|
||||
iex> Dict.get(dict, :a)
|
||||
nil
|
||||
|
||||
iex> dict = Enum.into([b: 2], dict_impl.new)
|
||||
iex> Dict.delete(dict, :a) == dict
|
||||
true
|
||||
|
||||
"""
|
||||
@spec delete(t, key) :: t
|
||||
def delete(dict, key) do
|
||||
target(dict).delete(dict, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges the dict `dict2` into dict `dict1`.
|
||||
|
||||
If one of the `dict2` entries already exists in `dict1`, the
|
||||
functions in entries in `dict2` have higher precedence unless a
|
||||
function is given to resolve conflicts.
|
||||
|
||||
Notice this function is polymorphic as it merges dicts of any
|
||||
type. Each dict implementation also provides a `merge` function,
|
||||
but they can only merge dicts of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict1 = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict2 = Enum.into([a: 3, d: 4], dict_impl.new)
|
||||
iex> dict = Dict.merge(dict1, dict2)
|
||||
iex> [a: Dict.get(dict, :a), b: Dict.get(dict, :b), d: Dict.get(dict, :d)]
|
||||
[a: 3, b: 2, d: 4]
|
||||
|
||||
iex> dict1 = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict2 = Enum.into([a: 3, d: 4], dict_impl.new)
|
||||
iex> dict = Dict.merge(dict1, dict2, fn(_k, v1, v2) ->
|
||||
...> v1 + v2
|
||||
...> end)
|
||||
iex> [a: Dict.get(dict, :a), b: Dict.get(dict, :b), d: Dict.get(dict, :d)]
|
||||
[a: 4, b: 2, d: 4]
|
||||
|
||||
"""
|
||||
@spec merge(t, t, (key, value, value -> value)) :: t
|
||||
def merge(dict1, dict2, fun \\ fn(_k, _v1, v2) -> v2 end) do
|
||||
target1 = target(dict1)
|
||||
target2 = target(dict2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.merge(dict1, dict2, fun)
|
||||
else
|
||||
Enumerable.reduce(dict2, {:cont, dict1}, fn({k, v}, acc) ->
|
||||
{:cont, target1.update(acc, k, v, fn(other) -> fun.(k, other, v) end)}
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value associated with `key` in `dict` as
|
||||
well as the `dict` without `key`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> {v, dict} = Dict.pop dict, :a
|
||||
iex> {v, Enum.sort(dict)}
|
||||
{1,[]}
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> {v, dict} = Dict.pop dict, :b
|
||||
iex> {v, Enum.sort(dict)}
|
||||
{nil,[a: 1]}
|
||||
|
||||
iex> dict = Enum.into([a: 1], dict_impl.new)
|
||||
iex> {v, dict} = Dict.pop dict, :b, 3
|
||||
iex> {v, Enum.sort(dict)}
|
||||
{3,[a: 1]}
|
||||
|
||||
"""
|
||||
@spec pop(t, key, value) :: {value, t}
|
||||
def pop(dict, key, default \\ nil) do
|
||||
target(dict).pop(dict, key, default)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a value in `dict` by calling `fun` on the value to get a new
|
||||
value. An exception is generated if `key` is not present in the dict.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict = Dict.update!(dict, :a, fn(val) -> -val end)
|
||||
iex> Dict.get(dict, :a)
|
||||
-1
|
||||
|
||||
"""
|
||||
@spec update!(t, key, (value -> value)) :: t
|
||||
def update!(dict, key, fun) do
|
||||
target(dict).update!(dict, key, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a value in `dict` by calling `fun` on the value to get a new value. If
|
||||
`key` is not present in `dict` then `initial` will be stored as the first
|
||||
value.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict = Dict.update(dict, :c, 3, fn(val) -> -val end)
|
||||
iex> Dict.get(dict, :c)
|
||||
3
|
||||
|
||||
"""
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update(dict, key, initial, fun) do
|
||||
target(dict).update(dict, key, initial, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a tuple of two dicts, where the first dict contains only
|
||||
entries from `dict` with keys in `keys`, and the second dict
|
||||
contains only entries from `dict` with keys not in `keys`
|
||||
|
||||
Any non-member keys are ignored.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2, c: 3, d: 4], dict_impl.new)
|
||||
iex> {dict1, dict2} = Dict.split(dict, [:a, :c, :e])
|
||||
iex> {Dict.to_list(dict1) |> Enum.sort, Dict.to_list(dict2) |> Enum.sort}
|
||||
{[a: 1, c: 3], [b: 2, d: 4]}
|
||||
|
||||
iex> dict = Enum.into([], dict_impl.new)
|
||||
iex> {dict1, dict2} = Dict.split(dict, [:a, :c])
|
||||
iex> {Dict.to_list(dict1), Dict.to_list(dict2)}
|
||||
{[], []}
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> {dict1, dict2} = Dict.split(dict, [:a, :b, :c])
|
||||
iex> {Dict.to_list(dict1) |> Enum.sort, Dict.to_list(dict2)}
|
||||
{[a: 1, b: 2], []}
|
||||
|
||||
"""
|
||||
@spec split(t, [key]) :: {t, t}
|
||||
def split(dict, keys) do
|
||||
target(dict).split(dict, keys)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a new dict where the given `keys` are removed from `dict`.
|
||||
Any non-member keys are ignored.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict = Dict.drop(dict, [:a, :c, :d])
|
||||
iex> Dict.to_list(dict)
|
||||
[b: 2]
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict = Dict.drop(dict, [:c, :d])
|
||||
iex> Dict.to_list(dict) |> Enum.sort
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec drop(t, [key]) :: t
|
||||
def drop(dict, keys) do
|
||||
target(dict).drop(dict, keys)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a new dict where only the keys in `keys` from `dict` are included.
|
||||
|
||||
Any non-member keys are ignored.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict = Enum.into([a: 1, b: 2], dict_impl.new)
|
||||
iex> dict = Dict.take(dict, [:a, :c, :d])
|
||||
iex> Dict.to_list(dict)
|
||||
[a: 1]
|
||||
iex> dict = Dict.take(dict, [:c, :d])
|
||||
iex> Dict.to_list(dict)
|
||||
[]
|
||||
|
||||
"""
|
||||
@spec take(t, [key]) :: t
|
||||
def take(dict, keys) do
|
||||
target(dict).take(dict, keys)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec empty(t) :: t
|
||||
def empty(dict) do
|
||||
target(dict).empty(dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if two dicts are equal using `===`.
|
||||
|
||||
Notice this function is polymorphic as it compares dicts of any
|
||||
type. Each dict implementation also provides an `equal?` function,
|
||||
but they can only compare dicts of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dict1 = Enum.into([a: 2, b: 3, f: 5, c: 123], dict_impl.new)
|
||||
iex> dict2 = [a: 2, b: 3, f: 5, c: 123]
|
||||
iex> Dict.equal?(dict1, dict2)
|
||||
true
|
||||
|
||||
iex> dict1 = Enum.into([a: 2, b: 3, f: 5, c: 123], dict_impl.new)
|
||||
iex> dict2 = []
|
||||
iex> Dict.equal?(dict1, dict2)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec equal?(t, t) :: boolean
|
||||
def equal?(dict1, dict2) do
|
||||
target1 = target(dict1)
|
||||
target2 = target(dict2)
|
||||
|
||||
cond do
|
||||
target1 == target2 ->
|
||||
target1.equal?(dict1, dict2)
|
||||
|
||||
target1.size(dict1) == target2.size(dict2) ->
|
||||
Enumerable.reduce(dict2, {:cont, true}, fn({k, v}, _acc) ->
|
||||
case target1.fetch(dict1, k) do
|
||||
{:ok, ^v} -> {:cont, true}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of key-value pairs stored in `dict`.
|
||||
No particular order is enforced.
|
||||
"""
|
||||
@spec to_list(t) :: list
|
||||
def to_list(dict) do
|
||||
target(dict).to_list(dict)
|
||||
end
|
||||
|
||||
defp unsupported_dict(dict) do
|
||||
raise ArgumentError, "unsupported dict: #{inspect dict}"
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,812 +0,0 @@
|
||||
defmodule Exception do
|
||||
@moduledoc """
|
||||
Functions to format throw/catch/exit and exceptions.
|
||||
|
||||
Note that stacktraces in Elixir are updated on throw,
|
||||
errors and exits. For example, at any given moment,
|
||||
`System.stacktrace` will return the stacktrace for the
|
||||
last throw/error/exit that occurred in the current process.
|
||||
|
||||
Do not rely on the particular format returned by the `format`
|
||||
functions in this module. They may be changed in future releases
|
||||
in order to better suit Elixir's tool chain. In other words,
|
||||
by using the functions in this module it is guaranteed you will
|
||||
format exceptions as in the current Elixir version being used.
|
||||
"""
|
||||
|
||||
@typedoc "The exception type"
|
||||
@type t :: %{__struct__: module, __exception__: true}
|
||||
|
||||
@typedoc "The kind handled by formatting functions"
|
||||
@type kind :: :error | :exit | :throw | {:EXIT, pid}
|
||||
|
||||
@type stacktrace :: [stacktrace_entry]
|
||||
@type stacktrace_entry ::
|
||||
{module, function, arity_or_args, location} |
|
||||
{function, arity_or_args, location}
|
||||
|
||||
@typep arity_or_args :: non_neg_integer | list
|
||||
@typep location :: Keyword.t
|
||||
|
||||
@callback exception(term) :: t
|
||||
@callback message(t) :: String.t
|
||||
|
||||
@doc """
|
||||
Returns true if the given argument is an exception.
|
||||
"""
|
||||
def exception?(%{__struct__: struct, __exception__: true}) when is_atom(struct), do: true
|
||||
def exception?(_), do: false
|
||||
|
||||
@doc """
|
||||
Gets the message for an exception.
|
||||
"""
|
||||
def message(%{__struct__: module, __exception__: true} = exception) when is_atom(module) do
|
||||
try do
|
||||
module.message(exception)
|
||||
rescue
|
||||
e ->
|
||||
raise ArgumentError,
|
||||
"Got #{inspect e.__struct__} with message " <>
|
||||
"\"#{message(e)}\" while retrieving message for #{inspect(exception)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Normalizes an exception, converting Erlang exceptions
|
||||
to Elixir exceptions.
|
||||
|
||||
It takes the `kind` spilled by `catch` as an argument and
|
||||
normalizes only `:error`, returning the untouched payload
|
||||
for others.
|
||||
|
||||
The third argument, a stacktrace, is optional. If it is
|
||||
not supplied `System.stacktrace/0` will sometimes be used
|
||||
to get additional information for the `kind` `:error`. If
|
||||
the stacktrace is unknown and `System.stacktrace/0` would
|
||||
not return the stacktrace corresponding to the exception
|
||||
an empty stacktrace, `[]`, must be used.
|
||||
"""
|
||||
@spec normalize(:error, any, stacktrace) :: t
|
||||
@spec normalize(kind, payload, stacktrace) :: payload when payload: var
|
||||
|
||||
# Generating a stacktrace is expensive, default to nil
|
||||
# to only fetch it when needed.
|
||||
def normalize(kind, payload, stacktrace \\ nil)
|
||||
|
||||
def normalize(:error, exception, stacktrace) do
|
||||
if exception?(exception) do
|
||||
exception
|
||||
else
|
||||
ErlangError.normalize(exception, stacktrace)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(_kind, payload, _stacktrace) do
|
||||
payload
|
||||
end
|
||||
|
||||
@doc """
|
||||
Normalizes and formats any throw, error and exit.
|
||||
|
||||
The message is formatted and displayed in the same
|
||||
format as used by Elixir's CLI.
|
||||
|
||||
The third argument, a stacktrace, is optional. If it is
|
||||
not supplied `System.stacktrace/0` will sometimes be used
|
||||
to get additional information for the `kind` `:error`. If
|
||||
the stacktrace is unknown and `System.stacktrace/0` would
|
||||
not return the stacktrace corresponding to the exception
|
||||
an empty stacktrace, `[]`, must be used.
|
||||
"""
|
||||
@spec format_banner(kind, any, stacktrace | nil) :: String.t
|
||||
def format_banner(kind, exception, stacktrace \\ nil)
|
||||
|
||||
def format_banner(:error, exception, stacktrace) do
|
||||
exception = normalize(:error, exception, stacktrace)
|
||||
"** (" <> inspect(exception.__struct__) <> ") " <> message(exception)
|
||||
end
|
||||
|
||||
def format_banner(:throw, reason, _stacktrace) do
|
||||
"** (throw) " <> inspect(reason)
|
||||
end
|
||||
|
||||
def format_banner(:exit, reason, _stacktrace) do
|
||||
"** (exit) " <> format_exit(reason, <<"\n ">>)
|
||||
end
|
||||
|
||||
def format_banner({:EXIT, pid}, reason, _stacktrace) do
|
||||
"** (EXIT from #{inspect pid}) " <> format_exit(reason, <<"\n ">>)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Normalizes and formats throw/errors/exits and stacktrace.
|
||||
|
||||
It relies on `format_banner/3` and `format_stacktrace/1`
|
||||
to generate the final format.
|
||||
|
||||
Note that `{:EXIT, pid}` do not generate a stacktrace though
|
||||
(as they are retrieved as messages without stacktraces).
|
||||
"""
|
||||
|
||||
@spec format(kind, any, stacktrace | nil) :: String.t
|
||||
|
||||
def format(kind, payload, stacktrace \\ nil)
|
||||
|
||||
def format({:EXIT, _} = kind, any, _) do
|
||||
format_banner(kind, any)
|
||||
end
|
||||
|
||||
def format(kind, payload, stacktrace) do
|
||||
stacktrace = stacktrace || System.stacktrace
|
||||
message = format_banner(kind, payload, stacktrace)
|
||||
case stacktrace do
|
||||
[] -> message
|
||||
_ -> message <> "\n" <> format_stacktrace(stacktrace)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats an exit, returns a string.
|
||||
|
||||
Often there are errors/exceptions inside exits. Exits are often
|
||||
wrapped by the caller and provide stacktraces too. This function
|
||||
formats exits in a way to nicely show the exit reason, caller
|
||||
and stacktrace.
|
||||
"""
|
||||
@spec format_exit(any) :: String.t
|
||||
def format_exit(reason) do
|
||||
format_exit(reason, <<"\n ">>)
|
||||
end
|
||||
|
||||
# 2-Tuple could be caused by an error if the second element is a stacktrace.
|
||||
defp format_exit({exception, maybe_stacktrace} = reason, joiner)
|
||||
when is_list(maybe_stacktrace) and maybe_stacktrace !== [] do
|
||||
try do
|
||||
Enum.map(maybe_stacktrace, &format_stacktrace_entry/1)
|
||||
else
|
||||
formatted_stacktrace ->
|
||||
# Assume a non-empty list formattable as stacktrace is a
|
||||
# stacktrace, so exit was caused by an error.
|
||||
message = "an exception was raised:" <> joiner <>
|
||||
format_banner(:error, exception, maybe_stacktrace)
|
||||
Enum.join([message | formatted_stacktrace], joiner <> <<" ">>)
|
||||
catch
|
||||
:error, _ ->
|
||||
# Not a stacktrace, was an exit.
|
||||
format_exit_reason(reason)
|
||||
end
|
||||
end
|
||||
|
||||
# :supervisor.start_link returns this error reason when it fails to init
|
||||
# because a child's start_link raises.
|
||||
defp format_exit({:shutdown,
|
||||
{:failed_to_start_child, child, {:EXIT, reason}}}, joiner) do
|
||||
format_start_child(child, reason, joiner)
|
||||
end
|
||||
|
||||
# :supervisor.start_link returns this error reason when it fails to init
|
||||
# because a child's start_link returns {:error, reason}.
|
||||
defp format_exit({:shutdown, {:failed_to_start_child, child, reason}},
|
||||
joiner) do
|
||||
format_start_child(child, reason, joiner)
|
||||
end
|
||||
|
||||
# 2-Tuple could be an exit caused by mfa if second element is mfa, args
|
||||
# must be a list of arguments - max length 255 due to max arity.
|
||||
defp format_exit({reason2, {mod, fun, args}} = reason, joiner)
|
||||
when length(args) < 256 do
|
||||
try do
|
||||
format_mfa(mod, fun, args)
|
||||
else
|
||||
mfa ->
|
||||
# Assume tuple formattable as an mfa is an mfa, so exit was caused by
|
||||
# failed mfa.
|
||||
"exited in: " <> mfa <> joiner <>
|
||||
"** (EXIT) " <> format_exit(reason2, joiner <> <<" ">>)
|
||||
catch
|
||||
:error, _ ->
|
||||
# Not an mfa, was an exit.
|
||||
format_exit_reason(reason)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_exit(reason, _joiner) do
|
||||
format_exit_reason(reason)
|
||||
end
|
||||
|
||||
defp format_exit_reason(:normal), do: "normal"
|
||||
defp format_exit_reason(:shutdown), do: "shutdown"
|
||||
|
||||
defp format_exit_reason({:shutdown, reason}) do
|
||||
"shutdown: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
defp format_exit_reason(:timeout), do: "time out"
|
||||
defp format_exit_reason(:killed), do: "killed"
|
||||
defp format_exit_reason(:noconnection), do: "no connection"
|
||||
|
||||
defp format_exit_reason(:noproc) do
|
||||
"no process"
|
||||
end
|
||||
|
||||
defp format_exit_reason({:nodedown, node_name}) when is_atom(node_name) do
|
||||
"no connection to #{node_name}"
|
||||
end
|
||||
|
||||
# :gen_server exit reasons
|
||||
|
||||
defp format_exit_reason({:already_started, pid}) do
|
||||
"already started: " <> inspect(pid)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:bad_return_value, value}) do
|
||||
"bad return value: " <> inspect(value)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:bad_call, request}) do
|
||||
"bad call: " <> inspect(request)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:bad_cast, request}) do
|
||||
"bad cast: " <> inspect(request)
|
||||
end
|
||||
|
||||
# :supervisor.start_link error reasons
|
||||
|
||||
# If value is a list will be be formatted by mfa exit in format_exit/1
|
||||
defp format_exit_reason({:bad_return, {mod, :init, value}})
|
||||
when is_atom(mod) do
|
||||
format_mfa(mod, :init, 1) <> " returned a bad value: " <> inspect(value)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:bad_start_spec, start_spec}) do
|
||||
"bad start spec: invalid children: " <> inspect(start_spec)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:start_spec, start_spec}) do
|
||||
"bad start spec: " <> format_sup_spec(start_spec)
|
||||
end
|
||||
|
||||
defp format_exit_reason({:supervisor_data, data}) do
|
||||
"bad supervisor data: " <> format_sup_data(data)
|
||||
end
|
||||
|
||||
defp format_exit_reason(reason), do: inspect(reason)
|
||||
|
||||
defp format_start_child(child, reason, joiner) do
|
||||
"shutdown: failed to start child: " <> inspect(child) <> joiner <>
|
||||
"** (EXIT) " <> format_exit(reason, joiner <> <<" ">>)
|
||||
end
|
||||
|
||||
defp format_sup_data({:invalid_type, type}) do
|
||||
"invalid type: " <> inspect(type)
|
||||
end
|
||||
|
||||
defp format_sup_data({:invalid_strategy, strategy}) do
|
||||
"invalid strategy: " <> inspect(strategy)
|
||||
end
|
||||
|
||||
defp format_sup_data({:invalid_intensity, intensity}) do
|
||||
"invalid intensity: " <> inspect(intensity)
|
||||
end
|
||||
|
||||
defp format_sup_data({:invalid_period, period}) do
|
||||
"invalid period: " <> inspect(period)
|
||||
end
|
||||
|
||||
defp format_sup_data(other), do: inspect(other)
|
||||
|
||||
defp format_sup_spec({:invalid_child_spec, child_spec}) do
|
||||
"invalid child spec: " <> inspect(child_spec)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_child_type, type}) do
|
||||
"invalid child type: " <> inspect(type)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_mfa, mfa}) do
|
||||
"invalid mfa: " <> inspect(mfa)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_restart_type, restart}) do
|
||||
"invalid restart type: " <> inspect(restart)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_shutdown, shutdown}) do
|
||||
"invalid shutdown: " <> inspect(shutdown)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_module, mod}) do
|
||||
"invalid module: " <> inspect(mod)
|
||||
end
|
||||
|
||||
defp format_sup_spec({:invalid_modules, modules}) do
|
||||
"invalid modules: " <> inspect(modules)
|
||||
end
|
||||
|
||||
defp format_sup_spec(other), do: inspect(other)
|
||||
|
||||
@doc """
|
||||
Receives a stacktrace entry and formats it into a string.
|
||||
"""
|
||||
@spec format_stacktrace_entry(stacktrace_entry) :: String.t
|
||||
def format_stacktrace_entry(entry)
|
||||
|
||||
# From Macro.Env.stacktrace
|
||||
def format_stacktrace_entry({module, :__MODULE__, 0, location}) do
|
||||
format_location(location) <> inspect(module) <> " (module)"
|
||||
end
|
||||
|
||||
# From :elixir_compiler_*
|
||||
def format_stacktrace_entry({_module, :__MODULE__, 1, location}) do
|
||||
format_location(location) <> "(module)"
|
||||
end
|
||||
|
||||
# From :elixir_compiler_*
|
||||
def format_stacktrace_entry({_module, :__FILE__, 1, location}) do
|
||||
format_location(location) <> "(file)"
|
||||
end
|
||||
|
||||
def format_stacktrace_entry({module, fun, arity, location}) do
|
||||
format_application(module) <> format_location(location) <> format_mfa(module, fun, arity)
|
||||
end
|
||||
|
||||
def format_stacktrace_entry({fun, arity, location}) do
|
||||
format_location(location) <> format_fa(fun, arity)
|
||||
end
|
||||
|
||||
defp format_application(module) do
|
||||
case :application.get_application(module) do
|
||||
{:ok, app} -> "(" <> Atom.to_string(app) <> ") "
|
||||
:undefined -> ""
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the stacktrace.
|
||||
|
||||
A stacktrace must be given as an argument. If not, the stacktrace
|
||||
is retrieved from `Process.info/2`.
|
||||
"""
|
||||
def format_stacktrace(trace \\ nil) do
|
||||
trace = trace || case Process.info(self, :current_stacktrace) do
|
||||
{:current_stacktrace, t} -> Enum.drop(t, 3)
|
||||
end
|
||||
|
||||
case trace do
|
||||
[] -> "\n"
|
||||
s -> " " <> Enum.map_join(s, "\n ", &format_stacktrace_entry(&1)) <> "\n"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives an anonymous function and arity and formats it as
|
||||
shown in stacktraces. The arity may also be a list of arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
Exception.format_fa(fn -> end, 1)
|
||||
#=> "#Function<...>/1"
|
||||
|
||||
"""
|
||||
def format_fa(fun, arity) when is_function(fun) do
|
||||
"#{inspect fun}#{format_arity(arity)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a module, fun and arity and formats it
|
||||
as shown in stacktraces. The arity may also be a list
|
||||
of arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Exception.format_mfa Foo, :bar, 1
|
||||
"Foo.bar/1"
|
||||
|
||||
iex> Exception.format_mfa Foo, :bar, []
|
||||
"Foo.bar()"
|
||||
|
||||
iex> Exception.format_mfa nil, :bar, []
|
||||
"nil.bar()"
|
||||
|
||||
Anonymous functions are reported as -func/arity-anonfn-count-,
|
||||
where func is the name of the enclosing function. Convert to
|
||||
"anonymous fn in func/arity"
|
||||
"""
|
||||
def format_mfa(module, fun, arity) when is_atom(module) and is_atom(fun) do
|
||||
fun =
|
||||
case inspect(fun) do
|
||||
":" <> fun -> fun
|
||||
fun -> fun
|
||||
end
|
||||
|
||||
case match?("\"-" <> _, fun) and String.split(fun, "-") do
|
||||
[ "\"", outer_fun, "fun", _count, "\"" ] ->
|
||||
"anonymous fn#{format_arity(arity)} in #{inspect module}.#{outer_fun}"
|
||||
_ ->
|
||||
"#{inspect module}.#{fun}#{format_arity(arity)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp format_arity(arity) when is_list(arity) do
|
||||
inspected = for x <- arity, do: inspect(x)
|
||||
"(#{Enum.join(inspected, ", ")})"
|
||||
end
|
||||
|
||||
defp format_arity(arity) when is_integer(arity) do
|
||||
"/" <> Integer.to_string(arity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the given file and line as shown in stacktraces.
|
||||
If any of the values are nil, they are omitted.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Exception.format_file_line("foo", 1)
|
||||
"foo:1:"
|
||||
|
||||
iex> Exception.format_file_line("foo", nil)
|
||||
"foo:"
|
||||
|
||||
iex> Exception.format_file_line(nil, nil)
|
||||
""
|
||||
|
||||
"""
|
||||
def format_file_line(file, line) do
|
||||
format_file_line(file, line, "")
|
||||
end
|
||||
|
||||
defp format_file_line(file, line, suffix) do
|
||||
if file do
|
||||
if line && line != 0 do
|
||||
"#{file}:#{line}:#{suffix}"
|
||||
else
|
||||
"#{file}:#{suffix}"
|
||||
end
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
defp format_location(opts) when is_list(opts) do
|
||||
format_file_line Keyword.get(opts, :file), Keyword.get(opts, :line), " "
|
||||
end
|
||||
end
|
||||
|
||||
# Some exceptions implement `message/1` instead of `exception/1` mostly
|
||||
# for bootstrap reasons. It is recommended for applications to implement
|
||||
# `exception/1` instead of `message/1` as described in `defexception/1`
|
||||
# docs.
|
||||
|
||||
defmodule RuntimeError do
|
||||
defexception message: "runtime error"
|
||||
|
||||
@spec exception(String.t) :: Exception.t
|
||||
def exception(msg) when is_binary(msg) do
|
||||
%RuntimeError{message: msg}
|
||||
end
|
||||
|
||||
def exception(arg) do
|
||||
super(arg)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ArgumentError do
|
||||
defexception message: "argument error"
|
||||
|
||||
@spec exception(String.t) :: Exception.t
|
||||
def exception(msg) when is_binary(msg) do
|
||||
%ArgumentError{message: msg}
|
||||
end
|
||||
|
||||
def exception(arg) do
|
||||
super(arg)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ArithmeticError do
|
||||
defexception []
|
||||
|
||||
def message(_) do
|
||||
"bad argument in arithmetic expression"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule SystemLimitError do
|
||||
defexception []
|
||||
|
||||
def message(_) do
|
||||
"a system limit has been reached"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule SyntaxError do
|
||||
defexception [file: nil, line: nil, description: "syntax error"]
|
||||
|
||||
def message(exception) do
|
||||
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
|
||||
" " <> exception.description
|
||||
end
|
||||
end
|
||||
|
||||
defmodule TokenMissingError do
|
||||
defexception [file: nil, line: nil, description: "expression is incomplete"]
|
||||
|
||||
def message(exception) do
|
||||
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
|
||||
" " <> exception.description
|
||||
end
|
||||
end
|
||||
|
||||
defmodule CompileError do
|
||||
defexception [file: nil, line: nil, description: "compile error"]
|
||||
|
||||
def message(exception) do
|
||||
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
|
||||
" " <> exception.description
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadFunctionError do
|
||||
defexception [term: nil]
|
||||
|
||||
def message(exception) do
|
||||
"expected a function, got: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadStructError do
|
||||
defexception [struct: nil, term: nil]
|
||||
|
||||
def message(exception) do
|
||||
"expected a struct named #{inspect(exception.struct)}, got: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MatchError do
|
||||
defexception [term: nil]
|
||||
|
||||
def message(exception) do
|
||||
"no match of right hand side value: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule CaseClauseError do
|
||||
defexception [term: nil]
|
||||
|
||||
def message(exception) do
|
||||
"no case clause matching: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule CondClauseError do
|
||||
defexception []
|
||||
|
||||
def message(_exception) do
|
||||
"no cond clause evaluated to a true value"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule TryClauseError do
|
||||
defexception [term: nil]
|
||||
|
||||
def message(exception) do
|
||||
"no try clause matching: #{inspect(exception.term)}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadArityError do
|
||||
defexception [function: nil, args: nil]
|
||||
|
||||
def message(exception) do
|
||||
fun = exception.function
|
||||
args = exception.args
|
||||
insp = Enum.map_join(args, ", ", &inspect/1)
|
||||
{:arity, arity} = :erlang.fun_info(fun, :arity)
|
||||
"#{inspect(fun)} with arity #{arity} called with #{count(length(args), insp)}"
|
||||
end
|
||||
|
||||
defp count(0, _insp), do: "no arguments"
|
||||
defp count(1, insp), do: "1 argument (#{insp})"
|
||||
defp count(x, insp), do: "#{x} arguments (#{insp})"
|
||||
end
|
||||
|
||||
defmodule UndefinedFunctionError do
|
||||
defexception [module: nil, function: nil, arity: nil, self: false]
|
||||
|
||||
def message(%{function: function, module: module, arity: arity, self: self}) do
|
||||
if function do
|
||||
formatted = Exception.format_mfa module, function, arity
|
||||
suffix = if self or is_nil(module) or :code.is_loaded(module) do
|
||||
""
|
||||
else
|
||||
" (module #{inspect module} is not available)"
|
||||
end
|
||||
"undefined function: #{formatted}" <> suffix
|
||||
else
|
||||
"undefined function"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule FunctionClauseError do
|
||||
defexception [module: nil, function: nil, arity: nil]
|
||||
|
||||
def message(exception) do
|
||||
if exception.function do
|
||||
formatted = Exception.format_mfa exception.module, exception.function, exception.arity
|
||||
"no function clause matching in #{formatted}"
|
||||
else
|
||||
"no function clause matches"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Code.LoadError do
|
||||
defexception [:file, :message]
|
||||
|
||||
def exception(opts) do
|
||||
file = Keyword.fetch!(opts, :file)
|
||||
%Code.LoadError{message: "could not load #{file}", file: file}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Protocol.UndefinedError do
|
||||
defexception [protocol: nil, value: nil, description: nil]
|
||||
|
||||
def message(exception) do
|
||||
msg = "protocol #{inspect exception.protocol} not implemented for #{inspect exception.value}"
|
||||
if exception.description do
|
||||
msg <> ", " <> exception.description
|
||||
else
|
||||
msg
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule KeyError do
|
||||
defexception key: nil, term: nil
|
||||
|
||||
def message(exception) do
|
||||
"key #{inspect exception.key} not found in: #{inspect exception.term}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule UnicodeConversionError do
|
||||
defexception [:encoded, :message]
|
||||
|
||||
def exception(opts) do
|
||||
%UnicodeConversionError{
|
||||
encoded: Keyword.fetch!(opts, :encoded),
|
||||
message: "#{Keyword.fetch!(opts, :kind)} #{detail Keyword.fetch!(opts, :rest)}"
|
||||
}
|
||||
end
|
||||
|
||||
defp detail(rest) when is_binary(rest) do
|
||||
"encoding starting at #{inspect rest}"
|
||||
end
|
||||
|
||||
defp detail([h|_]) do
|
||||
"code point #{h}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Enum.OutOfBoundsError do
|
||||
defexception []
|
||||
|
||||
def message(_) do
|
||||
"out of bounds error"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Enum.EmptyError do
|
||||
defexception []
|
||||
|
||||
def message(_) do
|
||||
"empty error"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule File.Error do
|
||||
defexception [reason: nil, action: "", path: nil]
|
||||
|
||||
def message(exception) do
|
||||
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
|
||||
"could not #{exception.action} #{exception.path}: #{formatted}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule File.CopyError do
|
||||
defexception [reason: nil, action: "", source: nil, destination: nil, on: nil]
|
||||
|
||||
def message(exception) do
|
||||
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
|
||||
location = if on = exception.on, do: ". #{on}", else: ""
|
||||
"could not #{exception.action} from #{exception.source} to " <>
|
||||
"#{exception.destination}#{location}: #{formatted}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ErlangError do
|
||||
defexception [original: nil]
|
||||
|
||||
def message(exception) do
|
||||
"erlang error: #{inspect(exception.original)}"
|
||||
end
|
||||
|
||||
@doc false
|
||||
def normalize(:badarg, _stacktrace) do
|
||||
%ArgumentError{}
|
||||
end
|
||||
|
||||
def normalize(:badarith, _stacktrace) do
|
||||
%ArithmeticError{}
|
||||
end
|
||||
|
||||
def normalize(:system_limit, _stacktrace) do
|
||||
%SystemLimitError{}
|
||||
end
|
||||
|
||||
def normalize(:cond_clause, _stacktrace) do
|
||||
%CondClauseError{}
|
||||
end
|
||||
|
||||
def normalize({:badarity, {fun, args}}, _stacktrace) do
|
||||
%BadArityError{function: fun, args: args}
|
||||
end
|
||||
|
||||
def normalize({:badfun, term}, _stacktrace) do
|
||||
%BadFunctionError{term: term}
|
||||
end
|
||||
|
||||
def normalize({:badstruct, struct, term}, _stacktrace) do
|
||||
%BadStructError{struct: struct, term: term}
|
||||
end
|
||||
|
||||
def normalize({:badmatch, term}, _stacktrace) do
|
||||
%MatchError{term: term}
|
||||
end
|
||||
|
||||
def normalize({:case_clause, term}, _stacktrace) do
|
||||
%CaseClauseError{term: term}
|
||||
end
|
||||
|
||||
def normalize({:try_clause, term}, _stacktrace) do
|
||||
%TryClauseError{term: term}
|
||||
end
|
||||
|
||||
def normalize(:undef, stacktrace) do
|
||||
stacktrace = stacktrace || :erlang.get_stacktrace
|
||||
{mod, fun, arity} = from_stacktrace(stacktrace)
|
||||
%UndefinedFunctionError{module: mod, function: fun, arity: arity, self: from_self(stacktrace)}
|
||||
end
|
||||
|
||||
def normalize(:function_clause, stacktrace) do
|
||||
{mod, fun, arity} = from_stacktrace(stacktrace || :erlang.get_stacktrace)
|
||||
%FunctionClauseError{module: mod, function: fun, arity: arity}
|
||||
end
|
||||
|
||||
def normalize({:badarg, payload}, _stacktrace) do
|
||||
%ArgumentError{message: "argument error: #{inspect(payload)}"}
|
||||
end
|
||||
|
||||
def normalize(other, _stacktrace) do
|
||||
%ErlangError{original: other}
|
||||
end
|
||||
|
||||
defp from_stacktrace([{module, function, args, _}|_]) when is_list(args) do
|
||||
{module, function, length(args)}
|
||||
end
|
||||
|
||||
defp from_stacktrace([{module, function, arity, _}|_]) do
|
||||
{module, function, arity}
|
||||
end
|
||||
|
||||
defp from_stacktrace(_) do
|
||||
{nil, nil, nil}
|
||||
end
|
||||
|
||||
defp from_self([{module, _, _, _}, {module, _, _, _}|_]), do: true
|
||||
defp from_self(_), do: false
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,75 +0,0 @@
|
||||
require Record
|
||||
|
||||
defmodule File.Stat do
|
||||
@moduledoc """
|
||||
A struct responsible to hold file information.
|
||||
|
||||
In Erlang, this struct is represented by a `:file_info` record.
|
||||
Therefore this module also provides functions for converting
|
||||
in between the Erlang record and the Elixir struct.
|
||||
|
||||
Its fields are:
|
||||
|
||||
* `size` - size of file in bytes.
|
||||
|
||||
* `type` - `:device | :directory | :regular | :other`; the type of the
|
||||
file.
|
||||
|
||||
* `access` - `:read | :write | :read_write | :none`; the current system
|
||||
access to the file.
|
||||
|
||||
* `atime` - the last time the file was read.
|
||||
|
||||
* `mtime` - the last time the file was written.
|
||||
|
||||
* `ctime` - the interpretation of this time field depends on the operating
|
||||
system. On Unix, it is the last time the file or the inode was changed.
|
||||
In Windows, it is the time of creation.
|
||||
|
||||
* `mode` - the file permissions.
|
||||
|
||||
* `links` - the number of links to this file. This is always 1 for file
|
||||
systems which have no concept of links.
|
||||
|
||||
* `major_device` - identifies the file system where the file is located.
|
||||
In windows, the number indicates a drive as follows: 0 means A:, 1 means
|
||||
B:, and so on.
|
||||
|
||||
* `minor_device` - only valid for character devices on Unix. In all other
|
||||
cases, this field is zero.
|
||||
|
||||
* `inode` - gives the inode number. On non-Unix file systems, this field
|
||||
will be zero.
|
||||
|
||||
* `uid` - indicates the owner of the file.
|
||||
|
||||
* `gid` - gives the group that the owner of the file belongs to. Will be
|
||||
zero for non-Unix file systems.
|
||||
|
||||
The time type returned in `atime`, `mtime`, and `ctime` is dependent on the
|
||||
time type set in options. `{:time, type}` where type can be `:local`,
|
||||
`:universal`, or `:posix`. Default is `:local`.
|
||||
"""
|
||||
|
||||
record = Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
|
||||
keys = :lists.map(&elem(&1, 0), record)
|
||||
vals = :lists.map(&{&1, [], nil}, keys)
|
||||
pairs = :lists.zip(keys, vals)
|
||||
|
||||
defstruct keys
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@doc """
|
||||
Converts a `File.Stat` struct to a `:file_info` record.
|
||||
"""
|
||||
def to_record(%File.Stat{unquote_splicing(pairs)}) do
|
||||
{:file_info, unquote_splicing(vals)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `:file_info` record into a `File.Stat`.
|
||||
"""
|
||||
def from_record({:file_info, unquote_splicing(vals)}) do
|
||||
%File.Stat{unquote_splicing(pairs)}
|
||||
end
|
||||
end
|
||||
@@ -1,102 +0,0 @@
|
||||
defmodule File.Stream do
|
||||
@moduledoc """
|
||||
Defines a `File.Stream` struct returned by `File.stream!/3`.
|
||||
|
||||
The following fields are public:
|
||||
|
||||
* `path` - the file path
|
||||
* `modes` - the file modes
|
||||
* `raw` - a boolean indicating if bin functions should be used
|
||||
* `line_or_bytes` - if reading should read lines or a given amount of bytes
|
||||
|
||||
"""
|
||||
|
||||
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@doc false
|
||||
def __build__(path, modes, line_or_bytes) do
|
||||
raw = :lists.keyfind(:encoding, 1, modes) == false
|
||||
|
||||
modes =
|
||||
if raw do
|
||||
if :lists.keyfind(:read_ahead, 1, modes) == {:read_ahead, false} do
|
||||
[:raw|modes]
|
||||
else
|
||||
[:raw, :read_ahead|modes]
|
||||
end
|
||||
else
|
||||
modes
|
||||
end
|
||||
|
||||
%File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes}
|
||||
end
|
||||
|
||||
defimpl Collectable do
|
||||
def empty(stream) do
|
||||
stream
|
||||
end
|
||||
|
||||
def into(%{path: path, modes: modes, raw: raw} = stream) do
|
||||
modes = for mode <- modes, not mode in [:read], do: mode
|
||||
|
||||
case :file.open(path, [:write|modes]) do
|
||||
{:ok, device} ->
|
||||
{:ok, into(device, stream, raw)}
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
end
|
||||
end
|
||||
|
||||
defp into(device, stream, raw) do
|
||||
fn
|
||||
:ok, {:cont, x} ->
|
||||
case raw do
|
||||
true -> IO.binwrite(device, x)
|
||||
false -> IO.write(device, x)
|
||||
end
|
||||
:ok, :done ->
|
||||
# If delayed_write option is used and the last write failed will
|
||||
# MatchError here as {:error, _} is returned.
|
||||
:ok = :file.close(device)
|
||||
stream
|
||||
:ok, :halt ->
|
||||
# If delayed_write option is used and the last write failed will
|
||||
# MatchError here as {:error, _} is returned.
|
||||
:ok = :file.close(device)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enumerable do
|
||||
def reduce(%{path: path, modes: modes, line_or_bytes: line_or_bytes, raw: raw}, acc, fun) do
|
||||
modes = for mode <- modes, not mode in [:write, :append], do: mode
|
||||
|
||||
start_fun =
|
||||
fn ->
|
||||
case :file.open(path, modes) do
|
||||
{:ok, device} -> device
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
end
|
||||
end
|
||||
|
||||
next_fun =
|
||||
case raw do
|
||||
true -> &IO.each_binstream(&1, line_or_bytes)
|
||||
false -> &IO.each_stream(&1, line_or_bytes)
|
||||
end
|
||||
|
||||
Stream.resource(start_fun, next_fun, &:file.close/1).(acc, fun)
|
||||
end
|
||||
|
||||
def count(_stream) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
|
||||
def member?(_stream, _term) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,276 +0,0 @@
|
||||
import Kernel, except: [round: 1]
|
||||
|
||||
defmodule Float do
|
||||
@moduledoc """
|
||||
Functions for working with floating point numbers.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Parses a binary into a float.
|
||||
|
||||
If successful, returns a tuple of the form `{float, remainder_of_binary}`.
|
||||
Otherwise `:error`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.parse("34")
|
||||
{34.0,""}
|
||||
|
||||
iex> Float.parse("34.25")
|
||||
{34.25,""}
|
||||
|
||||
iex> Float.parse("56.5xyz")
|
||||
{56.5,"xyz"}
|
||||
|
||||
iex> Float.parse("pi")
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec parse(binary) :: {float, binary} | :error
|
||||
def parse("-" <> binary) do
|
||||
case parse_unsign(binary) do
|
||||
:error -> :error
|
||||
{number, remainder} -> {-number, remainder}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(binary) do
|
||||
parse_unsign(binary)
|
||||
end
|
||||
|
||||
defp parse_unsign("-" <> _), do: :error
|
||||
defp parse_unsign(binary) when is_binary(binary) do
|
||||
case Integer.parse binary do
|
||||
:error -> :error
|
||||
{integer_part, after_integer} -> parse_unsign after_integer, integer_part
|
||||
end
|
||||
end
|
||||
|
||||
# Dot followed by digit is required afterwards or we are done
|
||||
defp parse_unsign(<< ?., char, rest :: binary >>, int) when char in ?0..?9 do
|
||||
parse_unsign(rest, char - ?0, 1, int)
|
||||
end
|
||||
|
||||
defp parse_unsign(rest, int) do
|
||||
{:erlang.float(int), rest}
|
||||
end
|
||||
|
||||
# Handle decimal points
|
||||
defp parse_unsign(<< char, rest :: binary >>, float, decimal, int) when char in ?0..?9 do
|
||||
parse_unsign rest, 10 * float + (char - ?0), decimal + 1, int
|
||||
end
|
||||
|
||||
defp parse_unsign(<< ?e, after_e :: binary >>, float, decimal, int) do
|
||||
case Integer.parse after_e do
|
||||
:error ->
|
||||
# Note we rebuild the binary here instead of breaking it apart at
|
||||
# the function clause because the current approach copies a binary
|
||||
# just on this branch. If we broke it apart in the function clause,
|
||||
# the copy would happen when calling Integer.parse/1.
|
||||
{floatify(int, float, decimal), << ?e, after_e :: binary >>}
|
||||
{exponential, after_exponential} ->
|
||||
{floatify(int, float, decimal, exponential), after_exponential}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_unsign(bitstring, float, decimal, int) do
|
||||
{floatify(int, float, decimal), bitstring}
|
||||
end
|
||||
|
||||
defp floatify(int, float, decimal, exponential \\ 0) do
|
||||
multiplier = if int < 0, do: -1.0, else: 1.0
|
||||
|
||||
# Try to ensure the minimum amount of rounding errors
|
||||
result = multiplier * (abs(int) * :math.pow(10, decimal) + float) * :math.pow(10, exponential - decimal)
|
||||
|
||||
# Try avoiding stuff like this:
|
||||
# iex(1)> 0.0001 * 75
|
||||
# 0.007500000000000001
|
||||
# Due to IEEE 754 floating point standard
|
||||
# http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
||||
|
||||
final_decimal_places = decimal - exponential
|
||||
if final_decimal_places > 0 do
|
||||
decimal_power_round = :math.pow(10, final_decimal_places)
|
||||
trunc(result * decimal_power_round) / decimal_power_round
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest integer less than or equal to `num`.
|
||||
|
||||
Floor also accepts a precision to round a floating point value down
|
||||
to an arbitrary number of fractional digits (between 0 and 15).
|
||||
|
||||
This function always returns floats. One may use `Kernel.trunc/1` to
|
||||
truncate the result to an integer afterwards.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.floor(34.25)
|
||||
34.0
|
||||
|
||||
iex> Float.floor(-56.5)
|
||||
-57.0
|
||||
|
||||
iex> Float.floor(34.253, 2)
|
||||
34.25
|
||||
|
||||
"""
|
||||
@spec floor(float, 0..15) :: float
|
||||
def floor(number, precision \\ 0) when is_float(number) and precision in 0..15 do
|
||||
power = power_of_10(precision)
|
||||
number = number * power
|
||||
truncated = trunc(number)
|
||||
variance = if number - truncated < 0, do: -1.0, else: 0.0
|
||||
(truncated + variance) / power
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest integer greater than or equal to `num`.
|
||||
|
||||
Ceil also accepts a precision to round a floating point value down to
|
||||
an arbitrary number of fractional digits (between 0 and 15).
|
||||
|
||||
This function always returns floats. One may use `Kernel.trunc/1` to
|
||||
truncate the result to an integer afterwards.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.ceil(34.25)
|
||||
35.0
|
||||
|
||||
iex> Float.ceil(-56.5)
|
||||
-56.0
|
||||
|
||||
iex> Float.ceil(34.253, 2)
|
||||
34.26
|
||||
|
||||
"""
|
||||
@spec ceil(float, 0..15) :: float
|
||||
def ceil(number, precision \\ 0) when is_float(number) and precision in 0..15 do
|
||||
power = power_of_10(precision)
|
||||
number = number * power
|
||||
truncated = trunc(number)
|
||||
variance = if number - truncated > 0, do: 1.0, else: 0.0
|
||||
(truncated + variance) / power
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a floating point value to an arbitrary number of fractional digits
|
||||
(between 0 and 15).
|
||||
|
||||
This function only accepts floats and returns floats. Use `Kernel.round/1`
|
||||
if you want a function that accepts both floats and integers and always
|
||||
returns an integer.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.round(5.5674, 3)
|
||||
5.567
|
||||
|
||||
iex> Float.round(5.5675, 3)
|
||||
5.568
|
||||
|
||||
iex> Float.round(-5.5674, 3)
|
||||
-5.567
|
||||
|
||||
iex> Float.round(-5.5675, 3)
|
||||
-5.568
|
||||
|
||||
"""
|
||||
@spec round(float, 0..15) :: float
|
||||
def round(number, precision \\ 0) when is_float(number) and precision in 0..15 do
|
||||
power = power_of_10(precision)
|
||||
Kernel.round(number * power) / power
|
||||
end
|
||||
|
||||
Enum.reduce 0..15, 1, fn x, acc ->
|
||||
defp power_of_10(unquote(x)), do: unquote(acc)
|
||||
acc * 10
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a char list which corresponds to the text representation of the given float.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.to_char_list(7.0)
|
||||
'7.00000000000000000000e+00'
|
||||
|
||||
"""
|
||||
@spec to_char_list(float) :: char_list
|
||||
def to_char_list(number) do
|
||||
:erlang.float_to_list(number)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list which corresponds to the text representation
|
||||
of `float`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:decimals` — number of decimal points to show
|
||||
* `:scientific` — number of decimal points to show, in scientific format
|
||||
* `:compact` — when true, use the most compact representation (ignored
|
||||
with the `scientific` option)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.to_char_list 7.1, [decimals: 2, compact: true]
|
||||
'7.1'
|
||||
|
||||
"""
|
||||
@spec to_char_list(float, list) :: char_list
|
||||
def to_char_list(float, options) do
|
||||
:erlang.float_to_list(float, expand_compact(options))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a binary which corresponds to the text representation
|
||||
of `some_float`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.to_string(7.0)
|
||||
"7.00000000000000000000e+00"
|
||||
|
||||
"""
|
||||
@spec to_string(float) :: String.t
|
||||
def to_string(some_float) do
|
||||
:erlang.float_to_binary(some_float)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a binary which corresponds to the text representation
|
||||
of `float`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:decimals` — number of decimal points to show
|
||||
* `:scientific` — number of decimal points to show, in scientific format
|
||||
* `:compact` — when true, use the most compact representation (ignored
|
||||
with the `scientific` option)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.to_string 7.1, [decimals: 2, compact: true]
|
||||
"7.1"
|
||||
|
||||
"""
|
||||
@spec to_string(float, list) :: String.t
|
||||
def to_string(float, options) do
|
||||
:erlang.float_to_binary(float, expand_compact(options))
|
||||
end
|
||||
|
||||
defp expand_compact([{:compact, false}|t]), do: expand_compact(t)
|
||||
defp expand_compact([{:compact, true}|t]), do: [:compact|expand_compact(t)]
|
||||
defp expand_compact([h|t]), do: [h|expand_compact(t)]
|
||||
defp expand_compact([]), do: []
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,168 +0,0 @@
|
||||
defmodule GenEvent.Stream do
|
||||
@moduledoc """
|
||||
Defines a `GenEvent` stream.
|
||||
|
||||
This is a struct returned by `stream/2`. The struct is public and
|
||||
contains the following fields:
|
||||
|
||||
* `:manager` - the manager reference given to `GenEvent.stream/2`
|
||||
* `:timeout` - the timeout in between events, defaults to `:infinity`
|
||||
|
||||
"""
|
||||
defstruct manager: nil, timeout: :infinity
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
manager: GenEvent.manager,
|
||||
timeout: timeout}
|
||||
|
||||
@doc false
|
||||
def init({_pid, _ref} = state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_event(event, _state) do
|
||||
exit({:bad_event, event})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(msg, _state) do
|
||||
exit({:bad_call, msg})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def terminate(_reason, _state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc false
|
||||
def code_change(_old, state, _extra) do
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: GenEvent.Stream do
|
||||
def reduce(stream, acc, fun) do
|
||||
start_fun = fn() -> start(stream) end
|
||||
next_fun = &next(stream, &1)
|
||||
stop_fun = &stop(stream, &1)
|
||||
Stream.resource(start_fun, next_fun, stop_fun).(acc, wrap_reducer(fun))
|
||||
end
|
||||
|
||||
def count(_stream) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
|
||||
def member?(_stream, _item) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
|
||||
defp wrap_reducer(fun) do
|
||||
fn
|
||||
{:ack, manager, ref, event}, acc ->
|
||||
send manager, {ref, :ok}
|
||||
fun.(event, acc)
|
||||
{:async, _manager, _ref, event}, acc ->
|
||||
fun.(event, acc)
|
||||
{:sync, manager, ref, event}, acc ->
|
||||
try do
|
||||
fun.(event, acc)
|
||||
after
|
||||
send manager, {ref, :ok}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp start(%{manager: manager} = stream) do
|
||||
try do
|
||||
{:ok, {pid, ref}} = :gen.call(manager, self(),
|
||||
{:add_process_handler, self(), self()}, :infinity)
|
||||
mon_ref = Process.monitor(pid)
|
||||
{pid, ref, mon_ref}
|
||||
catch
|
||||
:exit, reason -> exit({reason, {__MODULE__, :start, [stream]}})
|
||||
end
|
||||
end
|
||||
|
||||
defp next(%{timeout: timeout} = stream, {pid, ref, mon_ref} = acc) do
|
||||
self = self()
|
||||
|
||||
receive do
|
||||
# Got an async event.
|
||||
{_from, {^pid, ^ref}, {:notify, event}} ->
|
||||
{[{:async, pid, ref, event}], acc}
|
||||
|
||||
# Got a sync event.
|
||||
{_from, {^pid, ^ref}, {:sync_notify, event}} ->
|
||||
{[{:sync, pid, ref, event}], acc}
|
||||
|
||||
# Got an ack event.
|
||||
{_from, {^pid, ^ref}, {:ack_notify, event}} ->
|
||||
{[{:ack, pid, ref, event}], acc}
|
||||
|
||||
# The handler was removed. Stop iteration, resolve the
|
||||
# event later. We need to demonitor now, otherwise DOWN
|
||||
# appears with higher priority in the shutdown process.
|
||||
{:gen_event_EXIT, {^pid, ^ref}, _reason} = event ->
|
||||
Process.demonitor(mon_ref, [:flush])
|
||||
send(self, event)
|
||||
{:halt, {:removed, acc}}
|
||||
|
||||
# The manager died. Stop iteration, resolve the event later.
|
||||
{:DOWN, ^mon_ref, _, _, _} = event ->
|
||||
send(self, event)
|
||||
{:halt, {:removed, acc}}
|
||||
after
|
||||
timeout ->
|
||||
exit({:timeout, {__MODULE__, :next, [stream, acc]}})
|
||||
end
|
||||
end
|
||||
|
||||
# If we reach this branch, we know the handler was already
|
||||
# removed, so we don't trigger a request for doing so.
|
||||
defp stop(stream, {:removed, {pid, ref, mon_ref} = acc}) do
|
||||
case wait_for_handler_removal(pid, ref, mon_ref) do
|
||||
:ok ->
|
||||
flush_events(ref)
|
||||
{:error, reason} ->
|
||||
exit({reason, {__MODULE__, :stop, [stream, acc]}})
|
||||
end
|
||||
end
|
||||
|
||||
# If we reach this branch, the handler was not removed yet,
|
||||
# so we trigger a request for doing so.
|
||||
defp stop(stream, {pid, ref, _} = acc) do
|
||||
_ = GenEvent.remove_handler(pid, {pid, ref}, :shutdown)
|
||||
stop(stream, {:removed, acc})
|
||||
end
|
||||
|
||||
defp wait_for_handler_removal(pid, ref, mon_ref) do
|
||||
receive do
|
||||
{:gen_event_EXIT, {^pid, ^ref}, reason}
|
||||
when reason == :normal
|
||||
when reason == :shutdown
|
||||
when tuple_size(reason) == 3 and elem(reason, 0) == :swapped ->
|
||||
Process.demonitor(mon_ref, [:flush])
|
||||
:ok
|
||||
{:gen_event_EXIT, {^pid, ^ref}, reason} ->
|
||||
Process.demonitor(mon_ref, [:flush])
|
||||
{:error, reason}
|
||||
{:DOWN, ^mon_ref, _, _, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp flush_events(ref) do
|
||||
receive do
|
||||
{_from, {_pid, ^ref}, {notify, _event}} when notify in [:notify, :ack_notify, :sync_notify] ->
|
||||
flush_events(ref)
|
||||
after
|
||||
0 -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,480 +0,0 @@
|
||||
defmodule GenServer do
|
||||
@moduledoc """
|
||||
A behaviour module for implementing the server of a client-server relation.
|
||||
|
||||
A GenServer is a process as any other Elixir process and it can be used
|
||||
to keep state, execute code asynchronously and so on. The advantage of using
|
||||
a generic server process (GenServer) implemented using this module is that it
|
||||
will have a standard set of interface functions and include functionality for
|
||||
tracing and error reporting. It will also fit into a supervision tree.
|
||||
|
||||
## Example
|
||||
|
||||
The GenServer behaviour abstracts the common client-server interaction.
|
||||
Developers are only required to implement the callbacks and functionality they are
|
||||
interested in.
|
||||
|
||||
Let's start with a code example and then explore the available callbacks.
|
||||
Imagine we want a GenServer that works like a stack, allowing us to push
|
||||
and pop items:
|
||||
|
||||
defmodule Stack do
|
||||
use GenServer
|
||||
|
||||
# Callbacks
|
||||
|
||||
def handle_call(:pop, _from, [h|t]) do
|
||||
{:reply, h, t}
|
||||
end
|
||||
|
||||
def handle_cast({:push, item}, state) do
|
||||
{:noreply, [item|state]}
|
||||
end
|
||||
end
|
||||
|
||||
# Start the server
|
||||
{:ok, pid} = GenServer.start_link(Stack, [:hello])
|
||||
|
||||
# This is the client
|
||||
GenServer.call(pid, :pop)
|
||||
#=> :hello
|
||||
|
||||
GenServer.cast(pid, {:push, :world})
|
||||
#=> :ok
|
||||
|
||||
GenServer.call(pid, :pop)
|
||||
#=> :world
|
||||
|
||||
We start our `Stack` by calling `start_link/3`, passing the module
|
||||
with the server implementation and its initial argument (a list
|
||||
representing the stack containing the item `:hello`). We can primarily
|
||||
interact with the server by sending two types of messages. **call**
|
||||
messages expect a reply from the server (and are therefore synchronous)
|
||||
while **cast** messages do not.
|
||||
|
||||
Every time you do a `GenServer.call/3`, the client will send a message
|
||||
that must be handled by the `handle_call/3` callback in the GenServer.
|
||||
A `cast/2` message must be handled by `handle_cast/2`.
|
||||
|
||||
## Callbacks
|
||||
|
||||
There are 6 callbacks required to be implemented in a `GenServer`. By
|
||||
adding `use GenServer` to your module, Elixir will automatically define
|
||||
all 6 callbacks for you, leaving it up to you to implement the ones
|
||||
you want to customize. The callbacks are:
|
||||
|
||||
* `init(args)` - invoked when the server is started.
|
||||
|
||||
It must return:
|
||||
|
||||
- `{:ok, state}`
|
||||
- `{:ok, state, timeout}`
|
||||
- `:ignore`
|
||||
- `{:stop, reason}`
|
||||
|
||||
* `handle_call(msg, {from, ref}, state)` and `handle_cast(msg, state)` -
|
||||
invoked to handle call (sync) and cast (async) messages.
|
||||
|
||||
It must return:
|
||||
|
||||
- `{:reply, reply, new_state}`
|
||||
- `{:reply, reply, new_state, timeout}`
|
||||
- `{:reply, reply, new_state, :hibernate}`
|
||||
- `{:noreply, new_state}`
|
||||
- `{:noreply, new_state, timeout}`
|
||||
- `{:noreply, new_state, :hibernate}`
|
||||
- `{:stop, reason, new_state}`
|
||||
- `{:stop, reason, reply, new_state}`
|
||||
|
||||
* `handle_info(msg, state)` - invoked to handle all other messages which
|
||||
are received by the process.
|
||||
|
||||
It must return:
|
||||
|
||||
- `{:noreply, state}`
|
||||
- `{:noreply, state, timeout}`
|
||||
- `{:stop, reason, state}`
|
||||
|
||||
* `terminate(reason, state)` - called when the server is about to
|
||||
terminate, useful for cleaning up. It must return `:ok`.
|
||||
|
||||
* `code_change(old_vsn, state, extra)` - called when the application
|
||||
code is being upgraded live (hot code swapping).
|
||||
|
||||
It must return:
|
||||
|
||||
- `{:ok, new_state}`
|
||||
- `{:error, reason}`
|
||||
|
||||
## Name Registration
|
||||
|
||||
Both `start_link/3` and `start/3` support the `GenServer` to register
|
||||
a name on start via the `:name` option. Registered names are also
|
||||
automatically cleaned up on termination. The supported values are:
|
||||
|
||||
* an atom - the GenServer is registered locally with the given name
|
||||
using `Process.register/2`.
|
||||
|
||||
* `{:global, term}`- the GenServer is registered globally with the given
|
||||
term using the functions in the `:global` module.
|
||||
|
||||
* `{:via, module, term}` - the GenServer is registered with the given
|
||||
mechanism and name. The `:via` option expects a module name to control
|
||||
the registration mechanism alongside a name which can be any term.
|
||||
|
||||
For example, we could start and register our Stack server locally as follows:
|
||||
|
||||
# Start the server and register it locally with name MyStack
|
||||
{:ok, _} = GenServer.start_link(Stack, [:hello], name: MyStack)
|
||||
|
||||
# Now messages can be sent directly to MyStack
|
||||
GenServer.call(MyStack, :pop) #=> :hello
|
||||
|
||||
Once the server is started, the remaining functions in this module (`call/3`,
|
||||
`cast/2`, and friends) will also accept an atom, or any `:global` or `:via`
|
||||
tuples. In general, the following formats are supported:
|
||||
|
||||
* a `pid`
|
||||
* an `atom` if the server is locally registered
|
||||
* `{atom, node}` if the server is locally registered at another node
|
||||
* `{:global, term}` if the server is globally registered
|
||||
* `{:via, module, name}` if the server is registered through an alternative
|
||||
registry
|
||||
|
||||
## Client / Server APIs
|
||||
|
||||
Although in the example above we have used `GenServer.start_link/3` and
|
||||
friends to directly start and communicate with the server, most of the
|
||||
time we don't call the `GenServer` functions directly. Instead, we wrap
|
||||
the calls in new functions representing the public API of the server.
|
||||
|
||||
Here is a better implementation of our Stack module:
|
||||
|
||||
defmodule Stack do
|
||||
use GenServer
|
||||
|
||||
# Client
|
||||
|
||||
def start_link(default) do
|
||||
GenServer.start_link(__MODULE__, default)
|
||||
end
|
||||
|
||||
def push(pid, item) do
|
||||
GenServer.cast(pid, {:push, item})
|
||||
end
|
||||
|
||||
def pop(pid) do
|
||||
GenServer.call(pid, :pop)
|
||||
end
|
||||
|
||||
# Server (callbacks)
|
||||
|
||||
def handle_call(:pop, _from, [h|t]) do
|
||||
{:reply, h, t}
|
||||
end
|
||||
|
||||
def handle_call(request, from, state) do
|
||||
# Call the default implementation from GenServer
|
||||
super(request, from, state)
|
||||
end
|
||||
|
||||
def handle_cast({:push, item}, state) do
|
||||
{:noreply, [item|state]}
|
||||
end
|
||||
|
||||
def handle_cast(request, state) do
|
||||
super(request, state)
|
||||
end
|
||||
end
|
||||
|
||||
In practice, it is common to have both server and client functions in
|
||||
the same module. If the server and/or client implementations are growing
|
||||
complex, you may want to have them in different modules.
|
||||
|
||||
## Learn more
|
||||
|
||||
If you wish to find out more about gen servers, Elixir getting started
|
||||
guides provide a tutorial-like introduction. The documentation and links
|
||||
in Erlang can also provide extra insight.
|
||||
|
||||
* http://elixir-lang.org/getting_started/mix/1.html
|
||||
* http://www.erlang.org/doc/man/gen_server.html
|
||||
* http://www.erlang.org/doc/design_principles/gen_server_concepts.html
|
||||
* http://learnyousomeerlang.com/clients-and-servers
|
||||
"""
|
||||
|
||||
@typedoc "Return values of `start*` functions"
|
||||
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
|
||||
|
||||
@typedoc "The GenServer name"
|
||||
@type name :: atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@typedoc "Options used by the `start*` functions"
|
||||
@type options :: [debug: debug,
|
||||
name: name,
|
||||
timeout: timeout,
|
||||
spawn_opt: Process.spawn_opt]
|
||||
|
||||
@typedoc "debug options supported by the `start*` functions"
|
||||
@type debug :: [:trace | :log | :statistics | {:log_to_file, Path.t}]
|
||||
|
||||
@typedoc "The server reference"
|
||||
@type server :: pid | name | {atom, node}
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
@behaviour :gen_server
|
||||
|
||||
@doc false
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(msg, _from, state) do
|
||||
{:stop, {:bad_call, msg}, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_cast(msg, state) do
|
||||
{:stop, {:bad_cast, msg}, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def terminate(_reason, _state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc false
|
||||
def code_change(_old, state, _extra) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defoverridable [init: 1, handle_call: 3, handle_info: 2,
|
||||
handle_cast: 2, terminate: 2, code_change: 3]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a `GenServer` process linked to the current process.
|
||||
|
||||
This is often used to start the `GenServer` as part of a supervision tree.
|
||||
|
||||
Once the server is started, it calls the `init/1` function in the given `module`
|
||||
passing the given `args` to initialize it. To ensure a synchronized start-up
|
||||
procedure, this function does not return until `init/1` has returned.
|
||||
|
||||
Note that a `GenServer` started with `start_link/3` is linked to the
|
||||
parent process and will exit in case of crashes. The GenServer will also
|
||||
exit due to the `:normal` reasons in case it is configured to trap exits
|
||||
in the `init/1` callback.
|
||||
|
||||
## Options
|
||||
|
||||
The `:name` option is used for name registration as described in the module
|
||||
documentation. If the option `:timeout` option is present, the server is
|
||||
allowed to spend the given milliseconds initializing or it will be
|
||||
terminated and the start function will return `{:error, :timeout}`.
|
||||
|
||||
If the `:debug` option is present, the corresponding function in the
|
||||
[`:sys` module](http://www.erlang.org/doc/man/sys.html) will be invoked.
|
||||
|
||||
If the `:spawn_opt` option is present, its value will be passed as options
|
||||
to the underlying process as in `Process.spawn/4`.
|
||||
|
||||
## Return values
|
||||
|
||||
If the server is successfully created and initialized, the function returns
|
||||
`{:ok, pid}`, where pid is the pid of the server. If there already exists a
|
||||
process with the specified server name, the function returns
|
||||
`{:error, {:already_started, pid}}` with the pid of that process.
|
||||
|
||||
If the `init/1` callback fails with `reason`, the function returns
|
||||
`{:error, reason}`. Otherwise, if it returns `{:stop, reason}`
|
||||
or `:ignore`, the process is terminated and the function returns
|
||||
`{:error, reason}` or `:ignore`, respectively.
|
||||
"""
|
||||
@spec start_link(module, any, options) :: on_start
|
||||
def start_link(module, args, options \\ []) when is_atom(module) and is_list(options) do
|
||||
do_start(:link, module, args, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a `GenServer` process without links (outside of a supervision tree).
|
||||
|
||||
See `start_link/3` for more information.
|
||||
"""
|
||||
@spec start(module, any, options) :: on_start
|
||||
def start(module, args, options \\ []) when is_atom(module) and is_list(options) do
|
||||
do_start(:nolink, module, args, options)
|
||||
end
|
||||
|
||||
defp do_start(link, module, args, options) do
|
||||
case Keyword.pop(options, :name) do
|
||||
{nil, opts} ->
|
||||
:gen.start(:gen_server, link, module, args, opts)
|
||||
{atom, opts} when is_atom(atom) ->
|
||||
:gen.start(:gen_server, link, {:local, atom}, module, args, opts)
|
||||
{other, opts} when is_tuple(other) ->
|
||||
:gen.start(:gen_server, link, other, module, args, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Makes a synchronous call to the `server` and waits for its reply.
|
||||
|
||||
The client sends the given `request` to the server and waits until a reply
|
||||
arrives or a timeout occurs. `handle_call/3` will be called on the server
|
||||
to handle the request.
|
||||
|
||||
The server can be any of the values described in the `Name Registration`
|
||||
section of the module documentation.
|
||||
|
||||
## Timeouts
|
||||
|
||||
The `timeout` is an integer greater than zero which specifies how many
|
||||
milliseconds to wait for a reply, or the atom `:infinity` to wait
|
||||
indefinitely. The default value is 5000. If no reply is received within
|
||||
the specified time, the function call fails. If the caller catches the
|
||||
failure and continues running, and the server is just late with the reply,
|
||||
it may arrive at any time later into the caller's message queue. The caller
|
||||
must in this case be prepared for this and discard any such garbage messages
|
||||
that are two element tuples with a reference as the first element.
|
||||
"""
|
||||
@spec call(server, term, timeout) :: term
|
||||
def call(server, request, timeout \\ 5000) do
|
||||
try do
|
||||
:gen.call(server, :"$gen_call", request, timeout)
|
||||
catch
|
||||
:exit, reason ->
|
||||
exit({reason, {__MODULE__, :call, [server, request, timeout]}})
|
||||
else
|
||||
{:ok, res} -> res
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an asynchronous request to the `server`.
|
||||
|
||||
This function returns `:ok` immediately, regardless of
|
||||
whether the destination node or server does exists, unless
|
||||
the server is specified as an atom.
|
||||
|
||||
`handle_cast/2` will be called on the server to handle
|
||||
the request. In case the server is a node which is not
|
||||
yet connected to the caller one, the call is going to
|
||||
block until a connection happens. This is different than
|
||||
the behaviour in OTP's `:gen_server` where the message
|
||||
would be sent by another process, which could cause
|
||||
messages to arrive out of order.
|
||||
"""
|
||||
@spec cast(server, term) :: :ok
|
||||
def cast(server, request)
|
||||
|
||||
def cast({:global, name}, request) do
|
||||
try do
|
||||
:global.send(name, cast_msg(request))
|
||||
:ok
|
||||
catch
|
||||
_, _ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def cast({:via, mod, name}, request) do
|
||||
try do
|
||||
mod.send(name, cast_msg(request))
|
||||
:ok
|
||||
catch
|
||||
_, _ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def cast({name, node}, request) when is_atom(name) and is_atom(node),
|
||||
do: do_send({name, node}, cast_msg(request))
|
||||
|
||||
def cast(dest, request) when is_atom(dest) or is_pid(dest),
|
||||
do: do_send(dest, cast_msg(request))
|
||||
|
||||
@doc """
|
||||
Casts all servers locally registered as `name` at the specified nodes.
|
||||
|
||||
The function returns immediately and ignores nodes that do not exist, or where the
|
||||
server name does not exist.
|
||||
|
||||
See `multi_call/4` for more information.
|
||||
"""
|
||||
@spec abcast([node], name :: atom, term) :: :abcast
|
||||
def abcast(nodes \\ nodes(), name, request) when is_list(nodes) and is_atom(name) do
|
||||
msg = cast_msg(request)
|
||||
_ = for node <- nodes, do: do_send({name, node}, msg)
|
||||
:abcast
|
||||
end
|
||||
|
||||
defp cast_msg(req) do
|
||||
{:"$gen_cast", req}
|
||||
end
|
||||
|
||||
defp do_send(dest, msg) do
|
||||
send(dest, msg)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calls all servers locally registered as `name` at the specified `nodes`.
|
||||
|
||||
The `request` is first sent to every node and then we wait for the
|
||||
replies. This function returns a tuple containing the node and its reply
|
||||
as first element and all bad nodes as second element. The bad nodes is a
|
||||
list of nodes that either did not exist, or where a server with the given
|
||||
`name` did not exist or did not reply.
|
||||
|
||||
Nodes is a list of node names to which the request is sent. The default
|
||||
value is the list of all known nodes.
|
||||
|
||||
To avoid that late answers (after the timeout) pollute the caller's message
|
||||
queue, a middleman process is used to do the actual calls. Late answers will
|
||||
then be discarded when they arrive to a terminated process.
|
||||
"""
|
||||
@spec multi_call([node], name :: atom, term, timeout) ::
|
||||
{replies :: [{node, term}], bad_nodes :: [node]}
|
||||
def multi_call(nodes \\ nodes(), name, request, timeout \\ :infinity) do
|
||||
:gen_server.multi_call(nodes, name, request, timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Replies to a client.
|
||||
|
||||
This function can be used by a server to explicitly send a reply to a
|
||||
client that called `call/3` or `multi_call/4`. When the reply cannot be
|
||||
defined in the return value of `handle_call/3`.
|
||||
|
||||
The `client` must be the `from` argument (the second argument) received
|
||||
in `handle_call/3` callbacks. Reply is an arbitrary term which will be
|
||||
given back to the client as the return value of the call.
|
||||
|
||||
This function always returns `:ok`.
|
||||
"""
|
||||
@spec reply({pid, reference}, term) :: :ok
|
||||
def reply(client, reply)
|
||||
|
||||
def reply({to, tag}, reply) do
|
||||
try do
|
||||
send(to, {tag, reply})
|
||||
:ok
|
||||
catch
|
||||
_, _ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, [nodes: 0]}
|
||||
|
||||
defp nodes do
|
||||
[node()|:erlang.nodes()]
|
||||
end
|
||||
end
|
||||
@@ -1,267 +0,0 @@
|
||||
defmodule HashDict do
|
||||
@moduledoc """
|
||||
A key-value store.
|
||||
|
||||
The `HashDict` is represented internally as a struct, therefore
|
||||
`%HashDict{}` can be used whenever there is a need to match
|
||||
on any `HashDict`. Note though the struct fields are private and
|
||||
must not be accessed directly. Instead, use the functions on this
|
||||
or in the `Dict` module.
|
||||
|
||||
Implementation-wise, `HashDict` is implemented using tries, which
|
||||
grows in space as the number of keys grows, working well with both
|
||||
small and large set of keys. For more information about the
|
||||
functions and their APIs, please consult the `Dict` module.
|
||||
"""
|
||||
|
||||
use Dict
|
||||
|
||||
@node_bitmap 0b111
|
||||
@node_shift 3
|
||||
@node_size 8
|
||||
@node_template :erlang.make_tuple(@node_size, [])
|
||||
|
||||
@opaque t :: %__MODULE__{size: non_neg_integer, root: term}
|
||||
@doc false
|
||||
defstruct size: 0, root: @node_template
|
||||
|
||||
# Inline common instructions
|
||||
@compile :inline_list_funcs
|
||||
@compile {:inline, key_hash: 1, key_mask: 1, key_shift: 1}
|
||||
|
||||
@doc """
|
||||
Creates a new empty dict.
|
||||
"""
|
||||
@spec new :: Dict.t
|
||||
def new do
|
||||
%HashDict{}
|
||||
end
|
||||
|
||||
def put(%HashDict{root: root, size: size}, key, value) do
|
||||
{root, counter} = do_put(root, key, value, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
def update!(%HashDict{root: root, size: size} = dict, key, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> raise KeyError, key: key, term: dict end,
|
||||
fun, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
def update(%HashDict{root: root, size: size}, key, initial, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> initial end, fun, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
def fetch(%HashDict{root: root}, key) do
|
||||
do_fetch(root, key, key_hash(key))
|
||||
end
|
||||
|
||||
def delete(dict, key) do
|
||||
case dict_delete(dict, key) do
|
||||
{dict, _value} -> dict
|
||||
:error -> dict
|
||||
end
|
||||
end
|
||||
|
||||
def pop(dict, key, default \\ nil) do
|
||||
case dict_delete(dict, key) do
|
||||
{dict, value} -> {value, dict}
|
||||
:error -> {default, dict}
|
||||
end
|
||||
end
|
||||
|
||||
def size(%HashDict{size: size}) do
|
||||
size
|
||||
end
|
||||
|
||||
@doc false
|
||||
def reduce(%HashDict{root: root}, acc, fun) do
|
||||
do_reduce(root, acc, fun, @node_size, fn
|
||||
{:suspend, acc} -> {:suspended, acc, &{:done, elem(&1, 1)}}
|
||||
{:halt, acc} -> {:halted, acc}
|
||||
{:cont, acc} -> {:done, acc}
|
||||
end)
|
||||
end
|
||||
|
||||
## General helpers
|
||||
|
||||
@doc false
|
||||
def dict_delete(%HashDict{root: root, size: size}, key) do
|
||||
case do_delete(root, key, key_hash(key)) do
|
||||
{root, value} -> {%HashDict{root: root, size: size - 1}, value}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
## Dict manipulation
|
||||
|
||||
defp do_fetch(node, key, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[^key|v] -> {:ok, v}
|
||||
{^key, v, _} -> {:ok, v}
|
||||
{_, _, n} -> do_fetch(n, key, key_shift(hash))
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp do_put(node, key, value, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{put_elem(node, index, [key|value]), 1}
|
||||
[^key|_] ->
|
||||
{put_elem(node, index, [key|value]), 0}
|
||||
[k|v] ->
|
||||
n = put_elem(@node_template, key_mask(key_shift(hash)), [key|value])
|
||||
{put_elem(node, index, {k, v, n}), 1}
|
||||
{^key, _, n} ->
|
||||
{put_elem(node, index, {key, value, n}), 0}
|
||||
{k, v, n} ->
|
||||
{n, counter} = do_put(n, key, value, key_shift(hash))
|
||||
{put_elem(node, index, {k, v, n}), counter}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update(node, key, initial, fun, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{put_elem(node, index, [key|initial.()]), 1}
|
||||
[^key|value] ->
|
||||
{put_elem(node, index, [key|fun.(value)]), 0}
|
||||
[k|v] ->
|
||||
n = put_elem(@node_template, key_mask(key_shift(hash)), [key|initial.()])
|
||||
{put_elem(node, index, {k, v, n}), 1}
|
||||
{^key, value, n} ->
|
||||
{put_elem(node, index, {key, fun.(value), n}), 0}
|
||||
{k, v, n} ->
|
||||
{n, counter} = do_update(n, key, initial, fun, key_shift(hash))
|
||||
{put_elem(node, index, {k, v, n}), counter}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_delete(node, key, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
:error
|
||||
[^key|value] ->
|
||||
{put_elem(node, index, []), value}
|
||||
[_|_] ->
|
||||
:error
|
||||
{^key, value, n} ->
|
||||
{put_elem(node, index, do_compact_node(n)), value}
|
||||
{k, v, n} ->
|
||||
case do_delete(n, key, key_shift(hash)) do
|
||||
{@node_template, value} ->
|
||||
{put_elem(node, index, [k|v]), value}
|
||||
{n, value} ->
|
||||
{put_elem(node, index, {k, v, n}), value}
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Enum.each 0..(@node_size - 1), fn index ->
|
||||
defp do_compact_node(node) when elem(node, unquote(index)) != [] do
|
||||
case elem(node, unquote(index)) do
|
||||
[k|v] ->
|
||||
case put_elem(node, unquote(index), []) do
|
||||
@node_template -> [k|v]
|
||||
n -> {k, v, n}
|
||||
end
|
||||
{k, v, n} ->
|
||||
{k, v, put_elem(node, unquote(index), do_compact_node(n))}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Dict reduce
|
||||
|
||||
defp do_reduce_each(_node, {:halt, acc}, _fun, _next) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp do_reduce_each(node, {:suspend, acc}, fun, next) do
|
||||
{:suspended, acc, &do_reduce_each(node, &1, fun, next)}
|
||||
end
|
||||
|
||||
defp do_reduce_each([], acc, _fun, next) do
|
||||
next.(acc)
|
||||
end
|
||||
|
||||
defp do_reduce_each([k|v], {:cont, acc}, fun, next) do
|
||||
next.(fun.({k,v}, acc))
|
||||
end
|
||||
|
||||
defp do_reduce_each({k, v, n}, {:cont, acc}, fun, next) do
|
||||
do_reduce(n, fun.({k, v}, acc), fun, @node_size, next)
|
||||
end
|
||||
|
||||
defp do_reduce(node, acc, fun, count, next) when count > 0 do
|
||||
do_reduce_each(:erlang.element(count, node), acc, fun, &do_reduce(node, &1, fun, count - 1, next))
|
||||
end
|
||||
|
||||
defp do_reduce(_node, acc, _fun, 0, next) do
|
||||
next.(acc)
|
||||
end
|
||||
|
||||
## Key operations
|
||||
|
||||
import Bitwise
|
||||
|
||||
defp key_hash(key) do
|
||||
:erlang.phash2(key)
|
||||
end
|
||||
|
||||
defp key_mask(hash) do
|
||||
hash &&& @node_bitmap
|
||||
end
|
||||
|
||||
defp key_shift(hash) do
|
||||
hash >>> @node_shift
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: HashDict do
|
||||
def reduce(dict, acc, fun), do: HashDict.reduce(dict, acc, fun)
|
||||
def member?(dict, {k, v}), do: {:ok, match?({:ok, ^v}, HashDict.fetch(dict, k))}
|
||||
def member?(_dict, _), do: {:ok, false}
|
||||
def count(dict), do: {:ok, HashDict.size(dict)}
|
||||
end
|
||||
|
||||
defimpl Access, for: HashDict do
|
||||
def get(dict, key) do
|
||||
HashDict.get(dict, key, nil)
|
||||
end
|
||||
|
||||
def get_and_update(dict, key, fun) do
|
||||
{get, update} = fun.(HashDict.get(dict, key, nil))
|
||||
{get, HashDict.put(dict, key, update)}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: HashDict do
|
||||
def empty(_dict) do
|
||||
HashDict.new
|
||||
end
|
||||
|
||||
def into(original) do
|
||||
{original, fn
|
||||
dict, {:cont, {k, v}} -> Dict.put(dict, k, v)
|
||||
dict, :done -> dict
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: HashDict do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(dict, opts) do
|
||||
concat ["#HashDict<", Inspect.List.inspect(HashDict.to_list(dict), opts), ">"]
|
||||
end
|
||||
end
|
||||
@@ -1,275 +0,0 @@
|
||||
defmodule HashSet do
|
||||
@moduledoc """
|
||||
A set store.
|
||||
|
||||
The `HashSet` is represented internally as a struct, therefore
|
||||
`%HashSet{}` can be used whenever there is a need to match
|
||||
on any `HashSet`. Note though the struct fields are private and
|
||||
must not be accessed directly. Instead, use the functions on this
|
||||
or in the `Set` module.
|
||||
|
||||
The `HashSet` is implemented using tries, which grows in
|
||||
space as the number of keys grows, working well with both
|
||||
small and large set of keys. For more information about the
|
||||
functions and their APIs, please consult the `Set` module.
|
||||
"""
|
||||
|
||||
@behaviour Set
|
||||
|
||||
@node_bitmap 0b111
|
||||
@node_shift 3
|
||||
@node_size 8
|
||||
@node_template :erlang.make_tuple(@node_size, [])
|
||||
|
||||
@opaque t :: %__MODULE__{size: non_neg_integer, root: term}
|
||||
@doc false
|
||||
defstruct size: 0, root: @node_template
|
||||
|
||||
# Inline common instructions
|
||||
@compile :inline_list_funcs
|
||||
@compile {:inline, key_hash: 1, key_mask: 1, key_shift: 1}
|
||||
|
||||
@doc """
|
||||
Creates a new empty set.
|
||||
"""
|
||||
@spec new :: Set.t
|
||||
def new do
|
||||
%HashSet{}
|
||||
end
|
||||
|
||||
def union(%HashSet{size: size1} = set1, %HashSet{size: size2} = set2) when size1 <= size2 do
|
||||
set_fold set1, set2, fn v, acc -> put(acc, v) end
|
||||
end
|
||||
|
||||
def union(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
set_fold set2, set1, fn v, acc -> put(acc, v) end
|
||||
end
|
||||
|
||||
def intersection(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
set_fold set1, %HashSet{}, fn v, acc ->
|
||||
if member?(set2, v), do: put(acc, v), else: acc
|
||||
end
|
||||
end
|
||||
|
||||
def difference(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
set_fold set2, set1, fn v, acc -> delete(acc, v) end
|
||||
end
|
||||
|
||||
def to_list(set) do
|
||||
set_fold(set, [], &[&1|&2]) |> :lists.reverse
|
||||
end
|
||||
|
||||
def equal?(%HashSet{size: size1} = set1, %HashSet{size: size2} = set2) do
|
||||
case size1 do
|
||||
^size2 -> subset?(set1, set2)
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def subset?(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
reduce(set1, {:cont, true}, fn member, acc ->
|
||||
case member?(set2, member) do
|
||||
true -> {:cont, acc}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
def disjoint?(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
reduce(set2, {:cont, true}, fn member, acc ->
|
||||
case member?(set1, member) do
|
||||
false -> {:cont, acc}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
def member?(%HashSet{root: root}, term) do
|
||||
do_member?(root, term, key_hash(term))
|
||||
end
|
||||
|
||||
def put(%HashSet{root: root, size: size}, term) do
|
||||
{root, counter} = do_put(root, term, key_hash(term))
|
||||
%HashSet{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
def delete(%HashSet{root: root, size: size} = set, term) do
|
||||
case do_delete(root, term, key_hash(term)) do
|
||||
{:ok, root} -> %HashSet{root: root, size: size - 1}
|
||||
:error -> set
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def reduce(%HashSet{root: root}, acc, fun) do
|
||||
do_reduce(root, acc, fun, @node_size, fn
|
||||
{:suspend, acc} -> {:suspended, acc, &{:done, elem(&1, 1)}}
|
||||
{:halt, acc} -> {:halted, acc}
|
||||
{:cont, acc} -> {:done, acc}
|
||||
end)
|
||||
end
|
||||
|
||||
def size(%HashSet{size: size}) do
|
||||
size
|
||||
end
|
||||
|
||||
## Set helpers
|
||||
|
||||
defp set_fold(%HashSet{root: root}, acc, fun) do
|
||||
do_fold(root, acc, fun, @node_size)
|
||||
end
|
||||
|
||||
## Set manipulation
|
||||
|
||||
defp do_member?(node, term, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] -> false
|
||||
[^term|_] -> true
|
||||
[_] -> false
|
||||
[_|n] -> do_member?(n, term, key_shift(hash))
|
||||
end
|
||||
end
|
||||
|
||||
defp do_put(node, term, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{put_elem(node, index, [term]), 1}
|
||||
[^term|_] ->
|
||||
{node, 0}
|
||||
[t] ->
|
||||
n = put_elem(@node_template, key_mask(key_shift(hash)), [term])
|
||||
{put_elem(node, index, [t|n]), 1}
|
||||
[t|n] ->
|
||||
{n, counter} = do_put(n, term, key_shift(hash))
|
||||
{put_elem(node, index, [t|n]), counter}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_delete(node, term, hash) do
|
||||
index = key_mask(hash)
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
:error
|
||||
[^term] ->
|
||||
{:ok, put_elem(node, index, [])}
|
||||
[_] ->
|
||||
:error
|
||||
[^term|n] ->
|
||||
{:ok, put_elem(node, index, do_compact_node(n))}
|
||||
[t|n] ->
|
||||
case do_delete(n, term, key_shift(hash)) do
|
||||
{:ok, @node_template} ->
|
||||
{:ok, put_elem(node, index, [t])}
|
||||
{:ok, n} ->
|
||||
{:ok, put_elem(node, index, [t|n])}
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Enum.each 0..(@node_size - 1), fn index ->
|
||||
defp do_compact_node(node) when elem(node, unquote(index)) != [] do
|
||||
case elem(node, unquote(index)) do
|
||||
[t] ->
|
||||
case put_elem(node, unquote(index), []) do
|
||||
@node_template -> [t]
|
||||
n -> [t|n]
|
||||
end
|
||||
[t|n] ->
|
||||
[t|put_elem(node, unquote(index), do_compact_node(n))]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Set fold
|
||||
|
||||
defp do_fold_each([], acc, _fun), do: acc
|
||||
defp do_fold_each([t], acc, fun), do: fun.(t, acc)
|
||||
defp do_fold_each([t|n], acc, fun), do: do_fold(n, fun.(t, acc), fun, @node_size)
|
||||
|
||||
defp do_fold(node, acc, fun, count) when count > 0 do
|
||||
acc = do_fold_each(:erlang.element(count, node), acc, fun)
|
||||
do_fold(node, acc, fun, count - 1)
|
||||
end
|
||||
|
||||
defp do_fold(_node, acc, _fun, 0) do
|
||||
acc
|
||||
end
|
||||
|
||||
## Set reduce
|
||||
|
||||
defp do_reduce_each(_node, {:halt, acc}, _fun, _next) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp do_reduce_each(node, {:suspend, acc}, fun, next) do
|
||||
{:suspended, acc, &do_reduce_each(node, &1, fun, next)}
|
||||
end
|
||||
|
||||
defp do_reduce_each([], acc, _fun, next) do
|
||||
next.(acc)
|
||||
end
|
||||
|
||||
defp do_reduce_each([t], {:cont, acc}, fun, next) do
|
||||
next.(fun.(t, acc))
|
||||
end
|
||||
|
||||
defp do_reduce_each([t|n], {:cont, acc}, fun, next) do
|
||||
do_reduce(n, fun.(t, acc), fun, @node_size, next)
|
||||
end
|
||||
|
||||
defp do_reduce(node, acc, fun, count, next) when count > 0 do
|
||||
do_reduce_each(:erlang.element(count, node), acc, fun, &do_reduce(node, &1, fun, count - 1, next))
|
||||
end
|
||||
|
||||
defp do_reduce(_node, acc, _fun, 0, next) do
|
||||
next.(acc)
|
||||
end
|
||||
|
||||
## Key operations
|
||||
|
||||
import Bitwise
|
||||
|
||||
defp key_hash(key) do
|
||||
:erlang.phash2(key)
|
||||
end
|
||||
|
||||
defp key_mask(hash) do
|
||||
hash &&& @node_bitmap
|
||||
end
|
||||
|
||||
defp key_shift(hash) do
|
||||
hash >>> @node_shift
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: HashSet do
|
||||
def reduce(set, acc, fun), do: HashSet.reduce(set, acc, fun)
|
||||
def member?(set, v), do: {:ok, HashSet.member?(set, v)}
|
||||
def count(set), do: {:ok, HashSet.size(set)}
|
||||
end
|
||||
|
||||
defimpl Collectable, for: HashSet do
|
||||
def empty(_dict) do
|
||||
HashSet.new
|
||||
end
|
||||
|
||||
def into(original) do
|
||||
{original, fn
|
||||
set, {:cont, x} -> HashSet.put(set, x)
|
||||
set, :done -> set
|
||||
_, :halt -> :ok
|
||||
end}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: HashSet do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(set, opts) do
|
||||
concat ["#HashSet<", Inspect.List.inspect(HashSet.to_list(set), opts), ">"]
|
||||
end
|
||||
end
|
||||
@@ -1,524 +0,0 @@
|
||||
import Kernel, except: [inspect: 1]
|
||||
import Inspect.Algebra
|
||||
|
||||
defprotocol Inspect do
|
||||
@moduledoc """
|
||||
The `Inspect` protocol is responsible for converting any Elixir
|
||||
data structure into an algebra document. This document is then
|
||||
formatted, either in pretty printing format or a regular one.
|
||||
|
||||
The `inspect/2` function receives the entity to be inspected
|
||||
followed by the inspecting options, represented by the struct
|
||||
`Inspect.Opts`.
|
||||
|
||||
Inspection is done using the functions available in `Inspect.Algebra`.
|
||||
|
||||
## Examples
|
||||
|
||||
Many times, inspecting a structure can be implemented in function
|
||||
of existing entities. For example, here is `HashSet`'s `inspect`
|
||||
implementation:
|
||||
|
||||
defimpl Inspect, for: HashSet do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(dict, opts) do
|
||||
concat ["#HashSet<", to_doc(HashSet.to_list(dict), opts), ">"]
|
||||
end
|
||||
end
|
||||
|
||||
The `concat` function comes from `Inspect.Algebra` and it
|
||||
concatenates algebra documents together. In the example above,
|
||||
it is concatenating the string `"HashSet<"` (all strings are
|
||||
valid algebra documents that keep their formatting when pretty
|
||||
printed), the document returned by `Inspect.Algebra.to_doc/2` and the
|
||||
other string `">"`.
|
||||
|
||||
Since regular strings are valid entities in an algebra document,
|
||||
an implementation of inspect may simply return a string,
|
||||
although that will devoid it of any pretty-printing.
|
||||
|
||||
## Error handling
|
||||
|
||||
In case there is an error while your structure is being inspected,
|
||||
Elixir will automatically fall back to a raw representation.
|
||||
|
||||
You can however access the underlying error by invoking the Inspect
|
||||
implementation directly. For example, to test Inspect.HashSet above,
|
||||
you can invoke it as:
|
||||
|
||||
Inspect.HashSet.inspect(HashSet.new, Inspect.Opts.new)
|
||||
|
||||
"""
|
||||
|
||||
# Handle structs in Any
|
||||
@fallback_to_any true
|
||||
|
||||
def inspect(thing, opts)
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Atom do
|
||||
require Macro
|
||||
|
||||
def inspect(atom, _opts) do
|
||||
inspect(atom)
|
||||
end
|
||||
|
||||
def inspect(false), do: "false"
|
||||
def inspect(true), do: "true"
|
||||
def inspect(nil), do: "nil"
|
||||
def inspect(:""), do: ":\"\""
|
||||
|
||||
def inspect(atom) do
|
||||
binary = Atom.to_string(atom)
|
||||
|
||||
cond do
|
||||
valid_ref_identifier?(binary) ->
|
||||
if only_elixir?(binary) do
|
||||
binary
|
||||
else
|
||||
"Elixir." <> rest = binary
|
||||
rest
|
||||
end
|
||||
valid_atom_identifier?(binary) ->
|
||||
":" <> binary
|
||||
atom in [:%{}, :{}, :<<>>, :..., :%] ->
|
||||
":" <> binary
|
||||
atom in Macro.binary_ops or atom in Macro.unary_ops ->
|
||||
":" <> binary
|
||||
true ->
|
||||
<< ?:, ?", Inspect.BitString.escape(binary, ?") :: binary, ?" >>
|
||||
end
|
||||
end
|
||||
|
||||
defp only_elixir?("Elixir." <> rest), do: only_elixir?(rest)
|
||||
defp only_elixir?("Elixir"), do: true
|
||||
defp only_elixir?(_), do: false
|
||||
|
||||
# Detect if atom is an atom alias (Elixir.Foo.Bar.Baz)
|
||||
|
||||
defp valid_ref_identifier?("Elixir" <> rest) do
|
||||
valid_ref_piece?(rest)
|
||||
end
|
||||
|
||||
defp valid_ref_identifier?(_), do: false
|
||||
|
||||
defp valid_ref_piece?(<<?., h, t :: binary>>) when h in ?A..?Z do
|
||||
valid_ref_piece? valid_identifier?(t)
|
||||
end
|
||||
|
||||
defp valid_ref_piece?(<<>>), do: true
|
||||
defp valid_ref_piece?(_), do: false
|
||||
|
||||
# Detect if atom
|
||||
|
||||
defp valid_atom_identifier?(<<h, t :: binary>>) when h in ?a..?z or h in ?A..?Z or h == ?_ do
|
||||
valid_atom_piece?(t)
|
||||
end
|
||||
|
||||
defp valid_atom_identifier?(_), do: false
|
||||
|
||||
defp valid_atom_piece?(t) do
|
||||
case valid_identifier?(t) do
|
||||
<<>> -> true
|
||||
<<??>> -> true
|
||||
<<?!>> -> true
|
||||
<<?@, t::binary>> -> valid_atom_piece?(t)
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_identifier?(<<h, t :: binary>>)
|
||||
when h in ?a..?z
|
||||
when h in ?A..?Z
|
||||
when h in ?0..?9
|
||||
when h == ?_ do
|
||||
valid_identifier? t
|
||||
end
|
||||
|
||||
defp valid_identifier?(other), do: other
|
||||
end
|
||||
|
||||
defimpl Inspect, for: BitString do
|
||||
def inspect(thing, %Inspect.Opts{binaries: bins} = opts) when is_binary(thing) do
|
||||
if bins == :as_strings or (bins == :infer and String.printable?(thing)) do
|
||||
<<?", escape(thing, ?") :: binary, ?">>
|
||||
else
|
||||
inspect_bitstring(thing, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def inspect(thing, opts) do
|
||||
inspect_bitstring(thing, opts)
|
||||
end
|
||||
|
||||
## Escaping
|
||||
|
||||
@doc false
|
||||
def escape(other, char) do
|
||||
escape(other, char, <<>>)
|
||||
end
|
||||
|
||||
defp escape(<< char, t :: binary >>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, char >>)
|
||||
end
|
||||
defp escape(<<?#, ?{, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?#, ?{>>)
|
||||
end
|
||||
defp escape(<<?\a, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?a >>)
|
||||
end
|
||||
defp escape(<<?\b, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?b >>)
|
||||
end
|
||||
defp escape(<<?\d, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?d >>)
|
||||
end
|
||||
defp escape(<<?\e, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?e >>)
|
||||
end
|
||||
defp escape(<<?\f, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?f >>)
|
||||
end
|
||||
defp escape(<<?\n, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?n >>)
|
||||
end
|
||||
defp escape(<<?\r, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?r >>)
|
||||
end
|
||||
defp escape(<<?\\, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?\\ >>)
|
||||
end
|
||||
defp escape(<<?\t, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?t >>)
|
||||
end
|
||||
defp escape(<<?\v, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, ?\\, ?v >>)
|
||||
end
|
||||
defp escape(<<h :: utf8, t :: binary>>, char, binary) do
|
||||
head = << h :: utf8 >>
|
||||
if String.printable?(head) do
|
||||
escape(t, char, append(head, binary))
|
||||
else
|
||||
<< byte :: size(8), h :: binary >> = head
|
||||
t = << h :: binary, t :: binary >>
|
||||
escape(t, char, << binary :: binary, escape_char(byte) :: binary >>)
|
||||
end
|
||||
end
|
||||
defp escape(<<h, t :: binary>>, char, binary) do
|
||||
escape(t, char, << binary :: binary, escape_char(h) :: binary >>)
|
||||
end
|
||||
defp escape(<<>>, _char, binary), do: binary
|
||||
|
||||
@doc false
|
||||
# Also used by Regex
|
||||
def escape_char(0) do
|
||||
<<?\\, ?0>>
|
||||
end
|
||||
|
||||
def escape_char(char) when char < 0x100 do
|
||||
<<a::4, b::4>> = <<char::size(8)>>
|
||||
<<?\\, ?x, to_hex(a), to_hex(b)>>
|
||||
end
|
||||
|
||||
def escape_char(char) when char < 0x10000 do
|
||||
<<a::4, b::4, c::4, d::4>> = <<char::size(16)>>
|
||||
<<?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
|
||||
end
|
||||
|
||||
def escape_char(char) when char < 0x1000000 do
|
||||
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::size(24)>>
|
||||
<<?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c),
|
||||
to_hex(d), to_hex(e), to_hex(f), ?}>>
|
||||
end
|
||||
|
||||
defp to_hex(c) when c in 0..9, do: ?0+c
|
||||
defp to_hex(c) when c in 10..15, do: ?A+c-10
|
||||
|
||||
defp append(<<h, t :: binary>>, binary), do: append(t, << binary :: binary, h >>)
|
||||
defp append(<<>>, binary), do: binary
|
||||
|
||||
## Bitstrings
|
||||
|
||||
defp inspect_bitstring(bitstring, opts) do
|
||||
each_bit(bitstring, opts.limit, "<<") <> ">>"
|
||||
end
|
||||
|
||||
defp each_bit(_, 0, acc) do
|
||||
acc <> "..."
|
||||
end
|
||||
|
||||
defp each_bit(<<h, t :: bitstring>>, counter, acc) when t != <<>> do
|
||||
each_bit(t, decrement(counter), acc <> Integer.to_string(h) <> ", ")
|
||||
end
|
||||
|
||||
defp each_bit(<<h :: size(8)>>, _counter, acc) do
|
||||
acc <> Integer.to_string(h)
|
||||
end
|
||||
|
||||
defp each_bit(<<>>, _counter, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp each_bit(bitstring, _counter, acc) do
|
||||
size = bit_size(bitstring)
|
||||
<<h :: size(size)>> = bitstring
|
||||
acc <> Integer.to_string(h) <> "::size(" <> Integer.to_string(size) <> ")"
|
||||
end
|
||||
|
||||
defp decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
end
|
||||
|
||||
defimpl Inspect, for: List do
|
||||
def inspect([], _opts), do: "[]"
|
||||
|
||||
def inspect(thing, %Inspect.Opts{char_lists: lists} = opts) do
|
||||
cond do
|
||||
lists == :as_char_lists or (lists == :infer and printable?(thing)) ->
|
||||
<< ?', Inspect.BitString.escape(IO.chardata_to_string(thing), ?') :: binary, ?' >>
|
||||
keyword?(thing) ->
|
||||
surround_many("[", thing, "]", opts, &keyword/2)
|
||||
true ->
|
||||
surround_many("[", thing, "]", opts, &to_doc/2)
|
||||
end
|
||||
end
|
||||
|
||||
def keyword({key, value}, opts) do
|
||||
concat(
|
||||
key_to_binary(key) <> ": ",
|
||||
to_doc(value, opts)
|
||||
)
|
||||
end
|
||||
|
||||
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
case Atom.to_char_list(key) do
|
||||
'Elixir.' ++ _ -> false
|
||||
_ -> keyword?(rest)
|
||||
end
|
||||
end
|
||||
|
||||
def keyword?([]), do: true
|
||||
def keyword?(_other), do: false
|
||||
|
||||
## Private
|
||||
|
||||
defp key_to_binary(key) do
|
||||
case Inspect.Atom.inspect(key) do
|
||||
":" <> right -> right
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp printable?([c|cs]) when is_integer(c) and c in 32..126, do: printable?(cs)
|
||||
defp printable?([?\n|cs]), do: printable?(cs)
|
||||
defp printable?([?\r|cs]), do: printable?(cs)
|
||||
defp printable?([?\t|cs]), do: printable?(cs)
|
||||
defp printable?([?\v|cs]), do: printable?(cs)
|
||||
defp printable?([?\b|cs]), do: printable?(cs)
|
||||
defp printable?([?\f|cs]), do: printable?(cs)
|
||||
defp printable?([?\e|cs]), do: printable?(cs)
|
||||
defp printable?([?\a|cs]), do: printable?(cs)
|
||||
defp printable?([]), do: true
|
||||
defp printable?(_), do: false
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Tuple do
|
||||
def inspect({}, _opts), do: "{}"
|
||||
|
||||
def inspect(tuple, opts) do
|
||||
surround_many("{", Tuple.to_list(tuple), "}", opts, &to_doc/2)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Map do
|
||||
def inspect(map, opts) do
|
||||
nest inspect(map, "", opts), 1
|
||||
end
|
||||
|
||||
def inspect(map, name, opts) do
|
||||
map = :maps.to_list(map)
|
||||
surround_many("%" <> name <> "{", map, "}", opts, traverse_fun(map))
|
||||
end
|
||||
|
||||
defp traverse_fun(list) do
|
||||
if Inspect.List.keyword?(list) do
|
||||
&Inspect.List.keyword/2
|
||||
else
|
||||
&to_map/2
|
||||
end
|
||||
end
|
||||
|
||||
defp to_map({key, value}, opts) do
|
||||
concat(
|
||||
concat(to_doc(key, opts), " => "),
|
||||
to_doc(value, opts)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Integer do
|
||||
def inspect(thing, %Inspect.Opts{base: base}) do
|
||||
Integer.to_string(thing, base_to_value(base))
|
||||
|> prepend_prefix(base)
|
||||
end
|
||||
|
||||
defp base_to_value(base) do
|
||||
case base do
|
||||
:binary -> 2
|
||||
:decimal -> 10
|
||||
:octal -> 8
|
||||
:hex -> 16
|
||||
end
|
||||
end
|
||||
|
||||
defp prepend_prefix(value, :decimal), do: value
|
||||
defp prepend_prefix(value, base) do
|
||||
prefix = case base do
|
||||
:binary -> "0b"
|
||||
:octal -> "0o"
|
||||
:hex -> "0x"
|
||||
end
|
||||
prefix <> value
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Float do
|
||||
def inspect(thing, _opts) do
|
||||
IO.iodata_to_binary(:io_lib_format.fwrite_g(thing))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Regex do
|
||||
def inspect(regex, _opts) do
|
||||
delim = ?/
|
||||
concat ["~r",
|
||||
<<delim, escape(regex.source, delim)::binary, delim>>,
|
||||
regex.opts]
|
||||
end
|
||||
|
||||
defp escape(bin, term),
|
||||
do: escape(bin, <<>>, term)
|
||||
|
||||
defp escape(<<?\\, term>> <> rest, buf, term),
|
||||
do: escape(rest, buf <> <<?\\, term>>, term)
|
||||
|
||||
defp escape(<<term>> <> rest, buf, term),
|
||||
do: escape(rest, buf <> <<?\\, term>>, term)
|
||||
|
||||
# the list of characters is from `String.printable?` impl
|
||||
# minus characters treated specially by regex: \s, \d, \b, \e
|
||||
|
||||
defp escape(<<?\n>> <> rest, buf, term),
|
||||
do: escape(rest, <<buf::binary, ?\\, ?n>>, term)
|
||||
|
||||
defp escape(<<?\r>> <> rest, buf, term),
|
||||
do: escape(rest, <<buf::binary, ?\\, ?r>>, term)
|
||||
|
||||
defp escape(<<?\t>> <> rest, buf, term),
|
||||
do: escape(rest, <<buf::binary, ?\\, ?t>>, term)
|
||||
|
||||
defp escape(<<?\v>> <> rest, buf, term),
|
||||
do: escape(rest, <<buf::binary, ?\\, ?v>>, term)
|
||||
|
||||
defp escape(<<?\f>> <> rest, buf, term),
|
||||
do: escape(rest, <<buf::binary, ?\\, ?f>>, term)
|
||||
|
||||
defp escape(<<?\a>> <> rest, buf, term),
|
||||
do: escape(rest, <<buf::binary, ?\\, ?a>>, term)
|
||||
|
||||
defp escape(<<c::utf8>> <> rest, buf, term) do
|
||||
charstr = <<c::utf8>>
|
||||
if String.printable?(charstr) and not c in [?\d, ?\b, ?\e] do
|
||||
escape(rest, buf <> charstr, term)
|
||||
else
|
||||
escape(rest, buf <> Inspect.BitString.escape_char(c), term)
|
||||
end
|
||||
end
|
||||
|
||||
defp escape(<<c>> <> rest, buf, term),
|
||||
do: escape(rest, <<buf::binary, Inspect.BitString.escape_char(c)>>, term)
|
||||
|
||||
defp escape(<<>>, buf, _), do: buf
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Function do
|
||||
def inspect(function, _opts) do
|
||||
fun_info = :erlang.fun_info(function)
|
||||
mod = fun_info[:module]
|
||||
|
||||
if fun_info[:type] == :external and fun_info[:env] == [] do
|
||||
"&#{Inspect.Atom.inspect(mod)}.#{fun_info[:name]}/#{fun_info[:arity]}"
|
||||
else
|
||||
case Atom.to_char_list(mod) do
|
||||
'elixir_compiler_' ++ _ ->
|
||||
if function_exported?(mod, :__RELATIVE__, 0) do
|
||||
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
|
||||
else
|
||||
default_inspect(mod, fun_info)
|
||||
end
|
||||
_ ->
|
||||
default_inspect(mod, fun_info)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp default_inspect(mod, fun_info) do
|
||||
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in " <>
|
||||
"#{Inspect.Atom.inspect(mod)}#{extract_name(fun_info[:name])}>"
|
||||
end
|
||||
|
||||
defp extract_name([]) do
|
||||
""
|
||||
end
|
||||
|
||||
defp extract_name(name) do
|
||||
name = Atom.to_string(name)
|
||||
case :binary.split(name, "-", [:global]) do
|
||||
["", name | _] -> "." <> name
|
||||
_ -> "." <> name
|
||||
end
|
||||
end
|
||||
|
||||
defp uniq(fun_info) do
|
||||
Integer.to_string(fun_info[:new_index]) <> "." <>
|
||||
Integer.to_string(fun_info[:uniq])
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: PID do
|
||||
def inspect(pid, _opts) do
|
||||
"#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Port do
|
||||
def inspect(port, _opts) do
|
||||
IO.iodata_to_binary :erlang.port_to_list(port)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Reference do
|
||||
def inspect(ref, _opts) do
|
||||
'#Ref' ++ rest = :erlang.ref_to_list(ref)
|
||||
"#Reference" <> IO.iodata_to_binary(rest)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Any do
|
||||
def inspect(%{__struct__: struct} = map, opts) do
|
||||
try do
|
||||
struct.__struct__
|
||||
rescue
|
||||
_ -> Inspect.Map.inspect(map, opts)
|
||||
else
|
||||
dunder ->
|
||||
if :maps.keys(dunder) == :maps.keys(map) do
|
||||
pruned = :maps.remove(:__exception__, :maps.remove(:__struct__, map))
|
||||
Inspect.Map.inspect(pruned, Inspect.Atom.inspect(struct, opts), opts)
|
||||
else
|
||||
Inspect.Map.inspect(map, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,529 +0,0 @@
|
||||
defmodule Inspect.Opts do
|
||||
@moduledoc """
|
||||
Defines the Inspect.Opts used by the Inspect protocol.
|
||||
|
||||
The following fields are available:
|
||||
|
||||
* `:structs` - when false, structs are not formatted by the inspect
|
||||
protocol, they are instead printed as maps, defaults to true.
|
||||
|
||||
* `:binaries` - when `:as_strings` all binaries will be printed as strings,
|
||||
non-printable bytes will be escaped.
|
||||
|
||||
When `:as_binaries` all binaries will be printed in bit syntax.
|
||||
|
||||
When the default `:infer`, the binary will be printed as a string if it
|
||||
is printable, otherwise in bit syntax.
|
||||
|
||||
* `:char_lists` - when `:as_char_lists` all lists will be printed as char
|
||||
lists, non-printable elements will be escaped.
|
||||
|
||||
When `:as_lists` all lists will be printed as lists.
|
||||
|
||||
When the default `:infer`, the list will be printed as a char list if it
|
||||
is printable, otherwise as list.
|
||||
|
||||
* `:limit` - limits the number of items that are printed for tuples,
|
||||
bitstrings, and lists, does not apply to strings nor char lists, defaults
|
||||
to 50.
|
||||
|
||||
* `:pretty` - if set to true enables pretty printing, defaults to false.
|
||||
|
||||
* `:width` - defaults to the 80 characters, used when pretty is true or
|
||||
when printing to IO devices.
|
||||
|
||||
* `:base` - print integers as :binary, :octal, :decimal, or :hex, defaults
|
||||
to :decimal
|
||||
|
||||
"""
|
||||
|
||||
defstruct structs: true,
|
||||
binaries: :infer,
|
||||
char_lists: :infer,
|
||||
limit: 50,
|
||||
width: 80,
|
||||
base: :decimal,
|
||||
pretty: false
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
structs: boolean,
|
||||
binaries: :infer | :as_binaries | :as_strings,
|
||||
char_lists: :infer | :as_lists | :as_char_lists,
|
||||
limit: pos_integer,
|
||||
width: pos_integer | :infinity,
|
||||
base: :decimal | :binary | :hex | :octal,
|
||||
pretty: boolean}
|
||||
end
|
||||
|
||||
defmodule Inspect.Algebra do
|
||||
@moduledoc ~S"""
|
||||
A set of functions for creating and manipulating algebra
|
||||
documents, as described in ["Strictly Pretty" (2000) by Christian Lindig][0].
|
||||
|
||||
An algebra document is represented by an `Inspect.Algebra` node
|
||||
or a regular string.
|
||||
|
||||
iex> Inspect.Algebra.empty
|
||||
:doc_nil
|
||||
|
||||
iex> "foo"
|
||||
"foo"
|
||||
|
||||
With the functions in this module, we can concatenate different
|
||||
elements together and render them:
|
||||
|
||||
iex> doc = Inspect.Algebra.concat(Inspect.Algebra.empty, "foo")
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["foo"]
|
||||
|
||||
The functions `nest/2`, `space/2` and `line/2` help you put the
|
||||
document together into a rigid structure. However, the document
|
||||
algebra gets interesting when using functions like `break/2`, which
|
||||
converts the given string into a line break depending on how much space
|
||||
there is to print. Let's glue two docs together with a break and then
|
||||
render it:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue("a", " ", "b")
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["a", " ", "b"]
|
||||
|
||||
Notice the break was represented as is, because we haven't reached
|
||||
a line limit. Once we do, it is replaced by a newline:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
|
||||
iex> Inspect.Algebra.format(doc, 10)
|
||||
["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]
|
||||
|
||||
Finally, this module also contains Elixir related functions, a bit
|
||||
tied to Elixir formatting, namely `surround/3` and `surround_many/5`.
|
||||
|
||||
## Implementation details
|
||||
|
||||
The original Haskell implementation of the algorithm by [Wadler][1]
|
||||
relies on lazy evaluation to unfold document groups on two alternatives:
|
||||
`:flat` (breaks as spaces) and `:break` (breaks as newlines).
|
||||
Implementing the same logic in a strict language such as Elixir leads
|
||||
to an exponential growth of possible documents, unless document groups
|
||||
are encoded explictly as `:flat` or `:break`. Those groups are then reduced
|
||||
to a simple document, where the layout is already decided, per [Lindig][0].
|
||||
|
||||
This implementation slightly changes the semantic of Lindig's algorithm
|
||||
to allow elements that belong to the same group to be printed together
|
||||
in the same line, even if they do not fit the line fully. This was achieved
|
||||
by changing `:break` to mean a possible break and `:flat` to force a flat
|
||||
structure. Then deciding if a break works as a newline is just a matter
|
||||
of checking if we have enough space until the next break that is not
|
||||
inside a group (which is still flat).
|
||||
|
||||
Custom pretty printers can be implemented using the documents returned
|
||||
by this module and by providing their own rendering functions.
|
||||
|
||||
[0]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200
|
||||
[1]: http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
|
||||
|
||||
"""
|
||||
|
||||
@surround_separator ","
|
||||
@tail_separator " |"
|
||||
@newline "\n"
|
||||
@nesting 1
|
||||
@break " "
|
||||
|
||||
# Functional interface to `doc` records
|
||||
|
||||
@type t :: :doc_nil | :doc_line | doc_cons | doc_nest | doc_break | doc_group | binary
|
||||
|
||||
@typep doc_cons :: {:doc_cons, t, t}
|
||||
defmacrop doc_cons(left, right) do
|
||||
quote do: {:doc_cons, unquote(left), unquote(right)}
|
||||
end
|
||||
|
||||
@typep doc_nest :: {:doc_nest, t, non_neg_integer}
|
||||
defmacrop doc_nest(doc, indent) do
|
||||
quote do: {:doc_nest, unquote(doc), unquote(indent) }
|
||||
end
|
||||
|
||||
@typep doc_break :: {:doc_break, binary}
|
||||
defmacrop doc_break(break) do
|
||||
quote do: {:doc_break, unquote(break)}
|
||||
end
|
||||
|
||||
@typep doc_group :: {:doc_group, t}
|
||||
defmacrop doc_group(group) do
|
||||
quote do: {:doc_group, unquote(group)}
|
||||
end
|
||||
|
||||
defmacrop is_doc(doc) do
|
||||
if Macro.Env.in_guard?(__CALLER__) do
|
||||
do_is_doc(doc)
|
||||
else
|
||||
var = quote do: doc
|
||||
quote do
|
||||
unquote(var) = unquote(doc)
|
||||
unquote(do_is_doc(var))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_is_doc(doc) do
|
||||
quote do
|
||||
is_binary(unquote(doc)) or
|
||||
unquote(doc) in [:doc_nil, :doc_line] or
|
||||
(is_tuple(unquote(doc)) and
|
||||
elem(unquote(doc), 0) in [:doc_cons, :doc_nest, :doc_break, :doc_group])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an Elixir structure to an algebra document
|
||||
according to the inspect protocol.
|
||||
"""
|
||||
@spec to_doc(any, Inspect.Opts.t) :: t
|
||||
def to_doc(%{__struct__: struct} = map, %Inspect.Opts{} = opts) when is_atom(struct) do
|
||||
if opts.structs do
|
||||
try do
|
||||
Inspect.inspect(map, opts)
|
||||
rescue
|
||||
e ->
|
||||
# Because we try to raise a nice error message in case
|
||||
# we can't inspect a struct, there is a chance the error
|
||||
# message itself relies on the struct being printed, so
|
||||
# we need to trap the inspected messages to guarantee
|
||||
# we won't try to render any failed instruct when building
|
||||
# the error message.
|
||||
if Process.get(:inspect_trap) do
|
||||
Inspect.Map.inspect(map, opts)
|
||||
else
|
||||
try do
|
||||
Process.put(:inspect_trap, true)
|
||||
res = Inspect.Map.inspect(map, opts)
|
||||
formatted = IO.iodata_to_binary(format(res, :infinity))
|
||||
raise ArgumentError,
|
||||
"Got #{inspect e.__struct__} with message " <>
|
||||
"\"#{Exception.message(e)}\" while inspecting #{formatted}"
|
||||
after
|
||||
Process.delete(:inspect_trap)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
Inspect.Map.inspect(map, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def to_doc(arg, %Inspect.Opts{} = opts) do
|
||||
Inspect.inspect(arg, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a document entity used to represent nothingness.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Inspect.Algebra.empty
|
||||
:doc_nil
|
||||
|
||||
"""
|
||||
@spec empty() :: :doc_nil
|
||||
def empty, do: :doc_nil
|
||||
|
||||
@doc """
|
||||
Concatenates two document entities.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.concat "hello", "world"
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["hello", "world"]
|
||||
|
||||
"""
|
||||
@spec concat(t, t) :: doc_cons
|
||||
def concat(x, y) when is_doc(x) and is_doc(y) do
|
||||
doc_cons(x, y)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates a list of documents.
|
||||
"""
|
||||
@spec concat([t]) :: doc_cons
|
||||
def concat(docs) do
|
||||
folddoc(docs, &concat(&1, &2))
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Nests document entity `x` positions deep.
|
||||
|
||||
Nesting will be appended to the line breaks.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.nest(Inspect.Algebra.glue("hello", "world"), 5)
|
||||
iex> Inspect.Algebra.format(doc, 5)
|
||||
["hello", "\n ", "world"]
|
||||
|
||||
"""
|
||||
@spec nest(t, non_neg_integer) :: doc_nest
|
||||
def nest(x, 0) when is_doc(x) do
|
||||
x
|
||||
end
|
||||
|
||||
def nest(x, i) when is_doc(x) and is_integer(i) do
|
||||
doc_nest(x, i)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Document entity representing a break.
|
||||
|
||||
This break can be rendered as a linebreak or as spaces,
|
||||
depending on the `mode` of the chosen layout or the provided
|
||||
separator.
|
||||
|
||||
## Examples
|
||||
|
||||
Let's glue two docs together with a break and then render it:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue("a", " ", "b")
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["a", " ", "b"]
|
||||
|
||||
Notice the break was represented as is, because we haven't reached
|
||||
a line limit. Once we do, it is replaced by a newline:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
|
||||
iex> Inspect.Algebra.format(doc, 10)
|
||||
["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]
|
||||
|
||||
"""
|
||||
@spec break(binary) :: doc_break
|
||||
def break(s) when is_binary(s), do: doc_break(s)
|
||||
|
||||
@spec break() :: doc_break
|
||||
def break(), do: doc_break(@break)
|
||||
|
||||
@doc """
|
||||
Inserts a break between two docs. See `break/1` for more info.
|
||||
"""
|
||||
@spec glue(t, t) :: doc_cons
|
||||
def glue(x, y), do: concat(x, concat(break, y))
|
||||
|
||||
@doc """
|
||||
Inserts a break, passed as the second argument, between two docs,
|
||||
the first and the third arguments.
|
||||
"""
|
||||
@spec glue(t, binary, t) :: doc_cons
|
||||
def glue(x, g, y) when is_binary(g), do: concat(x, concat(break(g), y))
|
||||
|
||||
@doc ~S"""
|
||||
Returns a group containing the specified document.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.group(
|
||||
...> Inspect.Algebra.concat(
|
||||
...> Inspect.Algebra.group(
|
||||
...> Inspect.Algebra.concat(
|
||||
...> "Hello,",
|
||||
...> Inspect.Algebra.concat(
|
||||
...> Inspect.Algebra.break,
|
||||
...> "A"
|
||||
...> )
|
||||
...> )
|
||||
...> ),
|
||||
...> Inspect.Algebra.concat(
|
||||
...> Inspect.Algebra.break,
|
||||
...> "B"
|
||||
...> )
|
||||
...> ))
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["Hello,", " ", "A", " ", "B"]
|
||||
iex> Inspect.Algebra.format(doc, 6)
|
||||
["Hello,", "\n", "A", " ", "B"]
|
||||
|
||||
"""
|
||||
@spec group(t) :: doc_group
|
||||
def group(d) when is_doc(d) do
|
||||
doc_group(d)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inserts a mandatory single space between two document entities.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.space "Hughes", "Wadler"
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["Hughes", " ", "Wadler"]
|
||||
|
||||
"""
|
||||
@spec space(t, t) :: doc_cons
|
||||
def space(x, y), do: concat(x, concat(" ", y))
|
||||
|
||||
@doc ~S"""
|
||||
Inserts a mandatory linebreak between two document entities.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.line "Hughes", "Wadler"
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["Hughes", "\n", "Wadler"]
|
||||
|
||||
"""
|
||||
@spec line(t, t) :: doc_cons
|
||||
def line(x, y), do: concat(x, concat(:doc_line, y))
|
||||
|
||||
@doc """
|
||||
Folds a list of document entities into a document entity
|
||||
using a function that is passed as the first argument.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = ["A", "B"]
|
||||
iex> doc = Inspect.Algebra.folddoc(doc, fn(x,y) ->
|
||||
...> Inspect.Algebra.concat [x, "!", y]
|
||||
...> end)
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["A", "!", "B"]
|
||||
|
||||
"""
|
||||
@spec folddoc([t], ((t, t) -> t)) :: t
|
||||
def folddoc([], _), do: empty
|
||||
def folddoc([doc], _), do: doc
|
||||
def folddoc([d|ds], f), do: f.(d, folddoc(ds, f))
|
||||
|
||||
# Elixir conveniences
|
||||
|
||||
@doc ~S"""
|
||||
Surrounds a document with characters.
|
||||
|
||||
Puts the document between left and right enclosing and nesting it.
|
||||
The document is marked as a group, to show the maximum as possible
|
||||
concisely together.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.surround "[", Inspect.Algebra.glue("a", "b"), "]"
|
||||
iex> Inspect.Algebra.format(doc, 3)
|
||||
["[", "a", "\n ", "b", "]"]
|
||||
|
||||
"""
|
||||
@spec surround(binary, t, binary) :: t
|
||||
def surround(left, doc, right) do
|
||||
group concat left, concat(nest(doc, @nesting), right)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Maps and glues a collection of items.
|
||||
|
||||
It uses the given left and right as surrounding and a separator for
|
||||
each item. A limit can be passed which, once reached, stops gluing
|
||||
and outputs "..." instead.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]",
|
||||
...> %Inspect.Opts{limit: :infinity}, fn i, _opts -> to_string(i) end)
|
||||
iex> Inspect.Algebra.format(doc, 5) |> IO.iodata_to_binary
|
||||
"[1,\n 2,\n 3,\n 4,\n 5]"
|
||||
|
||||
iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]",
|
||||
...> %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end)
|
||||
iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
|
||||
"[1, 2, 3, ...]"
|
||||
|
||||
iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]",
|
||||
...> %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end, "!")
|
||||
iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
|
||||
"[1! 2! 3! ...]"
|
||||
"""
|
||||
@spec surround_many(binary, [any], binary, Inspect.Opts.t, (term, Inspect.Opts.t -> t), binary) :: t
|
||||
def surround_many(left, docs, right, opts, fun, separator \\ @surround_separator) do
|
||||
do_surround_many(left, docs, right, opts.limit, opts, fun, separator)
|
||||
end
|
||||
|
||||
defp do_surround_many(left, [], right, _, _opts, _fun, _) do
|
||||
concat(left, right)
|
||||
end
|
||||
|
||||
defp do_surround_many(left, docs, right, limit, _opts, fun, sep) do
|
||||
surround(left, do_surround_many(docs, limit, _opts, fun, sep), right)
|
||||
end
|
||||
|
||||
defp do_surround_many(_, 0, _opts, _fun, _sep) do
|
||||
"..."
|
||||
end
|
||||
|
||||
defp do_surround_many([h], limit, opts, fun, _sep) do
|
||||
fun.(h, %{opts | limit: limit})
|
||||
end
|
||||
|
||||
defp do_surround_many([h|t], limit, opts, fun, sep) when is_list(t) do
|
||||
limit = decrement(limit)
|
||||
glue(
|
||||
concat(fun.(h, %{opts | limit: limit}), sep),
|
||||
do_surround_many(t, limit, opts, fun, sep)
|
||||
)
|
||||
end
|
||||
|
||||
defp do_surround_many([h|t], limit, opts, fun, _sep) do
|
||||
limit = decrement(limit)
|
||||
glue(
|
||||
concat(fun.(h, %{opts | limit: limit}), @tail_separator),
|
||||
fun.(t, %{opts | limit: limit})
|
||||
)
|
||||
end
|
||||
|
||||
defp decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
|
||||
@doc """
|
||||
The formatting function.
|
||||
|
||||
Takes the maximum width and a document to print as its arguments
|
||||
and returns an IO data representation of the best layout for the
|
||||
document to fit in the given width.
|
||||
"""
|
||||
@spec format(t, non_neg_integer | :infinity) :: iodata
|
||||
def format(d, w) do
|
||||
format(w, 0, [{0, default_mode(w), doc_group(d)}])
|
||||
end
|
||||
|
||||
defp default_mode(:infinity), do: :flat
|
||||
defp default_mode(_), do: :break
|
||||
|
||||
# Record representing the document mode to be rendered: flat or broken
|
||||
@typep mode :: :flat | :break
|
||||
|
||||
@spec fits?(integer, [{integer, mode, t}]) :: boolean
|
||||
defp fits?(w, _) when w < 0, do: false
|
||||
defp fits?(_, []), do: true
|
||||
defp fits?(_, [{_, _, :doc_line} | _]), do: true
|
||||
defp fits?(w, [{_, _, :doc_nil} | t]), do: fits?(w, t)
|
||||
defp fits?(w, [{i, m, doc_cons(x, y)} | t]), do: fits?(w, [{i, m, x} | [{i, m, y} | t]])
|
||||
defp fits?(w, [{i, m, doc_nest(x, j)} | t]), do: fits?(w, [{i + j, m, x} | t])
|
||||
defp fits?(w, [{i, _, doc_group(x)} | t]), do: fits?(w, [{i, :flat, x} | t])
|
||||
defp fits?(w, [{_, _, s} | t]) when is_binary(s), do: fits?((w - byte_size s), t)
|
||||
defp fits?(w, [{_, :flat, doc_break(s)} | t]), do: fits?((w - byte_size s), t)
|
||||
defp fits?(_, [{_, :break, doc_break(_)} | _]), do: true
|
||||
|
||||
@spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
|
||||
defp format(_, _, []), do: []
|
||||
defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
|
||||
defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
|
||||
defp format(w, k, [{i, m, doc_cons(x, y)} | t]), do: format(w, k, [{i, m, x} | [{i, m, y} | t]])
|
||||
defp format(w, k, [{i, m, doc_nest(x, j)} | t]), do: format(w, k, [{i + j, m, x} | t])
|
||||
defp format(w, k, [{i, m, doc_group(x)} | t]), do: format(w, k, [{i, m, x} | t])
|
||||
defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, (k + byte_size s), t)]
|
||||
defp format(w, k, [{_, :flat, doc_break(s)} | t]), do: [s | format(w, (k + byte_size s), t)]
|
||||
defp format(w, k, [{i, :break, doc_break(s)} | t]) do
|
||||
k = k + byte_size(s)
|
||||
|
||||
if w == :infinity or fits?(w - k, t) do
|
||||
[s | format(w, k, t)]
|
||||
else
|
||||
[indent(i) | format(w, i, t)]
|
||||
end
|
||||
end
|
||||
|
||||
defp indent(0), do: @newline
|
||||
defp indent(i), do: @newline <> :binary.copy(" ", i)
|
||||
end
|
||||
@@ -1,141 +0,0 @@
|
||||
defmodule Integer do
|
||||
@moduledoc """
|
||||
Functions for working with integers.
|
||||
"""
|
||||
|
||||
import Bitwise
|
||||
|
||||
@doc """
|
||||
Determines if an integer is odd.
|
||||
|
||||
Returns `true` if `n` is an odd number, otherwise `false`.
|
||||
|
||||
Allowed in guard clauses.
|
||||
"""
|
||||
defmacro is_odd(n) do
|
||||
quote do: (unquote(n) &&& 1) == 1
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines if an integer is even.
|
||||
|
||||
Returns `true` if `n` is an even number, otherwise `false`.
|
||||
|
||||
Allowed in guard clauses.
|
||||
"""
|
||||
defmacro is_even(n) do
|
||||
quote do: (unquote(n) &&& 1) == 0
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a binary to an integer.
|
||||
|
||||
If successful, returns a tuple of the form `{integer, remainder_of_binary}`.
|
||||
Otherwise `:error`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.parse("34")
|
||||
{34,""}
|
||||
|
||||
iex> Integer.parse("34.5")
|
||||
{34,".5"}
|
||||
|
||||
iex> Integer.parse("three")
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec parse(binary) :: {integer, binary} | :error
|
||||
def parse(<< ?-, bin :: binary >>) do
|
||||
case do_parse(bin) do
|
||||
:error -> :error
|
||||
{number, remainder} -> {-number, remainder}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(<< ?+, bin :: binary >>) do
|
||||
do_parse(bin)
|
||||
end
|
||||
|
||||
def parse(bin) when is_binary(bin) do
|
||||
do_parse(bin)
|
||||
end
|
||||
|
||||
defp do_parse(<< char, bin :: binary >>) when char in ?0..?9, do: do_parse(bin, char - ?0)
|
||||
defp do_parse(_), do: :error
|
||||
|
||||
defp do_parse(<< char, rest :: binary >>, acc) when char in ?0..?9 do
|
||||
do_parse rest, 10 * acc + (char - ?0)
|
||||
end
|
||||
|
||||
defp do_parse(bitstring, acc) do
|
||||
{acc, bitstring}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a binary which corresponds to the text representation
|
||||
of `some_integer`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.to_string(123)
|
||||
"123"
|
||||
|
||||
"""
|
||||
@spec to_string(integer) :: String.t
|
||||
def to_string(some_integer) do
|
||||
:erlang.integer_to_binary(some_integer)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a binary which corresponds to the text representation
|
||||
of `some_integer` in base `base`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.to_string(100, 16)
|
||||
"64"
|
||||
|
||||
"""
|
||||
@spec to_string(integer, 2..36) :: String.t
|
||||
def to_string(some_integer, base) do
|
||||
:erlang.integer_to_binary(some_integer, base)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a char list which corresponds to the text representation of the given integer.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.to_char_list(7)
|
||||
'7'
|
||||
|
||||
"""
|
||||
@spec to_char_list(integer) :: char_list
|
||||
def to_char_list(number) do
|
||||
:erlang.integer_to_list(number)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a char list which corresponds to the text representation of the
|
||||
given integer in the given case.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.to_char_list(1023, 16)
|
||||
'3FF'
|
||||
|
||||
"""
|
||||
@spec to_char_list(integer, 2..36) :: char_list
|
||||
def to_char_list(number, base) do
|
||||
:erlang.integer_to_list(number, base)
|
||||
end
|
||||
end
|
||||
@@ -1,426 +0,0 @@
|
||||
defmodule IO do
|
||||
@moduledoc """
|
||||
Functions handling IO.
|
||||
|
||||
Many functions in this module expects an IO device as argument.
|
||||
An IO device must be a pid or an atom representing a process.
|
||||
For convenience, Elixir provides `:stdio` and `:stderr` as
|
||||
shortcuts to Erlang's `:standard_io` and `:standard_error`.
|
||||
|
||||
The majority of the functions expect char data, i.e. strings or
|
||||
lists of characters and strings. In case another type is given,
|
||||
it will do a conversion to string via the `String.Chars` protocol
|
||||
(as shown in typespecs).
|
||||
|
||||
The functions starting with `bin*` expects iodata as argument,
|
||||
i.e. binaries or lists of bytes and binaries.
|
||||
|
||||
## IO devices
|
||||
|
||||
An IO device may be an atom or a pid. In case it is an atom,
|
||||
the atom must be the name of a registered process. However,
|
||||
there are three exceptions for this rule:
|
||||
|
||||
* `:standard_io` - when the `:standard_io` atom is given,
|
||||
it is treated as a shortcut for `Process.group_leader`
|
||||
|
||||
* `:stdio` - is a shortcut for `:standard_io`
|
||||
|
||||
* `:stderr` - is a shortcut for `:standard_error`
|
||||
|
||||
"""
|
||||
|
||||
@type device :: atom | pid
|
||||
@type nodata :: {:error, term} | :eof
|
||||
@type chardata() :: :unicode.chardata()
|
||||
|
||||
import :erlang, only: [group_leader: 0]
|
||||
|
||||
defmacrop is_iodata(data) do
|
||||
quote do
|
||||
is_list(unquote(data)) or is_binary(unquote(data))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads `count` characters from the IO device, a whole
|
||||
`:line` or the whole device with `:all`.
|
||||
|
||||
It returns:
|
||||
|
||||
* `data` - the input characters
|
||||
|
||||
* `:eof` - end of file was encountered
|
||||
|
||||
* `{:error, reason}` - other (rare) error condition;
|
||||
for instance, `{:error, :estale}` if reading from an
|
||||
NFS volume
|
||||
|
||||
If `:all` is given, `:eof` is never returned, but an
|
||||
empty string in case the device has reached EOF.
|
||||
"""
|
||||
@spec read(device, :all | :line | non_neg_integer) :: chardata | nodata
|
||||
def read(device \\ group_leader, chars_or_line)
|
||||
|
||||
def read(device, :all) do
|
||||
do_read_all(map_dev(device), "")
|
||||
end
|
||||
|
||||
def read(device, :line) do
|
||||
:io.get_line(map_dev(device), '')
|
||||
end
|
||||
|
||||
def read(device, count) when count >= 0 do
|
||||
:io.get_chars(map_dev(device), '', count)
|
||||
end
|
||||
|
||||
defp do_read_all(mapped_dev, acc) do
|
||||
case :io.get_line(mapped_dev, "") do
|
||||
line when is_binary(line) -> do_read_all(mapped_dev, acc <> line)
|
||||
:eof -> acc
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads `count` characters from the IO device, a whole
|
||||
`:line` or the whole device with `:all`.
|
||||
|
||||
It returns:
|
||||
|
||||
* `data` - the input characters
|
||||
|
||||
* `:eof` - end of file was encountered
|
||||
|
||||
* `{:error, reason}` - other (rare) error condition;
|
||||
for instance, `{:error, :estale}` if reading from an
|
||||
NFS volume
|
||||
|
||||
If `:all` is given, `:eof` is never returned, but an
|
||||
empty string in case the device has reached EOF.
|
||||
|
||||
Note: do not use this function on IO devices in unicode mode
|
||||
as it will return the wrong result.
|
||||
"""
|
||||
@spec binread(device, :all | :line | non_neg_integer) :: iodata | nodata
|
||||
def binread(device \\ group_leader, chars_or_line)
|
||||
|
||||
def binread(device, :all) do
|
||||
do_binread_all(map_dev(device), "")
|
||||
end
|
||||
|
||||
def binread(device, :line) do
|
||||
case :file.read_line(map_dev(device)) do
|
||||
{:ok, data} -> data
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def binread(device, count) when count >= 0 do
|
||||
case :file.read(map_dev(device), count) do
|
||||
{:ok, data} -> data
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp do_binread_all(mapped_dev, acc) do
|
||||
case :file.read_line(mapped_dev) do
|
||||
{:ok, data} -> do_binread_all(mapped_dev, acc <> data)
|
||||
:eof -> acc
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes the given argument to the given device.
|
||||
|
||||
By default the device is the standard output.
|
||||
It returns `:ok` if it succeeds.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.write "sample"
|
||||
#=> "sample"
|
||||
|
||||
IO.write :stderr, "error"
|
||||
#=> "error"
|
||||
|
||||
"""
|
||||
@spec write(device, chardata | String.Chars.t) :: :ok
|
||||
def write(device \\ group_leader(), item) do
|
||||
:io.put_chars map_dev(device), to_chardata(item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes the given argument to the given device
|
||||
as a binary, no unicode conversion happens.
|
||||
|
||||
Check `write/2` for more information.
|
||||
|
||||
Note: do not use this function on IO devices in unicode mode
|
||||
as it will return the wrong result.
|
||||
"""
|
||||
@spec binwrite(device, iodata) :: :ok | {:error, term}
|
||||
def binwrite(device \\ group_leader(), item) when is_iodata(item) do
|
||||
:file.write map_dev(device), item
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes the argument to the device, similar to `write/2`,
|
||||
but adds a newline at the end. The argument is expected
|
||||
to be a chardata.
|
||||
"""
|
||||
@spec puts(device, chardata | String.Chars.t) :: :ok
|
||||
def puts(device \\ group_leader(), item) do
|
||||
erl_dev = map_dev(device)
|
||||
:io.put_chars erl_dev, [to_chardata(item), ?\n]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inspects and writes the given argument to the device.
|
||||
|
||||
It enables pretty printing by default with width of
|
||||
80 characters. The width can be changed by explicitly
|
||||
passing the `:width` option.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.inspect Process.list, width: 40
|
||||
|
||||
"""
|
||||
@spec inspect(term, Keyword.t) :: term
|
||||
def inspect(item, opts \\ []) do
|
||||
inspect group_leader(), item, opts
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inspects the item with options using the given device.
|
||||
"""
|
||||
@spec inspect(device, term, Keyword.t) :: term
|
||||
def inspect(device, item, opts) when is_list(opts) do
|
||||
opts = struct(Inspect.Opts, opts)
|
||||
iodata = Inspect.Algebra.format(Inspect.Algebra.to_doc(item, opts), opts.width)
|
||||
puts device, iodata
|
||||
item
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from the io device. If the
|
||||
io device is a unicode device, `count` implies
|
||||
the number of unicode codepoints to be retrieved.
|
||||
Otherwise, `count` is the number of raw bytes to be retrieved.
|
||||
It returns:
|
||||
|
||||
* `data` - the input characters
|
||||
|
||||
* `:eof` - end of file was encountered
|
||||
|
||||
* `{:error, reason}` - other (rare) error condition;
|
||||
for instance, `{:error, :estale}` if reading from an
|
||||
NFS volume
|
||||
"""
|
||||
@spec getn(chardata | String.Chars.t, pos_integer) :: chardata | nodata
|
||||
@spec getn(device, chardata | String.Chars.t) :: chardata | nodata
|
||||
def getn(prompt, count \\ 1)
|
||||
|
||||
def getn(prompt, count) when is_integer(count) do
|
||||
getn(group_leader, prompt, count)
|
||||
end
|
||||
|
||||
def getn(device, prompt) do
|
||||
getn(device, prompt, 1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from the io device. If the
|
||||
io device is a unicode device, `count` implies
|
||||
the number of unicode codepoints to be retrieved.
|
||||
Otherwise, `count` is the number of raw bytes to be retrieved.
|
||||
"""
|
||||
@spec getn(device, chardata | String.Chars.t, pos_integer) :: chardata | nodata
|
||||
def getn(device, prompt, count) do
|
||||
:io.get_chars(map_dev(device), to_chardata(prompt), count)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads a line from the IO device. It returns:
|
||||
|
||||
* `data` - the characters in the line terminated
|
||||
by a LF (or end of file)
|
||||
|
||||
* `:eof` - end of file was encountered
|
||||
|
||||
* `{:error, reason}` - other (rare) error condition;
|
||||
for instance, `{:error, :estale}` if reading from an
|
||||
NFS volume
|
||||
"""
|
||||
@spec gets(device, chardata | String.Chars.t) :: chardata | nodata
|
||||
def gets(device \\ group_leader(), prompt) do
|
||||
:io.get_line(map_dev(device), to_chardata(prompt))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the io device into a `IO.Stream`.
|
||||
|
||||
An `IO.Stream` implements both `Enumerable` and
|
||||
`Collectable`, allowing it to be used for both read
|
||||
and write.
|
||||
|
||||
The device is iterated line by line if `:line` is given or
|
||||
by a given number of codepoints.
|
||||
|
||||
This reads the IO as utf-8. Check out
|
||||
`IO.binstream/2` to handle the IO as a raw binary.
|
||||
|
||||
Note that an IO stream has side effects and every time
|
||||
you go over the stream you may get different results.
|
||||
|
||||
## Examples
|
||||
|
||||
Here is an example on how we mimic an echo server
|
||||
from the command line:
|
||||
|
||||
Enum.each IO.stream(:stdio, :line), &IO.write(&1)
|
||||
|
||||
"""
|
||||
@spec stream(device, :line | pos_integer) :: Enumerable.t
|
||||
def stream(device, line_or_codepoints) do
|
||||
IO.Stream.__build__(map_dev(device), false, line_or_codepoints)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the IO device into a `IO.Stream`.
|
||||
|
||||
An `IO.Stream` implements both `Enumerable` and
|
||||
`Collectable`, allowing it to be used for both read
|
||||
and write.
|
||||
|
||||
The device is iterated line by line or by a number of bytes.
|
||||
This reads the IO device as a raw binary.
|
||||
|
||||
Note that an IO stream has side effects and every time
|
||||
you go over the stream you may get different results.
|
||||
|
||||
Finally, do not use this function on IO devices in unicode
|
||||
mode as it will return the wrong result.
|
||||
"""
|
||||
@spec binstream(device, :line | pos_integer) :: Enumerable.t
|
||||
def binstream(device, line_or_bytes) do
|
||||
IO.Stream.__build__(map_dev(device), true, line_or_bytes)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts chardata (a list of integers representing codepoints,
|
||||
lists and strings) into a string.
|
||||
|
||||
In case the conversion fails, it raises a `UnicodeConversionError`.
|
||||
If a string is given, returns the string itself.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.chardata_to_string([0x00E6, 0x00DF])
|
||||
"æß"
|
||||
|
||||
iex> IO.chardata_to_string([0x0061, "bc"])
|
||||
"abc"
|
||||
|
||||
"""
|
||||
@spec chardata_to_string(chardata) :: String.t | no_return
|
||||
def chardata_to_string(string) when is_binary(string) do
|
||||
string
|
||||
end
|
||||
|
||||
def chardata_to_string(list) when is_list(list) do
|
||||
case :unicode.characters_to_binary(list) do
|
||||
result when is_binary(result) ->
|
||||
result
|
||||
|
||||
{:error, encoded, rest} ->
|
||||
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid
|
||||
|
||||
{:incomplete, encoded, rest} ->
|
||||
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :incomplete
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts iodata (a list of integers representing bytes, lists
|
||||
and binaries) into a binary.
|
||||
|
||||
Notice that this function treats lists of integers as raw bytes
|
||||
and does not perform any kind of encoding conversion. If you want
|
||||
to convert from a char list to a string (UTF-8 encoded), please
|
||||
use `chardata_to_string/1` instead.
|
||||
|
||||
If this function receives a binary, the same binary is returned.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bin1 = <<1, 2, 3>>
|
||||
iex> bin2 = <<4, 5>>
|
||||
iex> bin3 = <<6>>
|
||||
iex> IO.iodata_to_binary([bin1, 1, [2, 3, bin2], 4|bin3])
|
||||
<<1,2,3,1,2,3,4,5,4,6>>
|
||||
|
||||
iex> bin = <<1, 2, 3>>
|
||||
iex> IO.iodata_to_binary(bin)
|
||||
<<1,2,3>>
|
||||
|
||||
"""
|
||||
@spec iodata_to_binary(iodata) :: binary
|
||||
def iodata_to_binary(item) do
|
||||
:erlang.iolist_to_binary(item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the size of an iodata.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.iodata_length([1, 2|<<3, 4>>])
|
||||
4
|
||||
|
||||
"""
|
||||
@spec iodata_length(iodata) :: non_neg_integer
|
||||
def iodata_length(item) do
|
||||
:erlang.iolist_size(item)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def each_stream(device, what) do
|
||||
case read(device, what) do
|
||||
:eof ->
|
||||
{:halt, device}
|
||||
{:error, reason} ->
|
||||
raise IO.StreamError, reason: reason
|
||||
data ->
|
||||
{[data], device}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def each_binstream(device, what) do
|
||||
case binread(device, what) do
|
||||
:eof ->
|
||||
{:halt, device}
|
||||
{:error, reason} ->
|
||||
raise IO.StreamError, reason: reason
|
||||
data ->
|
||||
{[data], device}
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, map_dev: 1, to_chardata: 1}
|
||||
|
||||
# Map the Elixir names for standard io and error to Erlang names
|
||||
defp map_dev(:stdio), do: :standard_io
|
||||
defp map_dev(:stderr), do: :standard_error
|
||||
defp map_dev(other) when is_atom(other) or is_pid(other) or is_tuple(other), do: other
|
||||
|
||||
defp to_chardata(list) when is_list(list), do: list
|
||||
defp to_chardata(other), do: to_string(other)
|
||||
end
|
||||
@@ -1,209 +0,0 @@
|
||||
defmodule IO.ANSI.Sequence do
|
||||
@moduledoc false
|
||||
|
||||
defmacro defsequence(name, code \\ "", terminator \\ "m") do
|
||||
quote bind_quoted: [name: name, code: code, terminator: terminator] do
|
||||
def unquote(name)() do
|
||||
"\e[#{unquote(code)}#{unquote(terminator)}"
|
||||
end
|
||||
|
||||
defp format_sequence(unquote(name)) do
|
||||
unquote(name)()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule IO.ANSI do
|
||||
@moduledoc """
|
||||
Functionality to render ANSI escape sequences
|
||||
(http://en.wikipedia.org/wiki/ANSI_escape_code) — characters embedded
|
||||
in text used to control formatting, color, and other output options
|
||||
on video text terminals.
|
||||
"""
|
||||
|
||||
import IO.ANSI.Sequence
|
||||
|
||||
@typep ansicode :: atom()
|
||||
@typep ansilist :: maybe_improper_list(char() | ansicode() | binary() | ansilist(), binary() | ansicode() | [])
|
||||
@type ansidata :: ansilist() | ansicode() | binary()
|
||||
|
||||
@doc """
|
||||
Checks if ANSI coloring is supported and enabled on this machine.
|
||||
|
||||
This function simply reads the configuration value for
|
||||
`:ansi_enabled` in the `:elixir` application. The value is by
|
||||
default false unless Elixir can detect during startup that
|
||||
both `stdout` and `stderr` are terminals.
|
||||
"""
|
||||
@spec enabled? :: boolean
|
||||
def enabled? do
|
||||
Application.get_env(:elixir, :ansi_enabled, false)
|
||||
end
|
||||
|
||||
@doc "Resets all attributes"
|
||||
defsequence :reset, 0
|
||||
|
||||
@doc "Bright (increased intensity) or Bold"
|
||||
defsequence :bright, 1
|
||||
|
||||
@doc "Faint (decreased intensity), not widely supported"
|
||||
defsequence :faint, 2
|
||||
|
||||
@doc "Italic: on. Not widely supported. Sometimes treated as inverse."
|
||||
defsequence :italic, 3
|
||||
|
||||
@doc "Underline: Single"
|
||||
defsequence :underline, 4
|
||||
|
||||
@doc "Blink: Slow. Less than 150 per minute"
|
||||
defsequence :blink_slow, 5
|
||||
|
||||
@doc "Blink: Rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported"
|
||||
defsequence :blink_rapid, 6
|
||||
|
||||
@doc "Image: Negative. Swap foreground and background"
|
||||
defsequence :inverse, 7
|
||||
|
||||
@doc "Image: Negative. Swap foreground and background"
|
||||
defsequence :reverse, 7
|
||||
|
||||
@doc "Conceal. Not widely supported"
|
||||
defsequence :conceal, 8
|
||||
|
||||
@doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported."
|
||||
defsequence :crossed_out, 9
|
||||
|
||||
@doc "Sets primary (default) font"
|
||||
defsequence :primary_font, 10
|
||||
|
||||
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
|
||||
@doc "Sets alternative font #{font_n}"
|
||||
defsequence :"font_#{font_n}", font_n + 10
|
||||
end
|
||||
|
||||
@doc "Normal color or intensity"
|
||||
defsequence :normal, 22
|
||||
|
||||
@doc "Not italic"
|
||||
defsequence :not_italic, 23
|
||||
|
||||
@doc "Underline: None"
|
||||
defsequence :no_underline, 24
|
||||
|
||||
@doc "Blink: off"
|
||||
defsequence :blink_off, 25
|
||||
|
||||
colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
|
||||
colors = Enum.zip(0..(length(colors)-1), colors)
|
||||
|
||||
for {code, color} <- colors do
|
||||
@doc "Sets foreground color to #{color}"
|
||||
defsequence color, code + 30
|
||||
|
||||
@doc "Sets background color to #{color}"
|
||||
defsequence :"#{color}_background", code + 40
|
||||
end
|
||||
|
||||
@doc "Default text color"
|
||||
defsequence :default_color, 39
|
||||
|
||||
@doc "Default background color"
|
||||
defsequence :default_background, 49
|
||||
|
||||
@doc "Framed"
|
||||
defsequence :framed, 51
|
||||
|
||||
@doc "Encircled"
|
||||
defsequence :encircled, 52
|
||||
|
||||
@doc "Overlined"
|
||||
defsequence :overlined, 53
|
||||
|
||||
@doc "Not framed or encircled"
|
||||
defsequence :not_framed_encircled, 54
|
||||
|
||||
@doc "Not overlined"
|
||||
defsequence :not_overlined, 55
|
||||
|
||||
@doc "Send cursor home"
|
||||
defsequence :home, "", "H"
|
||||
|
||||
@doc "Clear screen"
|
||||
defsequence :clear, "2", "J"
|
||||
|
||||
defp format_sequence(other) do
|
||||
raise ArgumentError, "invalid ANSI sequence specification: #{other}"
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Formats a chardata-like argument by converting named ANSI sequences into actual
|
||||
ANSI codes.
|
||||
|
||||
The named sequences are represented by atoms.
|
||||
|
||||
It will also append an `IO.ANSI.reset` to the chardata when a conversion is
|
||||
performed. If you don't want this behaviour, use `format_fragment/2`.
|
||||
|
||||
An optional boolean parameter can be passed to enable or disable
|
||||
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
|
||||
By default checks if ANSI is enabled using the `enabled?/0` function.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.ANSI.format(["Hello, ", :red, :bright, "world!"], true)
|
||||
[[[[[[], "Hello, "] | "\e[31m"] | "\e[1m"], "world!"] | "\e[0m"]
|
||||
|
||||
"""
|
||||
def format(chardata, emit \\ enabled?) when is_boolean(emit) do
|
||||
do_format(chardata, [], [], emit, :maybe)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Formats a chardata-like argument by converting named ANSI sequences into actual
|
||||
ANSI codes.
|
||||
|
||||
The named sequences are represented by atoms.
|
||||
|
||||
An optional boolean parameter can be passed to enable or disable
|
||||
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
|
||||
By default checks if ANSI is enabled using the `enabled?/0` function.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.ANSI.format_fragment([:bright, 'Word'], true)
|
||||
[[[[[[] | "\e[1m"], 87], 111], 114], 100]
|
||||
|
||||
"""
|
||||
def format_fragment(chardata, emit \\ enabled?) when is_boolean(emit) do
|
||||
do_format(chardata, [], [], emit, false)
|
||||
end
|
||||
|
||||
defp do_format([term | rest], rem, acc, emit, append_reset) do
|
||||
do_format(term, [rest | rem], acc, emit, append_reset)
|
||||
end
|
||||
|
||||
defp do_format(term, rem, acc, true, append_reset) when is_atom(term) do
|
||||
do_format([], rem, [acc | format_sequence(term)], true, !!append_reset)
|
||||
end
|
||||
|
||||
defp do_format(term, rem, acc, false, append_reset) when is_atom(term) do
|
||||
do_format([], rem, acc, false, append_reset)
|
||||
end
|
||||
|
||||
defp do_format(term, rem, acc, emit, append_reset) when not is_list(term) do
|
||||
do_format([], rem, [acc | [term]], emit, append_reset)
|
||||
end
|
||||
|
||||
defp do_format([], [next | rest], acc, emit, append_reset) do
|
||||
do_format(next, rest, acc, emit, append_reset)
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, true, true) do
|
||||
[acc | IO.ANSI.reset]
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, _emit, _append_reset) do
|
||||
acc
|
||||
end
|
||||
end
|
||||
@@ -1,479 +0,0 @@
|
||||
defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@bullets [?*, ?-, ?+]
|
||||
|
||||
@doc """
|
||||
The default options used by this module.
|
||||
|
||||
The supported values are:
|
||||
|
||||
* `:enabled` - toggles coloring on and off (true)
|
||||
* `:doc_bold` - bold text (bright)
|
||||
* `:doc_code` - code blocks (cyan, bright)
|
||||
* `:doc_headings` - h1 and h2 headings (yellow, bright)
|
||||
* `:doc_inline_code` - inline code (cyan)
|
||||
* `:doc_table_heading` - style for table headings
|
||||
* `:doc_title` - top level heading (reverse, yellow, bright)
|
||||
* `:doc_underline` - underlined text (underline)
|
||||
* `:width` - the width to format the text (80)
|
||||
|
||||
Values for the color settings are strings with
|
||||
comma-separated ANSI values.
|
||||
"""
|
||||
def default_options do
|
||||
[enabled: true,
|
||||
doc_bold: [:bright],
|
||||
doc_code: [:cyan, :bright],
|
||||
doc_headings: [:yellow, :bright],
|
||||
doc_inline_code: [:cyan],
|
||||
doc_table_heading: [:reverse],
|
||||
doc_title: [:reverse, :yellow, :bright],
|
||||
doc_underline: [:underline],
|
||||
width: 80]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the head of the documentation (i.e. the function signature).
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
def print_heading(heading, options \\ []) do
|
||||
IO.puts IO.ANSI.reset
|
||||
options = Keyword.merge(default_options, options)
|
||||
width = options[:width]
|
||||
padding = div(width + String.length(heading), 2)
|
||||
heading = heading |> String.rjust(padding) |> String.ljust(width)
|
||||
write(:doc_title, heading, options)
|
||||
newline_after_block
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the documentation body.
|
||||
|
||||
In addition to the printing string, takes a set of options
|
||||
defined in `default_options/1`.
|
||||
"""
|
||||
def print(doc, options \\ []) do
|
||||
options = Keyword.merge(default_options, options)
|
||||
doc
|
||||
|> String.split(["\r\n","\n"], trim: false)
|
||||
|> Enum.map(&String.rstrip/1)
|
||||
|> process([], "", options)
|
||||
end
|
||||
|
||||
defp process([], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
end
|
||||
|
||||
defp process(["# " <> heading | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
write_h1(String.strip(heading), options)
|
||||
process(rest, [], "", options)
|
||||
end
|
||||
|
||||
defp process(["## " <> heading | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
write_h2(String.strip(heading), options)
|
||||
process(rest, [], "", options)
|
||||
end
|
||||
|
||||
defp process(["### " <> heading | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
write_h3(String.strip(heading), indent, options)
|
||||
process(rest, [], "", options)
|
||||
end
|
||||
|
||||
defp process(["" | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
defp process([" " <> line | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
process_code(rest, [line], indent, options)
|
||||
end
|
||||
|
||||
defp process(all=[line | rest], text, indent, options) do
|
||||
{stripped, count} = strip_spaces(line, 0, :infinity)
|
||||
if is_table_line?(stripped) and rest != [] and is_table_line?(hd(rest)) do
|
||||
write_text(text, indent, options)
|
||||
process_table(all, indent, options)
|
||||
else
|
||||
case stripped do
|
||||
<<bullet, ?\s, item :: binary>> when bullet in @bullets ->
|
||||
write_text(text, indent, options)
|
||||
process_list("• ", item, rest, count, indent, options)
|
||||
<<d1, ?., ?\s, item :: binary>> when d1 in ?0..?9 ->
|
||||
write_text(text, indent, options)
|
||||
process_list(<<d1, ?., ?\s>>, item, rest, count, indent, options)
|
||||
<<d1, d2, ?., ?\s, item :: binary>> when d1 in ?0..?9 and d2 in ?0..?9 ->
|
||||
write_text(text, indent, options)
|
||||
process_list(<<d1, d2, ?., ?\s>>, item, rest, count, indent, options)
|
||||
_ ->
|
||||
process(rest, [stripped | text], indent, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Headings
|
||||
|
||||
defp write_h1(heading, options) do
|
||||
write_h2(String.upcase(heading), options)
|
||||
end
|
||||
|
||||
defp write_h2(heading, options) do
|
||||
write(:doc_headings, heading, options)
|
||||
newline_after_block
|
||||
end
|
||||
|
||||
defp write_h3(heading, indent, options) do
|
||||
IO.write(indent)
|
||||
write(:doc_headings, heading, options)
|
||||
newline_after_block
|
||||
end
|
||||
|
||||
## Lists
|
||||
|
||||
defp process_list(entry, line, rest, count, indent, options) do
|
||||
# The first list always win some extra padding
|
||||
if indent == "", do: entry = " " <> entry
|
||||
new_indent = indent <> String.duplicate(" ", String.length(entry))
|
||||
|
||||
{contents, rest, done} = process_list_next(rest, count, byte_size(new_indent), [])
|
||||
process(contents, [indent <> entry <> line, :no_wrap], new_indent, options)
|
||||
|
||||
if done, do: newline_after_block()
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
defp process_list_next([line | rest], count, max, acc) do
|
||||
{stripped, next_count} = strip_spaces(line, 0, max)
|
||||
case process_list_next_kind(stripped, rest, count, next_count) do
|
||||
:next -> process_list_next(rest, count, max, [stripped | acc])
|
||||
:done -> {Enum.reverse(acc), [line | rest], true}
|
||||
:list -> {Enum.reverse(acc), [line | rest], false}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_list_next([], _count, _max, acc) do
|
||||
{Enum.reverse(acc), [], true}
|
||||
end
|
||||
|
||||
defp process_list_next_kind(stripped, rest, count, next_count) do
|
||||
case {stripped, rest} do
|
||||
{<<bullet, ?\s, _ :: binary>>, _} when bullet in @bullets and next_count <= count ->
|
||||
:list
|
||||
{<<d1, ?., ?\s, _ :: binary>>, _} when d1 in ?0..?9 and next_count <= count ->
|
||||
:list
|
||||
{<<d1, d2, ?., ?\s, _ :: binary>>, _} when d1 in ?0..?9 and d2 in ?0..?9 and next_count <= count ->
|
||||
:list
|
||||
{"", [" " <> _ | _]} ->
|
||||
:next
|
||||
{"", _} ->
|
||||
:done
|
||||
_ ->
|
||||
:next
|
||||
end
|
||||
end
|
||||
|
||||
## Text
|
||||
|
||||
defp write_text(text, indent, options) do
|
||||
case Enum.reverse(text) do
|
||||
[:no_wrap|rest] -> write_text(rest, indent, options, true)
|
||||
rest -> write_text(rest, indent, options, false)
|
||||
end
|
||||
end
|
||||
|
||||
defp write_text([], _indent, _options, _no_wrap) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_text(lines, indent, options, no_wrap) do
|
||||
lines
|
||||
|> Enum.join(" ")
|
||||
|> handle_links
|
||||
|> handle_inline(nil, [], [], options)
|
||||
|> String.split(~r{\s})
|
||||
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap)
|
||||
|
||||
unless no_wrap, do: newline_after_block()
|
||||
end
|
||||
|
||||
## Code blocks
|
||||
|
||||
defp process_code([], code, indent, options) do
|
||||
write_code(code, indent, options)
|
||||
end
|
||||
|
||||
# Blank line between code blocks
|
||||
defp process_code(["", " " <> line | rest], code, indent, options) do
|
||||
process_code(rest, [line, "" | code], indent, options)
|
||||
end
|
||||
|
||||
defp process_code([" " <> line | rest], code, indent, options) do
|
||||
process_code(rest, [line|code], indent, options)
|
||||
end
|
||||
|
||||
defp process_code(rest, code, indent, options) do
|
||||
write_code(code, indent, options)
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
defp write_code(code, indent, options) do
|
||||
write(:doc_code, "#{indent}┃ #{Enum.join(Enum.reverse(code), "\n#{indent}┃ ")}", options)
|
||||
newline_after_block
|
||||
end
|
||||
|
||||
## Tables
|
||||
|
||||
defp process_table(lines, indent, options) do
|
||||
{table, rest} = Enum.split_while(lines, &is_table_line?/1)
|
||||
table_lines(table, options)
|
||||
newline_after_block
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
defp table_lines(lines, options) do
|
||||
lines = Enum.map(lines, &split_into_columns(&1, options))
|
||||
count = Enum.map(lines, &length/1) |> Enum.max
|
||||
lines = Enum.map(lines, &pad_to_number_of_columns(&1, count))
|
||||
|
||||
widths = for line <- lines, do:
|
||||
(for {_col, length} <- line, do: length)
|
||||
|
||||
col_widths = Enum.reduce(widths,
|
||||
List.duplicate(0, count),
|
||||
&max_column_widths/2)
|
||||
|
||||
render_table(lines, col_widths, options)
|
||||
end
|
||||
|
||||
defp split_into_columns(line, options) do
|
||||
line
|
||||
|> String.strip(?|)
|
||||
|> String.strip()
|
||||
|> String.split(~r/\s\|\s/)
|
||||
|> Enum.map(&render_column(&1, options))
|
||||
end
|
||||
|
||||
defp render_column(col, options) do
|
||||
col = col
|
||||
|> String.replace(~r/\\ \|/x, "|")
|
||||
|> handle_links
|
||||
|> handle_inline(nil, [], [], options)
|
||||
{col, length_without_escape(col, 0)}
|
||||
end
|
||||
|
||||
defp pad_to_number_of_columns(cols, col_count),
|
||||
do: cols ++ List.duplicate({"", 0}, col_count - length(cols))
|
||||
|
||||
defp max_column_widths(cols, widths),
|
||||
do: Enum.zip(cols, widths) |> Enum.map(fn {a,b} -> max(a,b) end)
|
||||
|
||||
# If second line is heading separator, use the heading style on the first
|
||||
defp render_table([first, second | rest], widths, options) do
|
||||
combined = Enum.zip(first, widths)
|
||||
if table_header?(second) do
|
||||
draw_table_row(combined, options, :heading)
|
||||
render_table(rest, widths, options)
|
||||
else
|
||||
draw_table_row(combined, options)
|
||||
render_table([second | rest], widths, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp render_table([first | rest], widths, options) do
|
||||
combined = Enum.zip(first, widths)
|
||||
draw_table_row(combined, options)
|
||||
render_table(rest, widths, options)
|
||||
end
|
||||
|
||||
defp render_table([], _, _),
|
||||
do: nil
|
||||
|
||||
defp table_header?(row), do:
|
||||
Enum.all?(row, fn {col, _} -> col =~ ~r/^:?-+:?$/ end)
|
||||
|
||||
defp draw_table_row(cols_and_widths, options, heading \\ false) do
|
||||
columns =
|
||||
Enum.map_join(cols_and_widths, " | ", fn {{col, length}, width} ->
|
||||
col <> String.duplicate(" ", width - length)
|
||||
end)
|
||||
|
||||
if heading do
|
||||
write(:doc_table_heading, columns, options)
|
||||
else
|
||||
IO.puts columns
|
||||
end
|
||||
end
|
||||
|
||||
defp is_table_line?(line) do
|
||||
Regex.match?(~r'''
|
||||
( ^ \s{0,3} \| (?: [^|]+ \|)+ \s* $ )
|
||||
|
|
||||
(\s \| \s)
|
||||
'''x, line)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp strip_spaces(" " <> line, acc, max) when acc < max,
|
||||
do: strip_spaces(line, acc + 1, max)
|
||||
defp strip_spaces(rest, acc, _max),
|
||||
do: {rest, acc}
|
||||
|
||||
defp write(style, string, options) do
|
||||
IO.puts [color(style, options), string, IO.ANSI.reset]
|
||||
end
|
||||
|
||||
defp write_with_wrap([], _available, _indent, _first) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_with_wrap(words, available, indent, first) do
|
||||
{words, rest} = take_words(words, available, [])
|
||||
IO.puts (if first, do: "", else: indent) <> Enum.join(words, " ")
|
||||
write_with_wrap(rest, available, indent, false)
|
||||
end
|
||||
|
||||
defp take_words([word|words], available, acc) do
|
||||
available = available - length_without_escape(word, 0)
|
||||
|
||||
cond do
|
||||
# It fits, take one for space and continue decreasing
|
||||
available > 0 ->
|
||||
take_words(words, available - 1, [word|acc])
|
||||
|
||||
# No space but we got no words
|
||||
acc == [] ->
|
||||
{[word], words}
|
||||
|
||||
# Otherwise
|
||||
true ->
|
||||
{Enum.reverse(acc), [word|words]}
|
||||
end
|
||||
end
|
||||
|
||||
defp take_words([], _available, acc) do
|
||||
{Enum.reverse(acc), []}
|
||||
end
|
||||
|
||||
defp length_without_escape(<< ?\e, ?[, _, _, ?m, rest :: binary >>, count) do
|
||||
length_without_escape(rest, count)
|
||||
end
|
||||
|
||||
defp length_without_escape(<< ?\e, ?[, _, ?m, rest :: binary >>, count) do
|
||||
length_without_escape(rest, count)
|
||||
end
|
||||
|
||||
defp length_without_escape(rest, count) do
|
||||
case String.next_grapheme(rest) do
|
||||
{_, rest} -> length_without_escape(rest, count + 1)
|
||||
nil -> count
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_links(text) do
|
||||
text
|
||||
|> remove_square_brackets_in_link
|
||||
|> escape_underlines_in_link
|
||||
end
|
||||
|
||||
defp escape_underlines_in_link(text) do
|
||||
case Regex.match?(~r{.*(https?\S*)}, text) do
|
||||
true -> Regex.replace(~r{_}, text, "\\\\_")
|
||||
_ -> text
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_square_brackets_in_link(text) do
|
||||
Regex.replace(~r{\[(.*?)\]\((.*?)\)}, text, "\\1 (\\2)")
|
||||
end
|
||||
|
||||
# Single inline quotes.
|
||||
@single [?`, ?_, ?*]
|
||||
|
||||
# ` does not require space in between
|
||||
@spaced [?_, ?*]
|
||||
|
||||
# Clauses for handling spaces
|
||||
defp handle_inline(<<?*, ?*, ?\s, rest :: binary>>, nil, buffer, acc, options) do
|
||||
handle_inline(rest, nil, [?\s, ?*, ?*|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, ?\s, rest :: binary>>, nil, buffer, acc, options) when mark in @spaced do
|
||||
handle_inline(rest, nil, [?\s, mark|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<?\s, ?*, ?*, rest :: binary>>, limit, buffer, acc, options) do
|
||||
handle_inline(rest, limit, [?*, ?*, ?\s|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<?\s, mark, rest :: binary>>, limit, buffer, acc, options) when mark in @spaced do
|
||||
handle_inline(rest, limit, [mark, ?\s|buffer], acc, options)
|
||||
end
|
||||
|
||||
# Clauses for handling escape
|
||||
defp handle_inline(<<?\\, ?\\, rest :: binary>>, limit, buffer, acc, options) do
|
||||
handle_inline(rest, limit, [?\\|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<?\\, ?*, ?*, rest :: binary>>, limit, buffer, acc, options) do
|
||||
handle_inline(rest, limit, [?*, ?*|buffer], acc, options)
|
||||
end
|
||||
|
||||
# A escape is not valid inside `
|
||||
defp handle_inline(<<?\\, mark, rest :: binary>>, limit, buffer, acc, options)
|
||||
when mark in [?_, ?*, ?`] and not(mark == limit and mark == ?`) do
|
||||
handle_inline(rest, limit, [mark|buffer], acc, options)
|
||||
end
|
||||
|
||||
# Inline start
|
||||
defp handle_inline(<<?*, ?*, rest :: binary>>, nil, buffer, acc, options) when rest != "" do
|
||||
handle_inline(rest, ?d, ["**"], [Enum.reverse(buffer)|acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, rest :: binary>>, nil, buffer, acc, options) when rest != "" and mark in @single do
|
||||
handle_inline(rest, mark, [<<mark>>], [Enum.reverse(buffer)|acc], options)
|
||||
end
|
||||
|
||||
# Inline end
|
||||
defp handle_inline(<<?*, ?*, rest :: binary>>, ?d, buffer, acc, options) do
|
||||
handle_inline(rest, nil, [], [inline_buffer(buffer, options)|acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, rest :: binary>>, mark, buffer, acc, options) when mark in @single do
|
||||
handle_inline(rest, nil, [], [inline_buffer(buffer, options)|acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<char, rest :: binary>>, mark, buffer, acc, options) do
|
||||
handle_inline(rest, mark, [char|buffer], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<>>, _mark, buffer, acc, _options) do
|
||||
IO.iodata_to_binary Enum.reverse([Enum.reverse(buffer)|acc])
|
||||
end
|
||||
|
||||
defp inline_buffer(buffer, options) do
|
||||
[h|t] = Enum.reverse([IO.ANSI.reset|buffer])
|
||||
[color_for(h, options)|t]
|
||||
end
|
||||
|
||||
defp color_for(mark, colors) do
|
||||
case mark do
|
||||
"`" -> color(:doc_inline_code, colors)
|
||||
"_" -> color(:doc_underline, colors)
|
||||
"*" -> color(:doc_bold, colors)
|
||||
"**" -> color(:doc_bold, colors)
|
||||
end
|
||||
end
|
||||
|
||||
defp color(style, colors) do
|
||||
color = colors[style]
|
||||
IO.ANSI.format_fragment(color, colors[:enabled])
|
||||
end
|
||||
|
||||
defp newline_after_block, do: IO.puts(IO.ANSI.reset)
|
||||
end
|
||||
@@ -1,71 +0,0 @@
|
||||
defmodule IO.StreamError do
|
||||
defexception [:reason, :message]
|
||||
|
||||
def exception(opts) do
|
||||
reason = opts[:reason]
|
||||
formatted = IO.iodata_to_binary(:file.format_error(reason))
|
||||
%IO.StreamError{message: "error during streaming: #{formatted}", reason: reason}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule IO.Stream do
|
||||
@moduledoc """
|
||||
Defines a `IO.Stream` struct returned by `IO.stream/2` and `IO.binstream/2`.
|
||||
|
||||
The following fields are public:
|
||||
|
||||
* `device` - the IO device
|
||||
* `raw` - a boolean indicating if bin functions should be used
|
||||
* `line_or_bytes` - if reading should read lines or a given amount of bytes
|
||||
|
||||
"""
|
||||
|
||||
defstruct device: nil, raw: true, line_or_bytes: :line
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@doc false
|
||||
def __build__(device, raw, line_or_bytes) do
|
||||
%IO.Stream{device: device, raw: raw, line_or_bytes: line_or_bytes}
|
||||
end
|
||||
|
||||
defimpl Collectable do
|
||||
def empty(stream) do
|
||||
stream
|
||||
end
|
||||
|
||||
def into(%{device: device, raw: raw} = stream) do
|
||||
{:ok, into(stream, device, raw)}
|
||||
end
|
||||
|
||||
defp into(stream, device, raw) do
|
||||
fn
|
||||
:ok, {:cont, x} ->
|
||||
case raw do
|
||||
true -> IO.binwrite(device, x)
|
||||
false -> IO.write(device, x)
|
||||
end
|
||||
:ok, _ -> stream
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enumerable do
|
||||
def reduce(%{device: device, raw: raw, line_or_bytes: line_or_bytes}, acc, fun) do
|
||||
next_fun =
|
||||
case raw do
|
||||
true -> &IO.each_binstream(&1, line_or_bytes)
|
||||
false -> &IO.each_stream(&1, line_or_bytes)
|
||||
end
|
||||
Stream.resource(fn -> device end, next_fun, &(&1)).(acc, fun)
|
||||
end
|
||||
|
||||
def count(_stream) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
|
||||
def member?(_stream, _term) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,430 +0,0 @@
|
||||
defmodule Kernel.CLI do
|
||||
@moduledoc false
|
||||
|
||||
@blank_config %{commands: [], output: ".", compile: [],
|
||||
halt: true, compiler_options: [], errors: [],
|
||||
pa: [], pz: [], verbose_compile: false}
|
||||
|
||||
@doc """
|
||||
This is the API invoked by Elixir boot process.
|
||||
"""
|
||||
def main(argv) do
|
||||
argv = for arg <- argv, do: IO.chardata_to_string(arg)
|
||||
|
||||
{config, argv} = parse_argv(argv)
|
||||
:elixir_code_server.cast({:paths, config.pa, config.pz})
|
||||
System.argv(argv)
|
||||
|
||||
run fn _ ->
|
||||
errors = process_commands(config)
|
||||
|
||||
if errors != [] do
|
||||
Enum.each(errors, &IO.puts(:stderr, &1))
|
||||
System.halt(1)
|
||||
end
|
||||
end, config.halt
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs the given function by catching any failure
|
||||
and printing them to stdout. `at_exit` hooks are
|
||||
also invoked before exiting.
|
||||
|
||||
This function is used by Elixir's CLI and also
|
||||
by escripts generated by Elixir.
|
||||
"""
|
||||
def run(fun, halt \\ true) do
|
||||
res = exec_fun(fun, {:ok, 0})
|
||||
if elem(res, 0) == :shutdown or halt do
|
||||
{_, int} = at_exit(res)
|
||||
System.halt(int)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def parse_argv(argv) do
|
||||
parse_argv(argv, @blank_config)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def process_commands(config) do
|
||||
results = Enum.map(Enum.reverse(config.commands), &process_command(&1, config))
|
||||
errors = for {:error, msg} <- results, do: msg
|
||||
Enum.reverse(config.errors, errors)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp at_exit(res) do
|
||||
hooks = :elixir_code_server.call(:flush_at_exit)
|
||||
res = Enum.reduce(hooks, res, &exec_fun/2)
|
||||
if hooks == [], do: res, else: at_exit(res)
|
||||
end
|
||||
|
||||
defp exec_fun(fun, res) when is_function(fun, 1) and is_tuple(res) do
|
||||
parent = self()
|
||||
|
||||
{pid, ref} =
|
||||
spawn_monitor fn ->
|
||||
try do
|
||||
fun.(elem(res, 1))
|
||||
catch
|
||||
:exit, {:shutdown, int} when is_integer(int) ->
|
||||
send parent, {self, {:shutdown, int}}
|
||||
exit({:shutdown, int})
|
||||
:exit, reason
|
||||
when reason == :normal
|
||||
when reason == :shutdown
|
||||
when tuple_size(reason) == 2 and elem(reason, 0) == :shutdown ->
|
||||
send parent, {self, {:shutdown, 0}}
|
||||
exit(reason)
|
||||
kind, reason ->
|
||||
stack = System.stacktrace
|
||||
print_error(kind, reason, stack)
|
||||
send parent, {self, {:shutdown, 1}}
|
||||
:erlang.raise(kind, reason, stack)
|
||||
else
|
||||
_ ->
|
||||
send parent, {self, res}
|
||||
end
|
||||
end
|
||||
|
||||
receive do
|
||||
{^pid, res} ->
|
||||
:erlang.demonitor(ref, [:flush])
|
||||
res
|
||||
{:DOWN, ^ref, _, _, other} ->
|
||||
print_error({:EXIT, pid}, other, [])
|
||||
{:shutdown, 1}
|
||||
end
|
||||
end
|
||||
|
||||
defp shared_option?(list, config, callback) do
|
||||
case parse_shared(list, config) do
|
||||
{[h|hs], _} when h == hd(list) ->
|
||||
new_config = %{config | errors: ["#{h} : Unknown option" | config.errors]}
|
||||
callback.(hs, new_config)
|
||||
{new_list, new_config} ->
|
||||
callback.(new_list, new_config)
|
||||
end
|
||||
end
|
||||
|
||||
defp print_error(kind, reason, trace) do
|
||||
IO.puts :stderr, Exception.format(kind, reason, prune_stacktrace(trace))
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_translator,
|
||||
:elixir_expand, :elixir_lexical]
|
||||
|
||||
defp prune_stacktrace([{mod, _, _, _}|t]) when mod in @elixir_internals do
|
||||
prune_stacktrace(t)
|
||||
end
|
||||
|
||||
defp prune_stacktrace([{__MODULE__, :wrapper, 1, _}|_]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp prune_stacktrace([h|t]) do
|
||||
[h|prune_stacktrace(t)]
|
||||
end
|
||||
|
||||
defp prune_stacktrace([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
# Parse shared options
|
||||
|
||||
defp parse_shared([opt|_t], _config) when opt in ["-v", "--version"] do
|
||||
IO.puts "Elixir #{System.version}"
|
||||
System.halt 0
|
||||
end
|
||||
|
||||
defp parse_shared(["-pa", h|t], config) do
|
||||
paths = expand_code_path(h)
|
||||
Enum.each(paths, &:code.add_patha/1)
|
||||
parse_shared t, %{config | pa: config.pa ++ paths}
|
||||
end
|
||||
|
||||
defp parse_shared(["-pz", h|t], config) do
|
||||
paths = expand_code_path(h)
|
||||
Enum.each(paths, &:code.add_pathz/1)
|
||||
parse_shared t, %{config | pz: config.pz ++ paths}
|
||||
end
|
||||
|
||||
defp parse_shared(["--app", h|t], config) do
|
||||
parse_shared t, %{config | commands: [{:app, h} | config.commands]}
|
||||
end
|
||||
|
||||
defp parse_shared(["--no-halt"|t], config) do
|
||||
parse_shared t, %{config | halt: false}
|
||||
end
|
||||
|
||||
defp parse_shared(["-e", h|t], config) do
|
||||
parse_shared t, %{config | commands: [{:eval, h} | config.commands]}
|
||||
end
|
||||
|
||||
defp parse_shared(["-r", h|t], config) do
|
||||
parse_shared t, %{config | commands: [{:require, h} | config.commands]}
|
||||
end
|
||||
|
||||
defp parse_shared(["-pr", h|t], config) do
|
||||
parse_shared t, %{config | commands: [{:parallel_require, h} | config.commands]}
|
||||
end
|
||||
|
||||
defp parse_shared([erl, _|t], config) when erl in ["--erl", "--sname", "--name", "--cookie"] do
|
||||
parse_shared t, config
|
||||
end
|
||||
|
||||
defp parse_shared([erl|t], config) when erl in ["--detached", "--hidden", "--werl"] do
|
||||
parse_shared t, config
|
||||
end
|
||||
|
||||
defp parse_shared(list, config) do
|
||||
{list, config}
|
||||
end
|
||||
|
||||
defp expand_code_path(path) do
|
||||
path = Path.expand(path)
|
||||
case Path.wildcard(path) do
|
||||
[] -> [to_char_list(path)]
|
||||
list -> Enum.map(list, &to_char_list/1)
|
||||
end
|
||||
end
|
||||
|
||||
# Process init options
|
||||
|
||||
defp parse_argv(["--"|t], config) do
|
||||
{config, t}
|
||||
end
|
||||
|
||||
defp parse_argv(["+elixirc"|t], config) do
|
||||
parse_compiler t, config
|
||||
end
|
||||
|
||||
defp parse_argv(["+iex"|t], config) do
|
||||
parse_iex t, config
|
||||
end
|
||||
|
||||
defp parse_argv(["-S", h|t], config) do
|
||||
{%{config | commands: [{:script, h} | config.commands]}, t}
|
||||
end
|
||||
|
||||
defp parse_argv([h|t] = list, config) do
|
||||
case h do
|
||||
"-" <> _ ->
|
||||
shared_option? list, config, &parse_argv(&1, &2)
|
||||
_ ->
|
||||
if Keyword.has_key?(config.commands, :eval) do
|
||||
{config, list}
|
||||
else
|
||||
{%{config | commands: [{:file, h} | config.commands]}, t}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_argv([], config) do
|
||||
{config, []}
|
||||
end
|
||||
|
||||
# Parse compiler options
|
||||
|
||||
defp parse_compiler(["--"|t], config) do
|
||||
{config, t}
|
||||
end
|
||||
|
||||
defp parse_compiler(["-o", h|t], config) do
|
||||
parse_compiler t, %{config | output: h}
|
||||
end
|
||||
|
||||
defp parse_compiler(["--no-docs"|t], config) do
|
||||
parse_compiler t, %{config | compiler_options: [{:docs, false} | config.compiler_options]}
|
||||
end
|
||||
|
||||
defp parse_compiler(["--no-debug-info"|t], config) do
|
||||
parse_compiler t, %{config | compiler_options: [{:debug_info, false} | config.compiler_options]}
|
||||
end
|
||||
|
||||
defp parse_compiler(["--ignore-module-conflict"|t], config) do
|
||||
parse_compiler t, %{config | compiler_options: [{:ignore_module_conflict, true} | config.compiler_options]}
|
||||
end
|
||||
|
||||
defp parse_compiler(["--warnings-as-errors"|t], config) do
|
||||
parse_compiler t, %{config | compiler_options: [{:warnings_as_errors, true} | config.compiler_options]}
|
||||
end
|
||||
|
||||
defp parse_compiler(["--verbose"|t], config) do
|
||||
parse_compiler t, %{config | verbose_compile: true}
|
||||
end
|
||||
|
||||
defp parse_compiler([h|t] = list, config) do
|
||||
case h do
|
||||
"-" <> _ ->
|
||||
shared_option? list, config, &parse_compiler(&1, &2)
|
||||
_ ->
|
||||
pattern = if File.dir?(h), do: "#{h}/**/*.ex", else: h
|
||||
parse_compiler t, %{config | compile: [pattern | config.compile]}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_compiler([], config) do
|
||||
{%{config | commands: [{:compile, config.compile}|config.commands]}, []}
|
||||
end
|
||||
|
||||
# Parse iex options
|
||||
|
||||
defp parse_iex(["--"|t], config) do
|
||||
{config, t}
|
||||
end
|
||||
|
||||
# This clause is here so that Kernel.CLI does not
|
||||
# error out with "unknown option"
|
||||
defp parse_iex(["--dot-iex", _|t], config) do
|
||||
parse_iex t, config
|
||||
end
|
||||
|
||||
defp parse_iex([opt, _|t], config) when opt in ["--remsh"] do
|
||||
parse_iex t, config
|
||||
end
|
||||
|
||||
defp parse_iex(["-S", h|t], config) do
|
||||
{%{config | commands: [{:script, h} | config.commands]}, t}
|
||||
end
|
||||
|
||||
defp parse_iex([h|t] = list, config) do
|
||||
case h do
|
||||
"-" <> _ ->
|
||||
shared_option? list, config, &parse_iex(&1, &2)
|
||||
_ ->
|
||||
{%{config | commands: [{:file, h} | config.commands]}, t}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_iex([], config) do
|
||||
{config, []}
|
||||
end
|
||||
|
||||
# Process commands
|
||||
|
||||
defp process_command({:cookie, h}, _config) do
|
||||
if Node.alive? do
|
||||
wrapper fn -> Node.set_cookie(String.to_atom(h)) end
|
||||
else
|
||||
{:error, "--cookie : Cannot set cookie if the node is not alive (set --name or --sname)"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:eval, expr}, _config) when is_binary(expr) do
|
||||
wrapper fn -> Code.eval_string(expr, []) end
|
||||
end
|
||||
|
||||
defp process_command({:app, app}, _config) when is_binary(app) do
|
||||
case Application.ensure_all_started(String.to_atom(app)) do
|
||||
{:error, {app, reason}} ->
|
||||
{:error, "--app : Could not start application #{app}: " <>
|
||||
Application.format_error(reason)}
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:script, file}, _config) when is_binary(file) do
|
||||
if exec = find_elixir_executable(file) do
|
||||
wrapper fn -> Code.require_file(exec) end
|
||||
else
|
||||
{:error, "-S : Could not find executable #{file}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:file, file}, _config) when is_binary(file) do
|
||||
if File.regular?(file) do
|
||||
wrapper fn -> Code.require_file(file) end
|
||||
else
|
||||
{:error, "No file named #{file}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:require, pattern}, _config) when is_binary(pattern) do
|
||||
files = filter_patterns(pattern)
|
||||
|
||||
if files != [] do
|
||||
wrapper fn -> Enum.map files, &Code.require_file(&1) end
|
||||
else
|
||||
{:error, "-r : No files matched pattern #{pattern}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:parallel_require, pattern}, _config) when is_binary(pattern) do
|
||||
files = filter_patterns(pattern)
|
||||
|
||||
if files != [] do
|
||||
wrapper fn -> Kernel.ParallelRequire.files(files) end
|
||||
else
|
||||
{:error, "-pr : No files matched pattern #{pattern}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_command({:compile, patterns}, config) do
|
||||
# If ensuring the dir returns an error no files will be found.
|
||||
_ = :filelib.ensure_dir(:filename.join(config.output, "."))
|
||||
|
||||
case filter_multiple_patterns(patterns) do
|
||||
{:ok, []} ->
|
||||
{:error, "No files matched provided patterns"}
|
||||
{:ok, files} ->
|
||||
wrapper fn ->
|
||||
Code.compiler_options(config.compiler_options)
|
||||
Kernel.ParallelCompiler.files_to_path(files, config.output,
|
||||
each_file: fn file -> if config.verbose_compile do IO.puts "Compiled #{file}" end end)
|
||||
end
|
||||
{:missing, missing} ->
|
||||
{:error, "No files matched pattern(s) #{Enum.join(missing, ",")}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_patterns(pattern) do
|
||||
Enum.filter(Enum.uniq(Path.wildcard(pattern)), &File.regular?(&1))
|
||||
end
|
||||
|
||||
defp filter_multiple_patterns(patterns) do
|
||||
matched_files = Enum.map patterns, fn(pattern) ->
|
||||
case filter_patterns(pattern) do
|
||||
[] -> {:missing, pattern}
|
||||
files -> {:ok, files}
|
||||
end
|
||||
end
|
||||
|
||||
files = Enum.filter_map matched_files,
|
||||
fn(match) -> elem(match, 0) == :ok end,
|
||||
&elem(&1, 1)
|
||||
|
||||
missing_patterns = Enum.filter_map matched_files,
|
||||
fn(match) -> elem(match, 0) == :missing end,
|
||||
&elem(&1, 1)
|
||||
|
||||
if missing_patterns == [] do
|
||||
{:ok, Enum.uniq(Enum.concat(files))}
|
||||
else
|
||||
{:missing, Enum.uniq(missing_patterns)}
|
||||
end
|
||||
end
|
||||
|
||||
defp wrapper(fun) do
|
||||
_ = fun.()
|
||||
:ok
|
||||
end
|
||||
|
||||
defp find_elixir_executable(file) do
|
||||
if exec = System.find_executable(file) do
|
||||
# If we are on Windows, the executable is going to be
|
||||
# a .bat file that must be in the same directory as
|
||||
# the actual Elixir executable.
|
||||
case :os.type() do
|
||||
{:win32, _} ->
|
||||
base = Path.rootname(exec)
|
||||
if File.regular?(base), do: base, else: exec
|
||||
_ ->
|
||||
exec
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
# Implement error_handler pattern for Erlang
|
||||
# which is integrated with Kernel.ParallelCompiler
|
||||
defmodule Kernel.ErrorHandler do
|
||||
@moduledoc false
|
||||
|
||||
def undefined_function(module, fun, args) do
|
||||
ensure_loaded(module)
|
||||
:error_handler.undefined_function(module, fun, args)
|
||||
end
|
||||
|
||||
def undefined_lambda(module, fun, args) do
|
||||
ensure_loaded(module)
|
||||
:error_handler.undefined_lambda(module, fun, args)
|
||||
end
|
||||
|
||||
def release() do
|
||||
# On release, no further allow elixir_ensure_compiled
|
||||
# directives and revert to the original error handler.
|
||||
# Note we should not delete the elixir_compiler_pid though,
|
||||
# as we still want to send notifications to the compiler.
|
||||
:erlang.erase(:elixir_ensure_compiled)
|
||||
:erlang.process_flag(:error_handler, :error_handler)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_loaded(module) do
|
||||
case Code.ensure_loaded(module) do
|
||||
{:module, _} -> :ok
|
||||
{:error, _} ->
|
||||
parent = :erlang.get(:elixir_compiler_pid)
|
||||
ref = :erlang.make_ref
|
||||
send parent, {:waiting, :module, self(), ref, module}
|
||||
:erlang.garbage_collect(self)
|
||||
receive do
|
||||
{^ref, :ready} -> :ok
|
||||
{^ref, :release} -> release()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,184 +0,0 @@
|
||||
# This is a module Elixir responsible for tracking
|
||||
# the usage of aliases, imports and requires in the Elixir scope.
|
||||
#
|
||||
# The implementation simply stores dispatch information in an
|
||||
# ETS table and then consults this table once compilation is done.
|
||||
#
|
||||
# Note that since this is required for bootstrap, we can't use
|
||||
# any of the `GenServer.Behaviour` conveniences.
|
||||
defmodule Kernel.LexicalTracker do
|
||||
@moduledoc false
|
||||
@timeout 30_000
|
||||
@behaviour :gen_server
|
||||
|
||||
@import 2
|
||||
@alias 3
|
||||
|
||||
@doc """
|
||||
Returns all remotes linked to in this lexical scope.
|
||||
"""
|
||||
def remotes(arg) do
|
||||
ets = :gen_server.call(to_pid(arg), :ets, @timeout)
|
||||
:ets.match(ets, {:"$1", :_, :_}) |> List.flatten
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the destination the lexical scope is meant to
|
||||
compile to.
|
||||
"""
|
||||
def dest(arg) do
|
||||
:gen_server.call(to_pid(arg), :dest, @timeout)
|
||||
end
|
||||
|
||||
defp to_pid(pid) when is_pid(pid), do: pid
|
||||
defp to_pid(mod) when is_atom(mod) do
|
||||
table = :elixir_module.data_table(mod)
|
||||
[{_, val}] = :ets.lookup(table, {:elixir, :lexical_tracker})
|
||||
val
|
||||
end
|
||||
|
||||
# Internal API
|
||||
|
||||
# Starts the tracker and returns its pid.
|
||||
@doc false
|
||||
def start_link(dest) do
|
||||
:gen_server.start_link(__MODULE__, dest, [])
|
||||
end
|
||||
|
||||
@doc false
|
||||
def stop(pid) do
|
||||
:gen_server.cast(pid, :stop)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_import(pid, module, line, warn) do
|
||||
:gen_server.cast(pid, {:add_import, module, line, warn})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_alias(pid, module, line, warn) do
|
||||
:gen_server.cast(pid, {:add_alias, module, line, warn})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def remote_dispatch(pid, module) do
|
||||
:gen_server.cast(pid, {:remote_dispatch, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def import_dispatch(pid, module) do
|
||||
:gen_server.cast(pid, {:import_dispatch, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def alias_dispatch(pid, module) do
|
||||
:gen_server.cast(pid, {:alias_dispatch, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def collect_unused_imports(pid) do
|
||||
unused(pid, @import)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def collect_unused_aliases(pid) do
|
||||
unused(pid, @alias)
|
||||
end
|
||||
|
||||
defp unused(pid, pos) do
|
||||
ets = :gen_server.call(pid, :ets, @timeout)
|
||||
:ets.foldl(fn
|
||||
{module, _, _} = tuple, acc when is_integer(:erlang.element(pos, tuple)) ->
|
||||
[{module, :erlang.element(pos, tuple)}|acc]
|
||||
_, acc ->
|
||||
acc
|
||||
end, [], ets) |> Enum.sort
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
def init(dest) do
|
||||
{:ok, {:ets.new(:lexical, [:protected]), dest}}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(:ets, _from, {d, dest}) do
|
||||
{:reply, d, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_call(:dest, _from, {d, dest}) do
|
||||
{:reply, dest, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_call(request, _from, {d, dest}) do
|
||||
{:stop, {:bad_call, request}, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_dispatch, module}, {d, dest}) do
|
||||
add_module(d, module)
|
||||
{:noreply, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_cast({:import_dispatch, module}, {d, dest}) do
|
||||
add_dispatch(d, module, @import)
|
||||
{:noreply, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_cast({:alias_dispatch, module}, {d, dest}) do
|
||||
add_dispatch(d, module, @alias)
|
||||
{:noreply, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_cast({:add_import, module, line, warn}, {d, dest}) do
|
||||
add_directive(d, module, line, warn, @import)
|
||||
{:noreply, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_cast({:add_alias, module, line, warn}, {d, dest}) do
|
||||
add_directive(d, module, line, warn, @alias)
|
||||
{:noreply, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_cast(:stop, {d, dest}) do
|
||||
{:stop, :normal, {d, dest}}
|
||||
end
|
||||
|
||||
def handle_cast(msg, {d, dest}) do
|
||||
{:stop, {:bad_cast, msg}, {d, dest}}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, {d, dest}) do
|
||||
{:noreply, {d, dest}}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def terminate(_reason, _state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc false
|
||||
def code_change(_old, state, _extra) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
# Callbacks helpers
|
||||
|
||||
# In the table we keep imports and aliases.
|
||||
# If the value is false, it was not imported/aliased
|
||||
# If the value is true, it was imported/aliased
|
||||
# If the value is a line, it was imported/aliased and has a pending warning
|
||||
defp add_module(d, module) do
|
||||
:ets.insert_new(d, {module, false, false})
|
||||
end
|
||||
|
||||
defp add_dispatch(d, module, pos) do
|
||||
:ets.update_element(d, module, {pos, true})
|
||||
end
|
||||
|
||||
defp add_directive(d, module, line, warn, pos) do
|
||||
add_module(d, module)
|
||||
marker = if warn, do: line, else: true
|
||||
:ets.update_element(d, module, {pos, marker})
|
||||
end
|
||||
end
|
||||
@@ -1,254 +0,0 @@
|
||||
defmodule Kernel.ParallelCompiler do
|
||||
@moduledoc """
|
||||
A module responsible for compiling files in parallel.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Compiles the given files.
|
||||
|
||||
Those files are compiled in parallel and can automatically
|
||||
detect dependencies between them. Once a dependency is found,
|
||||
the current file stops being compiled until the dependency is
|
||||
resolved.
|
||||
|
||||
If there is an error during compilation or if `warnings_as_errors`
|
||||
is set to `true` and there is a warning, this function will fail
|
||||
with an exception.
|
||||
|
||||
This function accepts the following options:
|
||||
|
||||
* `:each_file` - for each file compiled, invokes the callback passing the
|
||||
file
|
||||
|
||||
* `:each_module` - for each module compiled, invokes the callback passing
|
||||
the file, module and the module bytecode
|
||||
|
||||
* `:dest` - the destination directory for the beam files. When using `files/2`,
|
||||
this information is only used to properly annotate the beam files before
|
||||
they are loaded into memory. If you want a file to actually be writen to
|
||||
`dest`, use `files_to_path/3` instead.
|
||||
|
||||
Returns the modules generated by each compiled file.
|
||||
"""
|
||||
def files(files, options \\ [])
|
||||
|
||||
def files(files, options) when is_list(options) do
|
||||
spawn_compilers(files, nil, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given files to the given path.
|
||||
Read `files/2` for more information.
|
||||
"""
|
||||
def files_to_path(files, path, options \\ [])
|
||||
|
||||
def files_to_path(files, path, options) when is_binary(path) and is_list(options) do
|
||||
spawn_compilers(files, path, options)
|
||||
end
|
||||
|
||||
defp spawn_compilers(files, path, options) do
|
||||
true = Code.ensure_loaded?(Kernel.ErrorHandler)
|
||||
compiler_pid = self()
|
||||
:elixir_code_server.cast({:reset_warnings, compiler_pid})
|
||||
schedulers = max(:erlang.system_info(:schedulers_online), 2)
|
||||
|
||||
result = spawn_compilers(files, files, path, options, [], [], schedulers, [])
|
||||
|
||||
# In case --warning-as-errors is enabled and there was a warning,
|
||||
# compilation status will be set to error and we fail with CompileError
|
||||
case :elixir_code_server.call({:compilation_status, compiler_pid}) do
|
||||
:ok -> result
|
||||
:error -> exit({:shutdown, 1})
|
||||
end
|
||||
end
|
||||
|
||||
# We already have 4 currently running, don't spawn new ones
|
||||
defp spawn_compilers(entries, original, output, options, waiting, queued, schedulers, result) when
|
||||
length(queued) - length(waiting) >= schedulers do
|
||||
wait_for_messages(entries, original, output, options, waiting, queued, schedulers, result)
|
||||
end
|
||||
|
||||
# Release waiting processes
|
||||
defp spawn_compilers([h|t], original, output, options, waiting, queued, schedulers, result) when is_pid(h) do
|
||||
{_kind, ^h, ref, _module} = List.keyfind(waiting, h, 1)
|
||||
send h, {ref, :ready}
|
||||
waiting = List.keydelete(waiting, h, 1)
|
||||
spawn_compilers(t, original, output, options, waiting, queued, schedulers, result)
|
||||
end
|
||||
|
||||
# Spawn a compiler for each file in the list until we reach the limit
|
||||
defp spawn_compilers([h|t], original, output, options, waiting, queued, schedulers, result) do
|
||||
parent = self()
|
||||
|
||||
{pid, ref} =
|
||||
:erlang.spawn_monitor fn ->
|
||||
# Notify Code.ensure_compiled/2 that we should
|
||||
# attempt to compile the module by doing a dispatch.
|
||||
:erlang.put(:elixir_ensure_compiled, true)
|
||||
|
||||
# Set the elixir_compiler_pid used by our custom Kernel.ErrorHandler.
|
||||
:erlang.put(:elixir_compiler_pid, parent)
|
||||
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
|
||||
|
||||
exit(try do
|
||||
_ = if output do
|
||||
:elixir_compiler.file_to_path(h, output)
|
||||
else
|
||||
:elixir_compiler.file(h, Keyword.get(options, :dest))
|
||||
end
|
||||
{:compiled, h}
|
||||
catch
|
||||
kind, reason ->
|
||||
{:failure, kind, reason, System.stacktrace}
|
||||
end)
|
||||
end
|
||||
|
||||
spawn_compilers(t, original, output, options, waiting,
|
||||
[{pid, ref, h}|queued], schedulers, result)
|
||||
end
|
||||
|
||||
# No more files, nothing waiting, queue is empty, we are done
|
||||
defp spawn_compilers([], _original, _output, _options, [], [], _schedulers, result) do
|
||||
for {:module, mod} <- result, do: mod
|
||||
end
|
||||
|
||||
# Queued x, waiting for x: POSSIBLE ERROR! Release processes so we get the failures
|
||||
defp spawn_compilers([], original, output, options, waiting, queued, schedulers, result) when length(waiting) == length(queued) do
|
||||
Enum.each queued, fn {child, _, _} ->
|
||||
{_kind, ^child, ref, _module} = List.keyfind(waiting, child, 1)
|
||||
send child, {ref, :release}
|
||||
end
|
||||
wait_for_messages([], original, output, options, waiting, queued, schedulers, result)
|
||||
end
|
||||
|
||||
# No more files, but queue and waiting are not full or do not match
|
||||
defp spawn_compilers([], original, output, options, waiting, queued, schedulers, result) do
|
||||
wait_for_messages([], original, output, options, waiting, queued, schedulers, result)
|
||||
end
|
||||
|
||||
# Wait for messages from child processes
|
||||
defp wait_for_messages(entries, original, output, options, waiting, queued, schedulers, result) do
|
||||
receive do
|
||||
{:struct_available, module} ->
|
||||
available = for {:struct, pid, _, waiting_module} <- waiting,
|
||||
module == waiting_module,
|
||||
not pid in entries,
|
||||
do: pid
|
||||
|
||||
spawn_compilers(available ++ entries, original, output, options,
|
||||
waiting, queued, schedulers, [{:struct, module}|result])
|
||||
|
||||
{:module_available, child, ref, file, module, binary} ->
|
||||
if callback = Keyword.get(options, :each_module) do
|
||||
callback.(file, module, binary)
|
||||
end
|
||||
|
||||
# Release the module loader which is waiting for an ack
|
||||
send child, {ref, :ack}
|
||||
|
||||
available = for {_kind, pid, _, waiting_module} <- waiting,
|
||||
module == waiting_module,
|
||||
not pid in entries,
|
||||
do: pid
|
||||
|
||||
spawn_compilers(available ++ entries, original, output, options,
|
||||
waiting, queued, schedulers, [{:module, module}|result])
|
||||
|
||||
{:waiting, kind, child, ref, on} ->
|
||||
defined = fn {k, m} -> on == m and k in [kind, :module] end
|
||||
|
||||
# Oops, we already got it, do not put it on waiting.
|
||||
if :lists.any(defined, result) do
|
||||
send child, {ref, :ready}
|
||||
else
|
||||
waiting = [{kind, child, ref, on}|waiting]
|
||||
end
|
||||
|
||||
spawn_compilers(entries, original, output, options, waiting, queued, schedulers, result)
|
||||
|
||||
{:DOWN, _down_ref, :process, down_pid, {:compiled, file}} ->
|
||||
if callback = Keyword.get(options, :each_file) do
|
||||
callback.(file)
|
||||
end
|
||||
|
||||
# Sometimes we may have spurious entries in the waiting
|
||||
# list because someone invoked try/rescue UndefinedFunctionError
|
||||
new_entries = List.delete(entries, down_pid)
|
||||
new_queued = List.keydelete(queued, down_pid, 0)
|
||||
new_waiting = List.keydelete(waiting, down_pid, 1)
|
||||
spawn_compilers(new_entries, original, output, options, new_waiting, new_queued, schedulers, result)
|
||||
|
||||
{:DOWN, down_ref, :process, _down_pid, reason} ->
|
||||
handle_failure(down_ref, reason, entries, waiting, queued)
|
||||
wait_for_messages(entries, original, output, options, waiting, queued, schedulers, result)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_failure(ref, reason, entries, waiting, queued) do
|
||||
if file = find_failure(ref, queued) do
|
||||
print_failure(file, reason)
|
||||
if all_missing?(entries, waiting, queued) do
|
||||
collect_failures(queued, length(queued) - 1)
|
||||
end
|
||||
exit({:shutdown, 1})
|
||||
end
|
||||
end
|
||||
|
||||
defp find_failure(ref, queued) do
|
||||
case List.keyfind(queued, ref, 1) do
|
||||
{_child, ^ref, file} -> file
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp print_failure(_file, {:compiled, _}) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp print_failure(file, {:failure, kind, reason, stacktrace}) do
|
||||
IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
|
||||
IO.puts Exception.format(kind, reason, prune_stacktrace(stacktrace))
|
||||
end
|
||||
|
||||
defp print_failure(file, reason) do
|
||||
IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
|
||||
IO.puts Exception.format(:exit, reason, [])
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_translator,
|
||||
:elixir_expand, :elixir_lexical]
|
||||
|
||||
defp prune_stacktrace([{mod, _, _, _}|t]) when mod in @elixir_internals do
|
||||
prune_stacktrace(t)
|
||||
end
|
||||
|
||||
defp prune_stacktrace([h|t]) do
|
||||
[h|prune_stacktrace(t)]
|
||||
end
|
||||
|
||||
defp prune_stacktrace([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp all_missing?(entries, waiting, queued) do
|
||||
entries == [] and waiting != [] and
|
||||
length(waiting) == length(queued)
|
||||
end
|
||||
|
||||
defp collect_failures(_queued, 0), do: :ok
|
||||
|
||||
defp collect_failures(queued, remaining) do
|
||||
receive do
|
||||
{:DOWN, down_ref, :process, _down_pid, reason} ->
|
||||
if file = find_failure(down_ref, queued) do
|
||||
print_failure(file, reason)
|
||||
collect_failures(queued, remaining - 1)
|
||||
else
|
||||
collect_failures(queued, remaining)
|
||||
end
|
||||
after
|
||||
# Give up if no failure appears in 5 seconds
|
||||
5000 -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,80 +0,0 @@
|
||||
defmodule Kernel.ParallelRequire do
|
||||
@moduledoc """
|
||||
A module responsible for requiring files in parallel.
|
||||
"""
|
||||
|
||||
defmacrop default_callback, do: quote(do: fn x -> x end)
|
||||
|
||||
@doc """
|
||||
Requires the given files.
|
||||
|
||||
A callback that is invoked every time a file is required
|
||||
can be optionally given as argument.
|
||||
|
||||
Returns the modules generated by each required file.
|
||||
"""
|
||||
def files(files, callback \\ default_callback) do
|
||||
schedulers = max(:erlang.system_info(:schedulers_online), 2)
|
||||
spawn_requires(files, [], callback, schedulers, [])
|
||||
end
|
||||
|
||||
defp spawn_requires([], [], _callback, _schedulers, result), do: result
|
||||
|
||||
defp spawn_requires([], waiting, callback, schedulers, result) do
|
||||
wait_for_messages([], waiting, callback, schedulers, result)
|
||||
end
|
||||
|
||||
defp spawn_requires(files, waiting, callback, schedulers, result) when length(waiting) >= schedulers do
|
||||
wait_for_messages(files, waiting, callback, schedulers, result)
|
||||
end
|
||||
|
||||
defp spawn_requires([h|t], waiting, callback, schedulers, result) do
|
||||
parent = self
|
||||
|
||||
compiler_pid = :erlang.get(:elixir_compiler_pid)
|
||||
ensure_compiled = :erlang.get(:elixir_ensure_compiled)
|
||||
{:error_handler, handler} = :erlang.process_info(parent, :error_handler)
|
||||
|
||||
{pid, ref} = :erlang.spawn_monitor fn ->
|
||||
if compiler_pid != :undefined do
|
||||
:erlang.put(:elixir_compiler_pid, compiler_pid)
|
||||
end
|
||||
|
||||
if ensure_compiled != :undefined do
|
||||
:erlang.put(:elixir_ensure_compiled, ensure_compiled)
|
||||
end
|
||||
|
||||
:erlang.process_flag(:error_handler, handler)
|
||||
|
||||
exit(try do
|
||||
new = Code.require_file(h) || []
|
||||
{:required, Enum.map(new, &elem(&1, 0)), h}
|
||||
catch
|
||||
kind, reason ->
|
||||
{:failure, kind, reason, System.stacktrace}
|
||||
end)
|
||||
end
|
||||
|
||||
spawn_requires(t, [{pid, ref}|waiting], callback, schedulers, result)
|
||||
end
|
||||
|
||||
defp wait_for_messages(files, waiting, callback, schedulers, result) do
|
||||
receive do
|
||||
{:DOWN, ref, :process, pid, status} ->
|
||||
tuple = {pid, ref}
|
||||
if tuple in waiting do
|
||||
case status do
|
||||
{:required, mods, file} ->
|
||||
callback.(file)
|
||||
result = mods ++ result
|
||||
waiting = List.delete(waiting, tuple)
|
||||
{:failure, kind, reason, stacktrace} ->
|
||||
:erlang.raise(kind, reason, stacktrace)
|
||||
other ->
|
||||
:erlang.raise(:exit, other, [])
|
||||
end
|
||||
end
|
||||
spawn_requires(files, waiting, callback, schedulers, result)
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,612 +0,0 @@
|
||||
defmodule Keyword do
|
||||
@moduledoc """
|
||||
A keyword is a list of tuples where the first element
|
||||
of the tuple is an atom and the second element can be
|
||||
any value.
|
||||
|
||||
A keyword may have duplicated keys so it is not strictly
|
||||
a dictionary. However most of the functions in this module
|
||||
behave exactly as a dictionary and mimic the API defined
|
||||
by the `Dict` behaviour.
|
||||
|
||||
For example, `Keyword.get/3` will get the first entry matching
|
||||
the given key, regardless if duplicated entries exist.
|
||||
Similarly, `Keyword.put/3` and `Keyword.delete/3` ensure all
|
||||
duplicated entries for a given key are removed when invoked.
|
||||
|
||||
A handful of functions exist to handle duplicated keys, in
|
||||
particular, `Enum.into/2` allows creating new keywords without
|
||||
removing duplicated keys, `get_values/2` returns all values for
|
||||
a given key and `delete_first/2` deletes just one of the existing
|
||||
entries.
|
||||
|
||||
Since a keyword list is simply a list, all the operations defined
|
||||
in `Enum` and `List` can be applied.
|
||||
"""
|
||||
|
||||
@compile :inline_list_funcs
|
||||
@behaviour Dict
|
||||
|
||||
@type key :: atom
|
||||
@type value :: any
|
||||
|
||||
@type t :: [{key, value}]
|
||||
@type t(value) :: [{key, value}]
|
||||
|
||||
@doc """
|
||||
Checks if the given argument is a keyword list or not.
|
||||
"""
|
||||
@spec keyword?(term) :: boolean
|
||||
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
keyword?(rest)
|
||||
end
|
||||
|
||||
def keyword?([]), do: true
|
||||
def keyword?(_other), do: false
|
||||
|
||||
@doc """
|
||||
Returns an empty keyword list, i.e. an empty list.
|
||||
"""
|
||||
@spec new :: t
|
||||
def new do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a keyword from an enumerable.
|
||||
|
||||
Duplicated entries are removed, the latest one prevails.
|
||||
Unlike `Enum.into(enumerable, [])`,
|
||||
`Keyword.new(enumerable)` guarantees the keys are unique.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.new([{:b, 1}, {:a, 2}])
|
||||
[a: 2, b: 1]
|
||||
|
||||
"""
|
||||
@spec new(Enum.t) :: t
|
||||
def new(pairs) do
|
||||
Enum.reduce pairs, [], fn {k, v}, keywords ->
|
||||
put(keywords, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a keyword from an enumerable via the transformation function.
|
||||
|
||||
Duplicated entries are removed, the latest one prevails.
|
||||
Unlike `Enum.into(enumerable, [], fun)`,
|
||||
`Keyword.new(enumerable, fun)` guarantees the keys are unique.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.new([:a, :b], fn (x) -> {x, x} end) |> Enum.sort
|
||||
[a: :a, b: :b]
|
||||
|
||||
"""
|
||||
@spec new(Enum.t, ({key, value} -> {key, value})) :: t
|
||||
def new(pairs, transform) do
|
||||
Enum.reduce pairs, [], fn i, keywords ->
|
||||
{k, v} = transform.(i)
|
||||
put(keywords, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value for a specific `key`.
|
||||
|
||||
If `key` does not exist, return the default value (`nil` if no default value).
|
||||
|
||||
If duplicated entries exist, the first one is returned.
|
||||
Use `get_values/2` to retrieve all entries.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.get([a: 1], :a)
|
||||
1
|
||||
|
||||
iex> Keyword.get([a: 1], :b)
|
||||
nil
|
||||
|
||||
iex> Keyword.get([a: 1], :b, 3)
|
||||
3
|
||||
|
||||
"""
|
||||
@spec get(t, key) :: value
|
||||
@spec get(t, key, value) :: value
|
||||
def get(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> value
|
||||
false -> default
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the value for a specific `key` and returns it in a tuple.
|
||||
|
||||
If the `key` does not exist, returns `:error`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.fetch([a: 1], :a)
|
||||
{:ok, 1}
|
||||
|
||||
iex> Keyword.fetch([a: 1], :b)
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec fetch(t, key) :: {:ok, value} | :error
|
||||
def fetch(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {:ok, value}
|
||||
false -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the value for specific `key`.
|
||||
|
||||
If `key` does not exist, a `KeyError` is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.fetch!([a: 1], :a)
|
||||
1
|
||||
|
||||
iex> Keyword.fetch!([a: 1], :b)
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
|
||||
"""
|
||||
@spec fetch!(t, key) :: value | no_return
|
||||
def fetch!(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> value
|
||||
false -> raise(KeyError, key: key, term: keywords)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets all values for a specific `key`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.get_values([a: 1, a: 2], :a)
|
||||
[1,2]
|
||||
|
||||
"""
|
||||
@spec get_values(t, key) :: [value]
|
||||
def get_values(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
fun = fn
|
||||
{k, v} when k === key -> {true, v}
|
||||
{_, _} -> false
|
||||
end
|
||||
|
||||
:lists.filtermap(fun, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all keys from the keyword list.
|
||||
|
||||
Duplicated keys appear duplicated in the final list of keys.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.keys([a: 1, b: 2])
|
||||
[:a,:b]
|
||||
|
||||
iex> Keyword.keys([a: 1, b: 2, a: 3])
|
||||
[:a,:b,:a]
|
||||
|
||||
"""
|
||||
@spec keys(t) :: [key]
|
||||
def keys(keywords) when is_list(keywords) do
|
||||
:lists.map(fn {k, _} -> k end, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all values from the keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.values([a: 1, b: 2])
|
||||
[1,2]
|
||||
|
||||
"""
|
||||
@spec values(t) :: [value]
|
||||
def values(keywords) when is_list(keywords) do
|
||||
:lists.map(fn {_, v} -> v end, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the entries in the keyword list for a `key` with `value`.
|
||||
|
||||
If no `key` with `value` exists, returns the keyword list unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2], :a, 1)
|
||||
[b: 2]
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2, a: 3], :a, 3)
|
||||
[a: 1, b: 2]
|
||||
|
||||
iex> Keyword.delete([b: 2], :a, 5)
|
||||
[b: 2]
|
||||
|
||||
"""
|
||||
@spec delete(t, key, value) :: t
|
||||
def delete(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
:lists.filter(fn {k, v} -> k != key or v != value end, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the entries in the keyword list for a specific `key`.
|
||||
|
||||
If the `key` does not exist, returns the keyword list unchanged.
|
||||
Use `delete_first/2` to delete just the first entry in case of
|
||||
duplicated keys.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2], :a)
|
||||
[b: 2]
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2, a: 3], :a)
|
||||
[b: 2]
|
||||
|
||||
iex> Keyword.delete([b: 2], :a)
|
||||
[b: 2]
|
||||
|
||||
"""
|
||||
@spec delete(t, key) :: t
|
||||
def delete(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
:lists.filter(fn {k, _} -> k != key end, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the first entry in the keyword list for a specific `key`.
|
||||
|
||||
If the `key` does not exist, returns the keyword list unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.delete_first([a: 1, b: 2, a: 3], :a)
|
||||
[b: 2, a: 3]
|
||||
|
||||
iex> Keyword.delete_first([b: 2], :a)
|
||||
[b: 2]
|
||||
|
||||
"""
|
||||
@spec delete_first(t, key) :: t
|
||||
def delete_first(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
:lists.keydelete(key, 1, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key`.
|
||||
|
||||
If a previous value is already stored, all entries are
|
||||
removed and the value is overridden.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.put([a: 1, b: 2], :a, 3)
|
||||
[a: 3, b: 2]
|
||||
|
||||
iex> Keyword.put([a: 1, b: 2, a: 4], :a, 3)
|
||||
[a: 3, b: 2]
|
||||
|
||||
"""
|
||||
@spec put(t, key, value) :: t
|
||||
def put(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
[{key, value}|delete(keywords, key)]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key` unless the entry `key`
|
||||
already exists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.put_new([a: 1], :b, 2)
|
||||
[b: 2, a: 1]
|
||||
|
||||
iex> Keyword.put_new([a: 1, b: 2], :a, 3)
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec put_new(t, key, value) :: t
|
||||
def put_new(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, _} -> keywords
|
||||
false -> [{key, value}|keywords]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if two keywords are equal.
|
||||
|
||||
Two keywords are considered to be equal if they contain
|
||||
the same keys and those keys contain the same values.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.equal?([a: 1, b: 2], [b: 2, a: 1])
|
||||
true
|
||||
|
||||
"""
|
||||
@spec equal?(t, t) :: boolean
|
||||
def equal?(left, right) when is_list(left) and is_list(right) do
|
||||
:lists.sort(left) == :lists.sort(right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges two keyword lists into one.
|
||||
|
||||
If they have duplicated keys, the one given in the second argument wins.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4]) |> Enum.sort
|
||||
[a: 3, b: 2, d: 4]
|
||||
|
||||
"""
|
||||
@spec merge(t, t) :: t
|
||||
def merge(d1, d2) when is_list(d1) and is_list(d2) do
|
||||
fun = fn {k, _v} -> not has_key?(d2, k) end
|
||||
d2 ++ :lists.filter(fun, d1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges two keyword lists into one.
|
||||
|
||||
If they have duplicated keys, the given function is invoked to solve conflicts.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4], fn (_k, v1, v2) ->
|
||||
...> v1 + v2
|
||||
...> end)
|
||||
[a: 4, b: 2, d: 4]
|
||||
|
||||
"""
|
||||
@spec merge(t, t, (key, value, value -> value)) :: t
|
||||
def merge(d1, d2, fun) when is_list(d1) and is_list(d2) do
|
||||
do_merge(d2, d1, fun)
|
||||
end
|
||||
|
||||
defp do_merge([{k, v2}|t], acc, fun) do
|
||||
do_merge t, update(acc, k, v2, fn(v1) -> fun.(k, v1, v2) end), fun
|
||||
end
|
||||
|
||||
defp do_merge([], acc, _fun) do
|
||||
acc
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether a given `key` exists in the given `keywords`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.has_key?([a: 1], :a)
|
||||
true
|
||||
|
||||
iex> Keyword.has_key?([a: 1], :b)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec has_key?(t, key) :: boolean
|
||||
def has_key?(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
:lists.keymember(key, 1, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the `key` with the given function.
|
||||
|
||||
If the `key` does not exist, raises `KeyError`.
|
||||
|
||||
If there are duplicated keys, they are all removed and only the first one
|
||||
is updated.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.update!([a: 1], :a, &(&1 * 2))
|
||||
[a: 2]
|
||||
|
||||
iex> Keyword.update!([a: 1], :b, &(&1 * 2))
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
|
||||
"""
|
||||
@spec update!(t, key, (value -> value)) :: t | no_return
|
||||
def update!(keywords, key, fun) do
|
||||
update!(keywords, key, fun, keywords)
|
||||
end
|
||||
|
||||
defp update!([{key, value}|keywords], key, fun, _dict) do
|
||||
[{key, fun.(value)}|delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp update!([{_, _} = e|keywords], key, fun, dict) do
|
||||
[e|update!(keywords, key, fun, dict)]
|
||||
end
|
||||
|
||||
defp update!([], key, _fun, dict) when is_atom(key) do
|
||||
raise(KeyError, key: key, term: dict)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the `key` with the given function.
|
||||
|
||||
If the `key` does not exist, inserts the given `initial` value.
|
||||
|
||||
If there are duplicated keys, they are all removed and only the first one
|
||||
is updated.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.update([a: 1], :a, 13, &(&1 * 2))
|
||||
[a: 2]
|
||||
|
||||
iex> Keyword.update([a: 1], :b, 11, &(&1 * 2))
|
||||
[a: 1, b: 11]
|
||||
|
||||
"""
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update([{key, value}|keywords], key, _initial, fun) do
|
||||
[{key, fun.(value)}|delete(keywords, key)]
|
||||
end
|
||||
|
||||
def update([{_, _} = e|keywords], key, initial, fun) do
|
||||
[e|update(keywords, key, initial, fun)]
|
||||
end
|
||||
|
||||
def update([], key, initial, _fun) when is_atom(key) do
|
||||
[{key, initial}]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Takes all entries corresponding to the given keys and extracts them into a
|
||||
separate keyword list.
|
||||
|
||||
Returns a tuple with the new list and the old list with removed keys.
|
||||
|
||||
Keys for which there are no entires in the keyword list are ignored.
|
||||
|
||||
Entries with duplicated keys end up in the same keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4]
|
||||
iex> Keyword.split(d, [:a, :c, :e])
|
||||
{[a: 1, c: 3], [b: 2, d: 4]}
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4, a: 5]
|
||||
iex> Keyword.split(d, [:a, :c, :e])
|
||||
{[a: 1, c: 3, a: 5], [b: 2, d: 4]}
|
||||
|
||||
"""
|
||||
def split(keywords, keys) when is_list(keywords) do
|
||||
fun = fn {k, v}, {take, drop} ->
|
||||
case k in keys do
|
||||
true -> {[{k, v}|take], drop}
|
||||
false -> {take, [{k, v}|drop]}
|
||||
end
|
||||
end
|
||||
|
||||
acc = {[], []}
|
||||
{take, drop} = :lists.foldl(fun, acc, keywords)
|
||||
{:lists.reverse(take), :lists.reverse(drop)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Takes all entries corresponding to the given keys and returns them in a new
|
||||
keyword list.
|
||||
|
||||
Duplicated keys are preserved in the new keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4]
|
||||
iex> Keyword.take(d, [:a, :c, :e])
|
||||
[a: 1, c: 3]
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4, a: 5]
|
||||
iex> Keyword.take(d, [:a, :c, :e])
|
||||
[a: 1, c: 3, a: 5]
|
||||
|
||||
"""
|
||||
def take(keywords, keys) when is_list(keywords) do
|
||||
:lists.filter(fn {k, _} -> k in keys end, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Drops the given keys from the keyword list.
|
||||
|
||||
Duplicated keys are preserved in the new keyword list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> d = [a: 1, b: 2, c: 3, d: 4]
|
||||
iex> Keyword.drop(d, [:b, :d])
|
||||
[a: 1, c: 3]
|
||||
|
||||
iex> d = [a: 1, b: 2, b: 3, c: 3, d: 4, a: 5]
|
||||
iex> Keyword.drop(d, [:b, :d])
|
||||
[a: 1, c: 3, a: 5]
|
||||
|
||||
"""
|
||||
def drop(keywords, keys) when is_list(keywords) do
|
||||
:lists.filter(fn {k, _} -> not k in keys end, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first value associated with `key` in the keyword
|
||||
list as well as the keyword list without `key`.
|
||||
|
||||
All duplicated keys are removed. See `pop_first/3` for
|
||||
removing only the first entry.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.pop [a: 1], :a
|
||||
{1,[]}
|
||||
|
||||
iex> Keyword.pop [a: 1], :b
|
||||
{nil,[a: 1]}
|
||||
|
||||
iex> Keyword.pop [a: 1], :b, 3
|
||||
{3,[a: 1]}
|
||||
|
||||
iex> Keyword.pop [a: 1], :b, 3
|
||||
{3,[a: 1]}
|
||||
|
||||
iex> Keyword.pop [a: 1, a: 2], :a
|
||||
{1,[]}
|
||||
|
||||
"""
|
||||
def pop(keywords, key, default \\ nil) when is_list(keywords) do
|
||||
{get(keywords, key, default), delete(keywords, key)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first value associated with `key` in the keyword
|
||||
list as well as the keyword list without that particular occurrence
|
||||
of `key`.
|
||||
|
||||
Duplicated keys are not removed.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.pop_first [a: 1], :a
|
||||
{1,[]}
|
||||
|
||||
iex> Keyword.pop_first [a: 1], :b
|
||||
{nil,[a: 1]}
|
||||
|
||||
iex> Keyword.pop_first [a: 1], :b, 3
|
||||
{3,[a: 1]}
|
||||
|
||||
iex> Keyword.pop_first [a: 1], :b, 3
|
||||
{3,[a: 1]}
|
||||
|
||||
iex> Keyword.pop_first [a: 1, a: 2], :a
|
||||
{1,[a: 2]}
|
||||
|
||||
"""
|
||||
def pop_first(keywords, key, default \\ nil) when is_list(keywords) do
|
||||
{get(keywords, key, default), delete_first(keywords, key)}
|
||||
end
|
||||
|
||||
# Dict callbacks
|
||||
|
||||
@doc false
|
||||
def size(keyword) do
|
||||
length(keyword)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def to_list(keyword) do
|
||||
keyword
|
||||
end
|
||||
end
|
||||
@@ -1,663 +0,0 @@
|
||||
defmodule List do
|
||||
@moduledoc """
|
||||
Implements functions that only make sense for lists
|
||||
and cannot be part of the Enum protocol. In general,
|
||||
favor using the Enum API instead of List.
|
||||
|
||||
Some functions in this module expect an index. Index
|
||||
access for list is linear. Negative indexes are also
|
||||
supported but they imply the list will be iterated twice,
|
||||
one to calculate the proper index and another to the
|
||||
operation.
|
||||
|
||||
A decision was taken to delegate most functions to
|
||||
Erlang's standard library but follow Elixir's convention
|
||||
of receiving the target (in this case, a list) as the
|
||||
first argument.
|
||||
"""
|
||||
|
||||
@compile :inline_list_funcs
|
||||
|
||||
@doc """
|
||||
Deletes the given item from the list. Returns a list without
|
||||
the item. If the item occurs more than once in the list, just
|
||||
the first occurrence is removed.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.delete([1, 2, 3], 1)
|
||||
[2,3]
|
||||
|
||||
iex> List.delete([1, 2, 2, 3], 2)
|
||||
[1, 2, 3]
|
||||
|
||||
"""
|
||||
@spec delete(list, any) :: list
|
||||
def delete(list, item) do
|
||||
:lists.delete(item, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Duplicates the given element `n` times in a list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.duplicate("hello", 3)
|
||||
["hello","hello","hello"]
|
||||
|
||||
iex> List.duplicate([1, 2], 2)
|
||||
[[1,2],[1,2]]
|
||||
|
||||
"""
|
||||
@spec duplicate(elem, non_neg_integer) :: [elem] when elem: var
|
||||
def duplicate(elem, n) do
|
||||
:lists.duplicate(n, elem)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Flattens the given `list` of nested lists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.flatten([1, [[2], 3]])
|
||||
[1,2,3]
|
||||
|
||||
"""
|
||||
@spec flatten(deep_list) :: list when deep_list: [any | deep_list]
|
||||
def flatten(list) do
|
||||
:lists.flatten(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Flattens the given `list` of nested lists.
|
||||
The list `tail` will be added at the end of
|
||||
the flattened list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.flatten([1, [[2], 3]], [4, 5])
|
||||
[1,2,3,4,5]
|
||||
|
||||
"""
|
||||
@spec flatten(deep_list, [elem]) :: [elem] when elem: var, deep_list: [elem | deep_list]
|
||||
def flatten(list, tail) do
|
||||
:lists.flatten(list, tail)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Folds (reduces) the given list to the left with
|
||||
a function. Requires an accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.foldl([5, 5], 10, fn (x, acc) -> x + acc end)
|
||||
20
|
||||
|
||||
iex> List.foldl([1, 2, 3, 4], 0, fn (x, acc) -> x - acc end)
|
||||
2
|
||||
|
||||
"""
|
||||
@spec foldl([elem], acc, (elem, acc -> acc)) :: acc when elem: var, acc: var
|
||||
def foldl(list, acc, function) when is_list(list) and is_function(function) do
|
||||
:lists.foldl(function, acc, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Folds (reduces) the given list to the right with
|
||||
a function. Requires an accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.foldr([1, 2, 3, 4], 0, fn (x, acc) -> x - acc end)
|
||||
-2
|
||||
|
||||
"""
|
||||
@spec foldr([elem], acc, (elem, acc -> acc)) :: acc when elem: var, acc: var
|
||||
def foldr(list, acc, function) when is_list(list) and is_function(function) do
|
||||
:lists.foldr(function, acc, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first element in `list` or `nil` if `list` is empty.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.first([])
|
||||
nil
|
||||
|
||||
iex> List.first([1])
|
||||
1
|
||||
|
||||
iex> List.first([1, 2, 3])
|
||||
1
|
||||
|
||||
"""
|
||||
@spec first([elem]) :: nil | elem when elem: var
|
||||
def first([]), do: nil
|
||||
def first([h|_]), do: h
|
||||
|
||||
@doc """
|
||||
Returns the last element in `list` or `nil` if `list` is empty.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.last([])
|
||||
nil
|
||||
|
||||
iex> List.last([1])
|
||||
1
|
||||
|
||||
iex> List.last([1, 2, 3])
|
||||
3
|
||||
|
||||
"""
|
||||
@spec last([elem]) :: nil | elem when elem: var
|
||||
def last([]), do: nil
|
||||
def last([h]), do: h
|
||||
def last([_|t]), do: last(t)
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns the first tuple
|
||||
where the item at `position` in the tuple matches the
|
||||
given `item`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keyfind([a: 1, b: 2], :a, 0)
|
||||
{:a, 1}
|
||||
|
||||
iex> List.keyfind([a: 1, b: 2], 2, 1)
|
||||
{:b, 2}
|
||||
|
||||
iex> List.keyfind([a: 1, b: 2], :c, 0)
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec keyfind([tuple], any, non_neg_integer, any) :: any
|
||||
def keyfind(list, key, position, default \\ nil) do
|
||||
:lists.keyfind(key, position + 1, list) || default
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns `true` if there is
|
||||
a tuple where the item at `position` in the tuple matches
|
||||
the given `item`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keymember?([a: 1, b: 2], :a, 0)
|
||||
true
|
||||
|
||||
iex> List.keymember?([a: 1, b: 2], 2, 1)
|
||||
true
|
||||
|
||||
iex> List.keymember?([a: 1, b: 2], :c, 0)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec keymember?([tuple], any, non_neg_integer) :: any
|
||||
def keymember?(list, key, position) do
|
||||
:lists.keymember(key, position + 1, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and replaces the item
|
||||
identified by `key` at `position` if it exists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keyreplace([a: 1, b: 2], :a, 0, {:a, 3})
|
||||
[a: 3, b: 2]
|
||||
|
||||
"""
|
||||
@spec keyreplace([tuple], any, non_neg_integer, tuple) :: [tuple]
|
||||
def keyreplace(list, key, position, new_tuple) do
|
||||
:lists.keyreplace(key, position + 1, list, new_tuple)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and sorts the items
|
||||
at `position` of the tuples. The sort is stable.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keysort([a: 5, b: 1, c: 3], 1)
|
||||
[b: 1, c: 3, a: 5]
|
||||
|
||||
iex> List.keysort([a: 5, c: 1, b: 3], 0)
|
||||
[a: 5, b: 3, c: 1]
|
||||
|
||||
"""
|
||||
@spec keysort([tuple], non_neg_integer) :: [tuple]
|
||||
def keysort(list, position) do
|
||||
:lists.keysort(position + 1, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and replaces the item
|
||||
identified by `key` at `position`. If the item
|
||||
does not exist, it is added to the end of the list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keystore([a: 1, b: 2], :a, 0, {:a, 3})
|
||||
[a: 3, b: 2]
|
||||
|
||||
iex> List.keystore([a: 1, b: 2], :c, 0, {:c, 3})
|
||||
[a: 1, b: 2, c: 3]
|
||||
|
||||
"""
|
||||
@spec keystore([tuple], any, non_neg_integer, tuple) :: [tuple, ...]
|
||||
def keystore(list, key, position, new_tuple) do
|
||||
:lists.keystore(key, position + 1, list, new_tuple)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and deletes the first tuple
|
||||
where the item at `position` matches the
|
||||
given `item`. Returns the new list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.keydelete([a: 1, b: 2], :a, 0)
|
||||
[b: 2]
|
||||
|
||||
iex> List.keydelete([a: 1, b: 2], 2, 1)
|
||||
[a: 1]
|
||||
|
||||
iex> List.keydelete([a: 1, b: 2], :c, 0)
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec keydelete([tuple], any, non_neg_integer) :: [tuple]
|
||||
def keydelete(list, key, position) do
|
||||
:lists.keydelete(key, position + 1, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Wraps the argument in a list.
|
||||
If the argument is already a list, returns the list.
|
||||
If the argument is `nil`, returns an empty list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.wrap("hello")
|
||||
["hello"]
|
||||
|
||||
iex> List.wrap([1, 2, 3])
|
||||
[1,2,3]
|
||||
|
||||
iex> List.wrap(nil)
|
||||
[]
|
||||
|
||||
"""
|
||||
@spec wrap(list | any) :: list
|
||||
def wrap(list) when is_list(list) do
|
||||
list
|
||||
end
|
||||
|
||||
def wrap(nil) do
|
||||
[]
|
||||
end
|
||||
|
||||
def wrap(other) do
|
||||
[other]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Zips corresponding elements from each list in `list_of_lists`.
|
||||
|
||||
The zipping finishes as soon as any list terminates.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.zip([[1, 2], [3, 4], [5, 6]])
|
||||
[{1, 3, 5}, {2, 4, 6}]
|
||||
|
||||
iex> List.zip([[1, 2], [3], [5, 6]])
|
||||
[{1, 3, 5}]
|
||||
|
||||
"""
|
||||
@spec zip([list]) :: [tuple]
|
||||
def zip([]), do: []
|
||||
def zip(list_of_lists) when is_list(list_of_lists) do
|
||||
do_zip(list_of_lists, [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with `value` inserted at the specified `index`.
|
||||
Note that `index` is capped at the list length. Negative indices
|
||||
indicate an offset from the end of the list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.insert_at([1, 2, 3, 4], 2, 0)
|
||||
[1, 2, 0, 3, 4]
|
||||
|
||||
iex> List.insert_at([1, 2, 3], 10, 0)
|
||||
[1, 2, 3, 0]
|
||||
|
||||
iex> List.insert_at([1, 2, 3], -1, 0)
|
||||
[1, 2, 3, 0]
|
||||
|
||||
iex> List.insert_at([1, 2, 3], -10, 0)
|
||||
[0, 1, 2, 3]
|
||||
|
||||
"""
|
||||
@spec insert_at(list, integer, any) :: list
|
||||
def insert_at(list, index, value) do
|
||||
if index < 0 do
|
||||
do_insert_at(list, length(list) + index + 1, value)
|
||||
else
|
||||
do_insert_at(list, index, value)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with a replaced value at the specified `index`.
|
||||
Negative indices indicate an offset from the end of the list.
|
||||
If `index` is out of bounds, the original `list` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.replace_at([1, 2, 3], 0, 0)
|
||||
[0, 2, 3]
|
||||
|
||||
iex> List.replace_at([1, 2, 3], 10, 0)
|
||||
[1, 2, 3]
|
||||
|
||||
iex> List.replace_at([1, 2, 3], -1, 0)
|
||||
[1, 2, 0]
|
||||
|
||||
iex> List.replace_at([1, 2, 3], -10, 0)
|
||||
[1, 2, 3]
|
||||
|
||||
"""
|
||||
@spec replace_at(list, integer, any) :: list
|
||||
def replace_at(list, index, value) do
|
||||
if index < 0 do
|
||||
do_replace_at(list, length(list) + index, value)
|
||||
else
|
||||
do_replace_at(list, index, value)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with an updated value at the specified `index`.
|
||||
Negative indices indicate an offset from the end of the list.
|
||||
If `index` is out of bounds, the original `list` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.update_at([1, 2, 3], 0, &(&1 + 10))
|
||||
[11, 2, 3]
|
||||
|
||||
iex> List.update_at([1, 2, 3], 10, &(&1 + 10))
|
||||
[1, 2, 3]
|
||||
|
||||
iex> List.update_at([1, 2, 3], -1, &(&1 + 10))
|
||||
[1, 2, 13]
|
||||
|
||||
iex> List.update_at([1, 2, 3], -10, &(&1 + 10))
|
||||
[1, 2, 3]
|
||||
|
||||
"""
|
||||
@spec update_at([elem], integer, (elem -> any)) :: list when elem: var
|
||||
def update_at(list, index, fun) do
|
||||
if index < 0 do
|
||||
do_update_at(list, length(list) + index, fun)
|
||||
else
|
||||
do_update_at(list, index, fun)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Produces a new list by removing the value at the specified `index`.
|
||||
Negative indices indicate an offset from the end of the list.
|
||||
If `index` is out of bounds, the original `list` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.delete_at([1, 2, 3], 0)
|
||||
[2, 3]
|
||||
|
||||
iex List.delete_at([1, 2, 3], 10)
|
||||
[1, 2, 3]
|
||||
|
||||
iex> List.delete_at([1, 2, 3], -1)
|
||||
[1, 2]
|
||||
|
||||
"""
|
||||
@spec delete_at(list, integer) :: list
|
||||
def delete_at(list, index) do
|
||||
if index < 0 do
|
||||
do_delete_at(list, length(list) + index)
|
||||
else
|
||||
do_delete_at(list, index)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a char list to an atom.
|
||||
|
||||
Currently Elixir does not support conversions from char lists
|
||||
which contains Unicode codepoints greater than 0xFF.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_atom('elixir')
|
||||
:elixir
|
||||
|
||||
"""
|
||||
@spec to_atom(char_list) :: atom
|
||||
def to_atom(char_list) do
|
||||
:erlang.list_to_atom(char_list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a char list to an existing atom.
|
||||
|
||||
Currently Elixir does not support conversions from char lists
|
||||
which contains Unicode codepoints greater than 0xFF.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec to_existing_atom(char_list) :: atom
|
||||
def to_existing_atom(char_list) do
|
||||
:erlang.list_to_existing_atom(char_list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the float whose text representation is `char_list`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_float('2.2017764e+0')
|
||||
2.2017764
|
||||
|
||||
"""
|
||||
@spec to_float(char_list) :: float
|
||||
def to_float(char_list) do
|
||||
:erlang.list_to_float(char_list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an integer whose text representation is `char_list`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_integer('123')
|
||||
123
|
||||
|
||||
"""
|
||||
@spec to_integer(char_list) :: integer
|
||||
def to_integer(char_list) do
|
||||
:erlang.list_to_integer(char_list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an integer whose text representation is `char_list` in base `base`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_integer('3FF', 16)
|
||||
1023
|
||||
|
||||
"""
|
||||
@spec to_integer(char_list, 2..36) :: integer
|
||||
def to_integer(char_list, base) do
|
||||
:erlang.list_to_integer(char_list, base)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a list to a tuple.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_tuple([:share, [:elixir, 163]])
|
||||
{:share, [:elixir, 163]}
|
||||
|
||||
"""
|
||||
@spec to_tuple(list) :: tuple
|
||||
def to_tuple(list) do
|
||||
:erlang.list_to_tuple(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a list of integers representing codepoints, lists or
|
||||
strings into a string.
|
||||
|
||||
Notice that this function expect a list of integer representing
|
||||
UTF-8 codepoints. If you have a list of bytes, you must instead use
|
||||
[the `:binary` module](http://erlang.org/doc/man/binary.html).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_string([0x00E6, 0x00DF])
|
||||
"æß"
|
||||
|
||||
iex> List.to_string([0x0061, "bc"])
|
||||
"abc"
|
||||
|
||||
"""
|
||||
@spec to_string(:unicode.charlist) :: String.t
|
||||
def to_string(list) when is_list(list) do
|
||||
case :unicode.characters_to_binary(list) do
|
||||
result when is_binary(result) ->
|
||||
result
|
||||
|
||||
{:error, encoded, rest} ->
|
||||
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid
|
||||
|
||||
{:incomplete, encoded, rest} ->
|
||||
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :incomplete
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
# replace_at
|
||||
|
||||
defp do_replace_at([], _index, _value) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp do_replace_at(list, index, _value) when index < 0 do
|
||||
list
|
||||
end
|
||||
|
||||
defp do_replace_at([_old|rest], 0, value) do
|
||||
[ value | rest ]
|
||||
end
|
||||
|
||||
defp do_replace_at([h|t], index, value) do
|
||||
[ h | do_replace_at(t, index - 1, value) ]
|
||||
end
|
||||
|
||||
# insert_at
|
||||
|
||||
defp do_insert_at([], _index, value) do
|
||||
[ value ]
|
||||
end
|
||||
|
||||
defp do_insert_at(list, index, value) when index <= 0 do
|
||||
[ value | list ]
|
||||
end
|
||||
|
||||
defp do_insert_at([h|t], index, value) do
|
||||
[ h | do_insert_at(t, index - 1, value) ]
|
||||
end
|
||||
|
||||
# update_at
|
||||
|
||||
defp do_update_at([value|list], 0, fun) do
|
||||
[ fun.(value) | list ]
|
||||
end
|
||||
|
||||
defp do_update_at(list, index, _fun) when index < 0 do
|
||||
list
|
||||
end
|
||||
|
||||
defp do_update_at([h|t], index, fun) do
|
||||
[ h | do_update_at(t, index - 1, fun) ]
|
||||
end
|
||||
|
||||
defp do_update_at([], _index, _fun) do
|
||||
[]
|
||||
end
|
||||
|
||||
# delete_at
|
||||
|
||||
defp do_delete_at([], _index) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp do_delete_at([_|t], 0) do
|
||||
t
|
||||
end
|
||||
|
||||
defp do_delete_at(list, index) when index < 0 do
|
||||
list
|
||||
end
|
||||
|
||||
defp do_delete_at([h|t], index) do
|
||||
[h | do_delete_at(t, index-1)]
|
||||
end
|
||||
|
||||
# zip
|
||||
|
||||
defp do_zip(list, acc) do
|
||||
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
|
||||
{mlist, heads} = :lists.mapfoldl converter, [], list
|
||||
|
||||
case heads do
|
||||
nil -> :lists.reverse acc
|
||||
_ -> do_zip mlist, [:erlang.list_to_tuple(:lists.reverse(heads))|acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp do_zip_each(_, nil) do
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
defp do_zip_each([h|t], acc) do
|
||||
{t, [h|acc]}
|
||||
end
|
||||
|
||||
defp do_zip_each([], _) do
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
|
||||
defp to_list(list) when is_list(list), do: list
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
defprotocol List.Chars do
|
||||
@moduledoc ~S"""
|
||||
The List.Chars protocol is responsible for
|
||||
converting a structure to a list (only if applicable).
|
||||
The only function required to be implemented is
|
||||
`to_char_list` which does the conversion.
|
||||
|
||||
The `to_char_list` function automatically imported
|
||||
by Kernel invokes this protocol.
|
||||
"""
|
||||
|
||||
def to_char_list(thing)
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Atom do
|
||||
def to_char_list(atom), do: Atom.to_char_list(atom)
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: BitString do
|
||||
@doc """
|
||||
Returns the given binary converted to a char list.
|
||||
"""
|
||||
def to_char_list(thing) when is_binary(thing) do
|
||||
String.to_char_list(thing)
|
||||
end
|
||||
|
||||
def to_char_list(thing) do
|
||||
raise Protocol.UndefinedError,
|
||||
protocol: @protocol,
|
||||
value: thing,
|
||||
description: "cannot convert a bitstring to a char list"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: List do
|
||||
def to_char_list(list), do: list
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Integer do
|
||||
def to_char_list(thing) do
|
||||
Integer.to_char_list(thing)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Float do
|
||||
@digits 20
|
||||
@limit :math.pow(10, @digits)
|
||||
|
||||
def to_char_list(thing) when thing > @limit do
|
||||
Float.to_char_list(thing, scientific: @digits)
|
||||
end
|
||||
|
||||
def to_char_list(thing) do
|
||||
Float.to_char_list(thing, compact: true, decimals: @digits)
|
||||
end
|
||||
end
|
||||
@@ -1,955 +0,0 @@
|
||||
import Kernel, except: [to_string: 1]
|
||||
|
||||
defmodule Macro do
|
||||
@moduledoc """
|
||||
Conveniences for working with macros.
|
||||
"""
|
||||
|
||||
@typedoc "Abstract Syntax Tree (AST)"
|
||||
@type t :: expr | {t, t} | atom | number | binary | pid | fun | [t]
|
||||
|
||||
@typedoc "Expr node (remaining ones are literals)"
|
||||
@type expr :: {expr | atom, Keyword.t, atom | [t]}
|
||||
|
||||
@binary_ops [:===, :!==,
|
||||
:==, :!=, :<=, :>=,
|
||||
:&&, :||, :<>, :++, :--, :\\, :::, :<-, :.., :|>, :=~,
|
||||
:<, :>, :->,
|
||||
:+, :-, :*, :/, :=, :|, :.,
|
||||
:and, :or, :when, :in,
|
||||
:~>>, :<<~, :~>, :<~, :<~>, :<|>,
|
||||
:<<<, :>>>, :|||, :&&&, :^^^, :~~~]
|
||||
|
||||
@doc false
|
||||
defmacro binary_ops, do: @binary_ops
|
||||
|
||||
@unary_ops [:!, :@, :^, :not, :+, :-, :~~~, :&]
|
||||
|
||||
@doc false
|
||||
defmacro unary_ops, do: @unary_ops
|
||||
|
||||
@spec binary_op_props(atom) :: {:left | :right, precedence :: integer}
|
||||
defp binary_op_props(o) do
|
||||
case o do
|
||||
o when o in [:<-, :\\] -> {:left, 40}
|
||||
:when -> {:right, 50}
|
||||
::: -> {:right, 60}
|
||||
:| -> {:right, 70}
|
||||
:= -> {:right, 90}
|
||||
o when o in [:||, :|||, :or] -> {:left, 130}
|
||||
o when o in [:&&, :&&&, :and] -> {:left, 140}
|
||||
o when o in [:==, :!=, :=~, :===, :!==] -> {:left, 150}
|
||||
o when o in [:<, :<=, :>=, :>] -> {:left, 160}
|
||||
o when o in [:|>, :<<<, :>>>, :<~, :~>,
|
||||
:<<~, :~>>, :<~>, :<|>] -> {:left, 170}
|
||||
:in -> {:left, 180}
|
||||
o when o in [:++, :--, :.., :<>] -> {:right, 200}
|
||||
o when o in [:+, :-] -> {:left, 210}
|
||||
o when o in [:*, :/] -> {:left, 220}
|
||||
:^^^ -> {:left, 250}
|
||||
:. -> {:left, 310}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Breaks a pipeline expression into a list.
|
||||
|
||||
Raises if the pipeline is ill-formed.
|
||||
"""
|
||||
@spec unpipe(Macro.t) :: [Macro.t]
|
||||
def unpipe(expr) do
|
||||
:lists.reverse(unpipe(expr, []))
|
||||
end
|
||||
|
||||
defp unpipe({:|>, _, [left, right]}, acc) do
|
||||
unpipe(right, unpipe(left, acc))
|
||||
end
|
||||
|
||||
defp unpipe(other, acc) do
|
||||
[{other, 0}|acc]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Pipes `expr` into the `call_args` at the given `position`.
|
||||
"""
|
||||
@spec pipe(Macro.t, Macro.t, integer) :: Macro.t | no_return
|
||||
def pipe(expr, call_args, position)
|
||||
|
||||
def pipe(expr, {:&, _, _} = call_args, _integer) do
|
||||
bad_pipe(expr, call_args)
|
||||
end
|
||||
|
||||
def pipe(expr, {call, line, atom}, integer) when is_atom(atom) do
|
||||
{call, line, List.insert_at([], integer, expr)}
|
||||
end
|
||||
|
||||
def pipe(expr, {call, line, args}, integer) when is_list(args) do
|
||||
{call, line, List.insert_at(args, integer, expr)}
|
||||
end
|
||||
|
||||
def pipe(expr, call_args, _integer) do
|
||||
bad_pipe(expr, call_args)
|
||||
end
|
||||
|
||||
defp bad_pipe(expr, call_args) do
|
||||
raise ArgumentError, "cannot pipe #{to_string expr} into #{to_string call_args}, " <>
|
||||
"can only pipe into local calls foo(), remote calls Foo.bar() or anonymous functions calls foo.()"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Applies the given function to the node metadata if it contains one.
|
||||
|
||||
This is often useful when used with `Macro.prewalk/1` to remove
|
||||
information like lines and hygienic counters from the expression
|
||||
for either storage or comparison.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> quoted = quote line: 10, do: sample()
|
||||
{:sample, [line: 10], []}
|
||||
iex> Macro.update_meta(quoted, &Keyword.delete(&1, :line))
|
||||
{:sample, [], []}
|
||||
|
||||
"""
|
||||
@spec update_meta(t, (Keyword.t -> Keyword.t)) :: t
|
||||
def update_meta(quoted, fun)
|
||||
|
||||
def update_meta({left, meta, right}, fun) when is_list(meta) do
|
||||
{left, fun.(meta), right}
|
||||
end
|
||||
|
||||
def update_meta(other, _fun) do
|
||||
other
|
||||
end
|
||||
|
||||
@doc """
|
||||
Genrates a AST node representing the variable given
|
||||
by the atoms `var` and `context`.
|
||||
|
||||
## Examples
|
||||
|
||||
In order to build a variable, a context is expected.
|
||||
Most of the times, in order to preserve hygiene, the
|
||||
context must be `__MODULE__`:
|
||||
|
||||
iex> Macro.var(:foo, __MODULE__)
|
||||
{:foo, [], __MODULE__}
|
||||
|
||||
However, if there is a need to access the user variable,
|
||||
nil can be given:
|
||||
|
||||
iex> Macro.var(:foo, nil)
|
||||
{:foo, [], nil}
|
||||
|
||||
"""
|
||||
@spec var(var, context) :: {var, [], context} when var: atom, context: atom
|
||||
def var(var, context) when is_atom(var) and is_atom(context) do
|
||||
{var, [], context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a depth-first, pre-order traversal of quoted expressions.
|
||||
"""
|
||||
@spec prewalk(t, (t -> t)) :: t
|
||||
def prewalk(ast, fun) when is_function(fun, 1) do
|
||||
elem(prewalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a depth-first, pre-order traversal of quoted expressions
|
||||
using an accumulator.
|
||||
"""
|
||||
@spec prewalk(t, any, (t, any -> {t, any})) :: {t, any}
|
||||
def prewalk(ast, acc, fun) when is_function(fun, 2) do
|
||||
{ast, acc} = fun.(ast, acc)
|
||||
do_prewalk(ast, acc, fun)
|
||||
end
|
||||
|
||||
defp do_prewalk({form, meta, args}, acc, fun) do
|
||||
unless is_atom(form) do
|
||||
{form, acc} = fun.(form, acc)
|
||||
{form, acc} = do_prewalk(form, acc, fun)
|
||||
end
|
||||
|
||||
unless is_atom(args) do
|
||||
{args, acc} = Enum.map_reduce(args, acc, fn x, acc ->
|
||||
{x, acc} = fun.(x, acc)
|
||||
do_prewalk(x, acc, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
{{form, meta, args}, acc}
|
||||
end
|
||||
|
||||
defp do_prewalk({left, right}, acc, fun) do
|
||||
{left, acc} = fun.(left, acc)
|
||||
{left, acc} = do_prewalk(left, acc, fun)
|
||||
{right, acc} = fun.(right, acc)
|
||||
{right, acc} = do_prewalk(right, acc, fun)
|
||||
{{left, right}, acc}
|
||||
end
|
||||
|
||||
defp do_prewalk(list, acc, fun) when is_list(list) do
|
||||
Enum.map_reduce(list, acc, fn x, acc ->
|
||||
{x, acc} = fun.(x, acc)
|
||||
do_prewalk(x, acc, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_prewalk(x, acc, _fun) do
|
||||
{x, acc}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a depth-first, post-order traversal of quoted expressions.
|
||||
"""
|
||||
@spec postwalk(t, (t -> t)) :: t
|
||||
def postwalk(ast, fun) when is_function(fun, 1) do
|
||||
elem(postwalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a depth-first, post-order traversal of quoted expressions
|
||||
using an accumulator.
|
||||
"""
|
||||
@spec postwalk(t, any, (t, any -> {t, any})) :: {t, any}
|
||||
def postwalk(ast, acc, fun) when is_function(fun, 2) do
|
||||
do_postwalk(ast, acc, fun)
|
||||
end
|
||||
|
||||
defp do_postwalk({form, meta, args}, acc, fun) do
|
||||
unless is_atom(form) do
|
||||
{form, acc} = do_postwalk(form, acc, fun)
|
||||
end
|
||||
|
||||
unless is_atom(args) do
|
||||
{args, acc} = Enum.map_reduce(args, acc, &do_postwalk(&1, &2, fun))
|
||||
end
|
||||
|
||||
fun.({form, meta, args}, acc)
|
||||
end
|
||||
|
||||
defp do_postwalk({left, right}, acc, fun) do
|
||||
{left, acc} = do_postwalk(left, acc, fun)
|
||||
{right, acc} = do_postwalk(right, acc, fun)
|
||||
fun.({left, right}, acc)
|
||||
end
|
||||
|
||||
defp do_postwalk(list, acc, fun) when is_list(list) do
|
||||
{list, acc} = Enum.map_reduce(list, acc, &do_postwalk(&1, &2, fun))
|
||||
fun.(list, acc)
|
||||
end
|
||||
|
||||
defp do_postwalk(x, acc, fun) do
|
||||
fun.(x, acc)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decomposes a local or remote call into its remote part (when provided),
|
||||
function name and argument list.
|
||||
|
||||
Returns `:error` when an invalid call syntax is provided.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.decompose_call(quote do: foo)
|
||||
{:foo, []}
|
||||
|
||||
iex> Macro.decompose_call(quote do: foo())
|
||||
{:foo, []}
|
||||
|
||||
iex> Macro.decompose_call(quote do: foo(1, 2, 3))
|
||||
{:foo, [1, 2, 3]}
|
||||
|
||||
iex> Macro.decompose_call(quote do: Elixir.M.foo(1, 2, 3))
|
||||
{{:__aliases__, [], [:Elixir, :M]}, :foo, [1, 2, 3]}
|
||||
|
||||
iex> Macro.decompose_call(quote do: 42)
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec decompose_call(Macro.t) :: {atom, [Macro.t]} | {Macro.t, atom, [Macro.t]} | :error
|
||||
def decompose_call({{:., _, [remote, function]}, _, args}) when is_tuple(remote) or is_atom(remote),
|
||||
do: {remote, function, args}
|
||||
|
||||
def decompose_call({name, _, args}) when is_atom(name) and is_atom(args),
|
||||
do: {name, []}
|
||||
|
||||
def decompose_call({name, _, args}) when is_atom(name) and is_list(args),
|
||||
do: {name, args}
|
||||
|
||||
def decompose_call(_),
|
||||
do: :error
|
||||
|
||||
@doc """
|
||||
Recursively escapes a value so it can be inserted
|
||||
into a syntax tree.
|
||||
|
||||
One may pass `unquote: true` to `escape/2`
|
||||
which leaves `unquote` statements unescaped, effectively
|
||||
unquoting the contents on escape.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.escape(:foo)
|
||||
:foo
|
||||
|
||||
iex> Macro.escape({:a, :b, :c})
|
||||
{:{}, [], [:a, :b, :c]}
|
||||
|
||||
iex> Macro.escape({:unquote, [], [1]}, unquote: true)
|
||||
1
|
||||
|
||||
"""
|
||||
@spec escape(term) :: Macro.t
|
||||
@spec escape(term, Keyword.t) :: Macro.t
|
||||
def escape(expr, opts \\ []) do
|
||||
elem(:elixir_quote.escape(expr, Keyword.get(opts, :unquote, false)), 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates the given expressions are valid quoted expressions.
|
||||
|
||||
Check the `type:Macro.t` for the specification of a valid
|
||||
quoted expression.
|
||||
"""
|
||||
@spec validate(term) :: :ok | {:error, term}
|
||||
def validate(expr) do
|
||||
find_invalid(expr) || :ok
|
||||
end
|
||||
|
||||
defp find_invalid({left, right}), do:
|
||||
find_invalid(left) || find_invalid(right)
|
||||
|
||||
defp find_invalid({left, meta, right}) when is_list(meta) and (is_atom(right) or is_list(right)), do:
|
||||
find_invalid(left) || find_invalid(right)
|
||||
|
||||
defp find_invalid(list) when is_list(list), do:
|
||||
Enum.find_value(list, &find_invalid/1)
|
||||
|
||||
defp find_invalid(pid) when is_pid(pid), do: nil
|
||||
defp find_invalid(atom) when is_atom(atom), do: nil
|
||||
defp find_invalid(num) when is_number(num), do: nil
|
||||
defp find_invalid(bin) when is_binary(bin), do: nil
|
||||
|
||||
defp find_invalid(fun) when is_function(fun) do
|
||||
unless :erlang.fun_info(fun, :env) == {:env, []} and
|
||||
:erlang.fun_info(fun, :type) == {:type, :external} do
|
||||
{:error, fun}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_invalid(other), do: {:error, other}
|
||||
|
||||
@doc ~S"""
|
||||
Unescape the given chars.
|
||||
|
||||
This is the unescaping behaviour used by default in Elixir
|
||||
single- and double-quoted strings. Check `unescape_string/2`
|
||||
for information on how to customize the escaping map.
|
||||
|
||||
In this setup, Elixir will escape the following: `\0`, `\a`, `\b`,
|
||||
`\d`, `\e`, `\f`, `\n`, `\r`, `\s`, `\t` and `\v`. Hexadecimals
|
||||
are also supported via `\xNN` and `\x{NN...}` syntax.
|
||||
|
||||
This function is commonly used on sigil implementations
|
||||
(like `~r`, `~s` and others) which receive a raw, unescaped
|
||||
string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.unescape_string("example\\n")
|
||||
"example\n"
|
||||
|
||||
In the example above, we pass a string with `\n` escaped
|
||||
and return a version with it unescaped.
|
||||
"""
|
||||
@spec unescape_string(String.t) :: String.t
|
||||
def unescape_string(chars) do
|
||||
:elixir_interpolation.unescape_chars(chars)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Unescape the given chars according to the map given.
|
||||
|
||||
Check `unescape_string/1` if you want to use the same map
|
||||
as Elixir single- and double-quoted strings.
|
||||
|
||||
## Map
|
||||
|
||||
The map must be a function. The function receives an integer
|
||||
representing the codepoint of the character it wants to unescape.
|
||||
Here is the default mapping function implemented by Elixir:
|
||||
|
||||
def unescape_map(?0), do: ?0
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(?b), do: ?\b
|
||||
def unescape_map(?d), do: ?\d
|
||||
def unescape_map(?e), do: ?\e
|
||||
def unescape_map(?f), do: ?\f
|
||||
def unescape_map(?n), do: ?\n
|
||||
def unescape_map(?r), do: ?\r
|
||||
def unescape_map(?s), do: ?\s
|
||||
def unescape_map(?t), do: ?\t
|
||||
def unescape_map(?v), do: ?\v
|
||||
def unescape_map(?x), do: true
|
||||
def unescape_map(e), do: e
|
||||
|
||||
If the `unescape_map` function returns `false`. The char is
|
||||
not escaped and `\` is kept in the char list.
|
||||
|
||||
Hexadecimals will be escaped if the map function returns `true`
|
||||
for `?x`.
|
||||
|
||||
## Examples
|
||||
|
||||
Using the `unescape_map` function defined above is easy:
|
||||
|
||||
Macro.unescape_string "example\\n", &unescape_map(&1)
|
||||
|
||||
"""
|
||||
@spec unescape_string(String.t, (non_neg_integer -> non_neg_integer | false)) :: String.t
|
||||
def unescape_string(chars, map) do
|
||||
:elixir_interpolation.unescape_chars(chars, map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unescape the given tokens according to the default map.
|
||||
|
||||
Check `unescape_string/1` and `unescape_string/2` for more
|
||||
information about unescaping.
|
||||
|
||||
Only tokens that are binaries are unescaped, all others are
|
||||
ignored. This function is useful when implementing your own
|
||||
sigils. Check the implementation of `Kernel.sigil_s/2`
|
||||
for examples.
|
||||
"""
|
||||
@spec unescape_tokens([Macro.t]) :: [Macro.t]
|
||||
def unescape_tokens(tokens) do
|
||||
:elixir_interpolation.unescape_tokens(tokens)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unescape the given tokens according to the given map.
|
||||
|
||||
Check `unescape_tokens/1` and `unescape_string/2` for more information.
|
||||
"""
|
||||
@spec unescape_tokens([Macro.t], (non_neg_integer -> non_neg_integer | false)) :: [Macro.t]
|
||||
def unescape_tokens(tokens, map) do
|
||||
:elixir_interpolation.unescape_tokens(tokens, map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given expression to a binary.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.to_string(quote do: foo.bar(1, 2, 3))
|
||||
"foo.bar(1, 2, 3)"
|
||||
|
||||
"""
|
||||
@spec to_string(Macro.t) :: String.t
|
||||
@spec to_string(Macro.t, (Macro.t, String.t -> String.t)) :: String.t
|
||||
def to_string(tree, fun \\ fn(_ast, string) -> string end)
|
||||
|
||||
# Variables
|
||||
def to_string({var, _, atom} = ast, fun) when is_atom(atom) do
|
||||
fun.(ast, Atom.to_string(var))
|
||||
end
|
||||
|
||||
# Aliases
|
||||
def to_string({:__aliases__, _, refs} = ast, fun) do
|
||||
fun.(ast, Enum.map_join(refs, ".", &call_to_string(&1, fun)))
|
||||
end
|
||||
|
||||
# Blocks
|
||||
def to_string({:__block__, _, [expr]} = ast, fun) do
|
||||
fun.(ast, to_string(expr, fun))
|
||||
end
|
||||
|
||||
def to_string({:__block__, _, _} = ast, fun) do
|
||||
block = adjust_new_lines block_to_string(ast, fun), "\n "
|
||||
fun.(ast, "(\n " <> block <> "\n)")
|
||||
end
|
||||
|
||||
# Bits containers
|
||||
def to_string({:<<>>, _, args} = ast, fun) do
|
||||
fun.(ast, case Enum.map_join(args, ", ", &to_string(&1, fun)) do
|
||||
"<" <> rest -> "<< <" <> rest <> " >>"
|
||||
rest -> "<<" <> rest <> ">>"
|
||||
end)
|
||||
end
|
||||
|
||||
# Tuple containers
|
||||
def to_string({:{}, _, args} = ast, fun) do
|
||||
tuple = "{" <> Enum.map_join(args, ", ", &to_string(&1, fun)) <> "}"
|
||||
fun.(ast, tuple)
|
||||
end
|
||||
|
||||
# Map containers
|
||||
def to_string({:%{}, _, args} = ast, fun) do
|
||||
map = "%{" <> map_to_string(args, fun) <> "}"
|
||||
fun.(ast, map)
|
||||
end
|
||||
|
||||
def to_string({:%, _, [structname, map]} = ast, fun) do
|
||||
{:%{}, _, args} = map
|
||||
struct = "%" <> to_string(structname, fun) <> "{" <> map_to_string(args, fun) <> "}"
|
||||
fun.(ast, struct)
|
||||
end
|
||||
|
||||
# Fn keyword
|
||||
def to_string({:fn, _, [{:->, _, [_, tuple]}] = arrow} = ast, fun)
|
||||
when not is_tuple(tuple) or elem(tuple, 0) != :__block__ do
|
||||
fun.(ast, "fn " <> arrow_to_string(arrow, fun) <> " end")
|
||||
end
|
||||
|
||||
def to_string({:fn, _, [{:->, _, _}] = block} = ast, fun) do
|
||||
fun.(ast, "fn " <> block_to_string(block, fun) <> "\nend")
|
||||
end
|
||||
|
||||
def to_string({:fn, _, block} = ast, fun) do
|
||||
block = adjust_new_lines block_to_string(block, fun), "\n "
|
||||
fun.(ast, "fn\n " <> block <> "\nend")
|
||||
end
|
||||
|
||||
# left -> right
|
||||
def to_string([{:->, _, _}|_] = ast, fun) do
|
||||
fun.(ast, "(" <> arrow_to_string(ast, fun, true) <> ")")
|
||||
end
|
||||
|
||||
# left when right
|
||||
def to_string({:when, _, [left, right]} = ast, fun) do
|
||||
if right != [] and Keyword.keyword?(right) do
|
||||
right = kw_list_to_string(right, fun)
|
||||
else
|
||||
right = fun.(ast, op_to_string(right, fun, :when, :right))
|
||||
end
|
||||
|
||||
fun.(ast, op_to_string(left, fun, :when, :left) <> " when " <> right)
|
||||
end
|
||||
|
||||
# Binary ops
|
||||
def to_string({op, _, [left, right]} = ast, fun) when op in unquote(@binary_ops) do
|
||||
fun.(ast, op_to_string(left, fun, op, :left) <> " #{op} " <> op_to_string(right, fun, op, :right))
|
||||
end
|
||||
|
||||
# Splat when
|
||||
def to_string({:when, _, args} = ast, fun) do
|
||||
{left, right} = :elixir_utils.split_last(args)
|
||||
fun.(ast, "(" <> Enum.map_join(left, ", ", &to_string(&1, fun)) <> ") when " <> to_string(right, fun))
|
||||
end
|
||||
|
||||
# Unary ops
|
||||
def to_string({unary, _, [{binary, _, [_, _]} = arg]} = ast, fun)
|
||||
when unary in unquote(@unary_ops) and binary in unquote(@binary_ops) do
|
||||
fun.(ast, Atom.to_string(unary) <> "(" <> to_string(arg, fun) <> ")")
|
||||
end
|
||||
|
||||
def to_string({:not, _, [arg]} = ast, fun) do
|
||||
fun.(ast, "not " <> to_string(arg, fun))
|
||||
end
|
||||
|
||||
def to_string({op, _, [arg]} = ast, fun) when op in unquote(@unary_ops) do
|
||||
fun.(ast, Atom.to_string(op) <> to_string(arg, fun))
|
||||
end
|
||||
|
||||
# Access
|
||||
def to_string({{:., _, [Access, :get]}, _, [left, right]} = ast, fun) do
|
||||
fun.(ast, to_string(left, fun) <> to_string([right], fun))
|
||||
end
|
||||
|
||||
# All other calls
|
||||
def to_string({target, _, args} = ast, fun) when is_list(args) do
|
||||
if sigil = sigil_call(ast, fun) do
|
||||
sigil
|
||||
else
|
||||
{list, last} = :elixir_utils.split_last(args)
|
||||
fun.(ast, case kw_blocks?(last) do
|
||||
true -> call_to_string_with_args(target, list, fun) <> kw_blocks_to_string(last, fun)
|
||||
false -> call_to_string_with_args(target, args, fun)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
# Two-item tuples
|
||||
def to_string({left, right}, fun) do
|
||||
to_string({:{}, [], [left, right]}, fun)
|
||||
end
|
||||
|
||||
# Lists
|
||||
def to_string(list, fun) when is_list(list) do
|
||||
fun.(list, cond do
|
||||
list == [] ->
|
||||
"[]"
|
||||
:io_lib.printable_list(list) ->
|
||||
"'" <> Inspect.BitString.escape(IO.chardata_to_string(list), ?') <> "'"
|
||||
Keyword.keyword?(list) ->
|
||||
"[" <> kw_list_to_string(list, fun) <> "]"
|
||||
true ->
|
||||
"[" <> Enum.map_join(list, ", ", &to_string(&1, fun)) <> "]"
|
||||
end)
|
||||
end
|
||||
|
||||
# All other structures
|
||||
def to_string(other, fun), do: fun.(other, inspect(other, []))
|
||||
|
||||
# Block keywords
|
||||
@kw_keywords [:do, :catch, :rescue, :after, :else]
|
||||
|
||||
defp kw_blocks?([_|_] = kw) do
|
||||
Enum.all?(kw, &match?({x, _} when x in unquote(@kw_keywords), &1))
|
||||
end
|
||||
defp kw_blocks?(_), do: false
|
||||
|
||||
defp module_to_string(atom, _fun) when is_atom(atom), do: inspect(atom, [])
|
||||
defp module_to_string(other, fun), do: call_to_string(other, fun)
|
||||
|
||||
defp sigil_call({func, _, [{:<<>>, _, [string]}, args]} = ast, fun) when is_list(args) do
|
||||
sigil =
|
||||
case Atom.to_string(func) do
|
||||
<<"sigil_", name>> ->
|
||||
"~" <> <<name>> <>
|
||||
fun.(string, inspect(string, [])) <>
|
||||
sigil_args(args, fun)
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
fun.(ast, sigil)
|
||||
end
|
||||
|
||||
defp sigil_call(_other, _fun) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp sigil_args([], _fun), do: ""
|
||||
defp sigil_args(args, fun), do: fun.(args, List.to_string(args))
|
||||
|
||||
defp call_to_string(atom, _fun) when is_atom(atom), do: Atom.to_string(atom)
|
||||
defp call_to_string({:., _, [arg]}, fun), do: module_to_string(arg, fun) <> "."
|
||||
defp call_to_string({:., _, [left, right]}, fun), do: module_to_string(left, fun) <> "." <> call_to_string(right, fun)
|
||||
defp call_to_string(other, fun), do: to_string(other, fun)
|
||||
|
||||
defp call_to_string_with_args(target, args, fun) do
|
||||
target = call_to_string(target, fun)
|
||||
args = args_to_string(args, fun)
|
||||
target <> "(" <> args <> ")"
|
||||
end
|
||||
|
||||
defp args_to_string(args, fun) do
|
||||
{list, last} = :elixir_utils.split_last(args)
|
||||
|
||||
if last != [] and Keyword.keyword?(last) do
|
||||
args = Enum.map_join(list, ", ", &to_string(&1, fun))
|
||||
if list != [], do: args = args <> ", "
|
||||
args <> kw_list_to_string(last, fun)
|
||||
else
|
||||
Enum.map_join(args, ", ", &to_string(&1, fun))
|
||||
end
|
||||
end
|
||||
|
||||
defp kw_blocks_to_string(kw, fun) do
|
||||
Enum.reduce(@kw_keywords, " ", fn(x, acc) ->
|
||||
case Keyword.has_key?(kw, x) do
|
||||
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
|
||||
false -> acc
|
||||
end
|
||||
end) <> "end"
|
||||
end
|
||||
|
||||
defp kw_block_to_string(key, value, fun) do
|
||||
block = adjust_new_lines block_to_string(value, fun), "\n "
|
||||
Atom.to_string(key) <> "\n " <> block <> "\n"
|
||||
end
|
||||
|
||||
defp block_to_string([{:->, _, _}|_] = block, fun) do
|
||||
Enum.map_join(block, "\n", fn({:->, _, [left, right]}) ->
|
||||
left = comma_join_or_empty_paren(left, fun, false)
|
||||
left <> "->\n " <> adjust_new_lines block_to_string(right, fun), "\n "
|
||||
end)
|
||||
end
|
||||
|
||||
defp block_to_string({:__block__, _, exprs}, fun) do
|
||||
Enum.map_join(exprs, "\n", &to_string(&1, fun))
|
||||
end
|
||||
|
||||
defp block_to_string(other, fun), do: to_string(other, fun)
|
||||
|
||||
defp map_to_string([{:|, _, [update_map, update_args]}], fun) do
|
||||
to_string(update_map, fun) <> " | " <> map_to_string(update_args, fun)
|
||||
end
|
||||
|
||||
defp map_to_string(list, fun) do
|
||||
cond do
|
||||
Keyword.keyword?(list) -> kw_list_to_string(list, fun)
|
||||
true -> map_list_to_string(list, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp kw_list_to_string(list, fun) do
|
||||
Enum.map_join(list, ", ", fn {key, value} ->
|
||||
atom_name = case Inspect.Atom.inspect(key) do
|
||||
":" <> rest -> rest
|
||||
other -> other
|
||||
end
|
||||
atom_name <> ": " <> to_string(value, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
defp map_list_to_string(list, fun) do
|
||||
Enum.map_join(list, ", ", fn {key, value} ->
|
||||
to_string(key, fun) <> " => " <> to_string(value, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
defp parenthise(expr, fun) do
|
||||
"(" <> to_string(expr, fun) <> ")"
|
||||
end
|
||||
|
||||
defp op_to_string({op, _, [_, _]} = expr, fun, parent_op, side) when op in unquote(@binary_ops) do
|
||||
{parent_assoc, parent_prec} = binary_op_props(parent_op)
|
||||
{_, prec} = binary_op_props(op)
|
||||
cond do
|
||||
parent_prec < prec -> to_string(expr, fun)
|
||||
parent_prec > prec -> parenthise(expr, fun)
|
||||
true ->
|
||||
# parent_prec == prec, so look at associativity.
|
||||
if parent_assoc == side do
|
||||
to_string(expr, fun)
|
||||
else
|
||||
parenthise(expr, fun)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp op_to_string(expr, fun, _, _), do: to_string(expr, fun)
|
||||
|
||||
defp arrow_to_string(pairs, fun, paren \\ false) do
|
||||
Enum.map_join(pairs, "; ", fn({:->, _, [left, right]}) ->
|
||||
left = comma_join_or_empty_paren(left, fun, paren)
|
||||
left <> "-> " <> to_string(right, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
defp comma_join_or_empty_paren([], _fun, true), do: "() "
|
||||
defp comma_join_or_empty_paren([], _fun, false), do: ""
|
||||
|
||||
defp comma_join_or_empty_paren(left, fun, _) do
|
||||
Enum.map_join(left, ", ", &to_string(&1, fun)) <> " "
|
||||
end
|
||||
|
||||
defp adjust_new_lines(block, replacement) do
|
||||
for <<x <- block>>, into: "" do
|
||||
case x == ?\n do
|
||||
true -> replacement
|
||||
false -> <<x>>
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives an AST node and expands it once.
|
||||
|
||||
The following contents are expanded:
|
||||
|
||||
* Macros (local or remote)
|
||||
* Aliases are expanded (if possible) and return atoms
|
||||
* Pseudo-variables (`__ENV__`, `__MODULE__` and `__DIR__`)
|
||||
* Module attributes reader (`@foo`)
|
||||
|
||||
If the expression cannot be expanded, it returns the expression
|
||||
itself. Notice that `expand_once/2` performs the expansion just
|
||||
once and it is not recursive. Check `expand/2` for expansion
|
||||
until the node can no longer be expanded.
|
||||
|
||||
## Examples
|
||||
|
||||
In the example below, we have a macro that generates a module
|
||||
with a function named `name_length` that returns the length
|
||||
of the module name. The value of this function will be calculated
|
||||
at compilation time and not at runtime.
|
||||
|
||||
Consider the implementation below:
|
||||
|
||||
defmacro defmodule_with_length(name, do: block) do
|
||||
length = length(Atom.to_char_list(name))
|
||||
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
def name_length, do: unquote(length)
|
||||
unquote(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
When invoked like this:
|
||||
|
||||
defmodule_with_length My.Module do
|
||||
def other_function, do: ...
|
||||
end
|
||||
|
||||
The compilation will fail because `My.Module` when quoted
|
||||
is not an atom, but a syntax tree as follow:
|
||||
|
||||
{:__aliases__, [], [:My, :Module]}
|
||||
|
||||
That said, we need to expand the aliases node above to an
|
||||
atom, so we can retrieve its length. Expanding the node is
|
||||
not straight-forward because we also need to expand the
|
||||
caller aliases. For example:
|
||||
|
||||
alias MyHelpers, as: My
|
||||
|
||||
defmodule_with_length My.Module do
|
||||
def other_function, do: ...
|
||||
end
|
||||
|
||||
The final module name will be `MyHelpers.Module` and not
|
||||
`My.Module`. With `Macro.expand/2`, such aliases are taken
|
||||
into consideration. Local and remote macros are also
|
||||
expanded. We could rewrite our macro above to use this
|
||||
function as:
|
||||
|
||||
defmacro defmodule_with_length(name, do: block) do
|
||||
expanded = Macro.expand(name, __CALLER__)
|
||||
length = length(Atom.to_char_list(expanded))
|
||||
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
def name_length, do: unquote(length)
|
||||
unquote(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
def expand_once(ast, env) do
|
||||
elem(do_expand_once(ast, env), 0)
|
||||
end
|
||||
|
||||
defp do_expand_once({:__aliases__, _, _} = original, env) do
|
||||
case :elixir_aliases.expand(original, env.aliases, env.macro_aliases, env.lexical_tracker) do
|
||||
receiver when is_atom(receiver) ->
|
||||
:elixir_lexical.record_remote(receiver, env.lexical_tracker)
|
||||
{receiver, true}
|
||||
aliases ->
|
||||
aliases = for alias <- aliases, do: elem(do_expand_once(alias, env), 0)
|
||||
|
||||
case :lists.all(&is_atom/1, aliases) do
|
||||
true ->
|
||||
receiver = :elixir_aliases.concat(aliases)
|
||||
:elixir_lexical.record_remote(receiver, env.lexical_tracker)
|
||||
{receiver, true}
|
||||
false ->
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Expand @ calls
|
||||
defp do_expand_once({:@, _, [{name, _, args}]} = original, env) when is_atom(args) or args == [] do
|
||||
case (module = env.module) && Module.open?(module) do
|
||||
true -> {escape(Module.get_attribute(module, name)), true}
|
||||
false -> {original, false}
|
||||
end
|
||||
end
|
||||
|
||||
# Expand pseudo-variables
|
||||
defp do_expand_once({:__MODULE__, _, atom}, env) when is_atom(atom),
|
||||
do: {env.module, true}
|
||||
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
|
||||
do: {:filename.dirname(env.file), true}
|
||||
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom),
|
||||
do: {{:%{}, [], Map.to_list(env)}, true}
|
||||
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env) when
|
||||
is_atom(atom) and is_atom(field) do
|
||||
if Map.has_key?(env, field) do
|
||||
{Map.get(env, field), true}
|
||||
else
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
|
||||
# Expand possible macro import invocation
|
||||
defp do_expand_once({atom, meta, context} = original, env)
|
||||
when is_atom(atom) and is_list(meta) and is_atom(context) do
|
||||
if :lists.member({atom, Keyword.get(meta, :counter, context)}, env.vars) do
|
||||
{original, false}
|
||||
else
|
||||
case do_expand_once({atom, meta, []}, env) do
|
||||
{_, true} = exp -> exp
|
||||
{_, false} -> {original, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_expand_once({atom, meta, args} = original, env)
|
||||
when is_atom(atom) and is_list(args) and is_list(meta) do
|
||||
arity = length(args)
|
||||
|
||||
if :elixir_import.special_form(atom, arity) do
|
||||
{original, false}
|
||||
else
|
||||
module = env.module
|
||||
extra = if function_exported?(module, :__info__, 1) do
|
||||
[{module, module.__info__(:macros)}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
expand = :elixir_dispatch.expand_import(meta, {atom, length(args)}, args,
|
||||
env, extra, true)
|
||||
|
||||
case expand do
|
||||
{:ok, receiver, quoted} ->
|
||||
next = :elixir_counter.next
|
||||
{:elixir_quote.linify_with_context_counter(0, {receiver, next}, quoted), true}
|
||||
{:ok, _receiver, _name, _args} ->
|
||||
{original, false}
|
||||
:error ->
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Expand possible macro require invocation
|
||||
defp do_expand_once({{:., _, [left, right]}, meta, args} = original, env) when is_atom(right) do
|
||||
{receiver, _} = do_expand_once(left, env)
|
||||
|
||||
case is_atom(receiver) do
|
||||
false -> {original, false}
|
||||
true ->
|
||||
expand = :elixir_dispatch.expand_require(meta, receiver, {right, length(args)}, args, env)
|
||||
|
||||
case expand do
|
||||
{:ok, receiver, quoted} ->
|
||||
next = :elixir_counter.next
|
||||
{:elixir_quote.linify_with_context_counter(0, {receiver, next}, quoted), true}
|
||||
:error ->
|
||||
{original, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Anything else is just returned
|
||||
defp do_expand_once(other, _env), do: {other, false}
|
||||
|
||||
@doc """
|
||||
Receives an AST node and expands it until it can no longer
|
||||
be expanded.
|
||||
|
||||
This function uses `expand_once/2` under the hood. Check
|
||||
`expand_once/2` for more information and examples.
|
||||
"""
|
||||
def expand(tree, env) do
|
||||
expand_until({tree, true}, env)
|
||||
end
|
||||
|
||||
defp expand_until({tree, true}, env) do
|
||||
expand_until(do_expand_once(tree, env), env)
|
||||
end
|
||||
|
||||
defp expand_until({tree, false}, _env) do
|
||||
tree
|
||||
end
|
||||
end
|
||||
@@ -1,136 +0,0 @@
|
||||
defmodule Macro.Env do
|
||||
@moduledoc """
|
||||
A struct that holds compile time environment information.
|
||||
|
||||
The current environment can be accessed at any time as
|
||||
`__ENV__`. Inside macros, the caller environment can be
|
||||
accessed as `__CALLER__`.
|
||||
|
||||
An instance of `Macro.Env` must not be modified by hand. If you need to
|
||||
create a custom environment to pass to `Code.eval_quoted/3`, use the
|
||||
following trick:
|
||||
|
||||
def make_custom_env do
|
||||
import SomeModule, only: [some_function: 2]
|
||||
alias A.B.C
|
||||
__ENV__
|
||||
end
|
||||
|
||||
You may then call `make_custom_env()` to get a struct with the desired
|
||||
imports and aliases included.
|
||||
|
||||
It contains the following fields:
|
||||
|
||||
* `module` - the current module name
|
||||
* `file` - the current file name as a binary
|
||||
* `line` - the current line as an integer
|
||||
* `function` - a tuple as `{atom, integer`}, where the first
|
||||
element is the function name and the seconds its arity; returns
|
||||
`nil` if not inside a function
|
||||
* `context` - the context of the environment; it can be `nil`
|
||||
(default context), inside a guard or inside an assign
|
||||
* `aliases` - a list of two item tuples, where the first
|
||||
item is the aliased name and the second the actual name
|
||||
* `requires` - the list of required modules
|
||||
* `functions` - a list of functions imported from each module
|
||||
* `macros` - a list of macros imported from each module
|
||||
* `macro_aliases` - a list of aliases defined inside the current macro
|
||||
* `context_modules` - a list of modules defined in the current context
|
||||
* `vars` - a list keeping all defined variables as `{var, context}`
|
||||
* `export_vars` - a list keeping all variables to be exported in a
|
||||
construct (may be `nil`)
|
||||
* `lexical_tracker` - PID of the lexical tracker which is responsible to
|
||||
keep user info
|
||||
* `local` - the module to expand local functions to
|
||||
"""
|
||||
|
||||
@type name_arity :: {atom, arity}
|
||||
@type file :: binary
|
||||
@type line :: non_neg_integer
|
||||
@type aliases :: [{module, module}]
|
||||
@type macro_aliases :: [{module, {integer, module}}]
|
||||
@type context :: :match | :guard | nil
|
||||
@type requires :: [module]
|
||||
@type functions :: [{module, [name_arity]}]
|
||||
@type macros :: [{module, [name_arity]}]
|
||||
@type context_modules :: [module]
|
||||
@type vars :: [{atom, atom | non_neg_integer}]
|
||||
@type export_vars :: vars | nil
|
||||
@type lexical_tracker :: pid
|
||||
@type local :: atom | nil
|
||||
|
||||
@type t :: %{__struct__: __MODULE__,
|
||||
module: atom,
|
||||
file: file,
|
||||
line: line,
|
||||
function: name_arity | nil,
|
||||
context: context,
|
||||
requires: requires,
|
||||
aliases: aliases,
|
||||
functions: functions,
|
||||
macros: macros,
|
||||
macro_aliases: aliases,
|
||||
context_modules: context_modules,
|
||||
vars: vars,
|
||||
export_vars: export_vars,
|
||||
lexical_tracker: lexical_tracker,
|
||||
local: local}
|
||||
|
||||
def __struct__ do
|
||||
%{__struct__: __MODULE__,
|
||||
module: nil,
|
||||
file: "nofile",
|
||||
line: 0,
|
||||
function: nil,
|
||||
context: nil,
|
||||
requires: [],
|
||||
aliases: [],
|
||||
functions: [],
|
||||
macros: [],
|
||||
macro_aliases: [],
|
||||
context_modules: [],
|
||||
vars: [],
|
||||
export_vars: nil,
|
||||
lexical_tracker: nil,
|
||||
local: nil}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a keyword list containing the file and line
|
||||
information as keys.
|
||||
"""
|
||||
def location(%{__struct__: Macro.Env, file: file, line: line}) do
|
||||
[file: file, line: line]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether the compilation environment is currently
|
||||
inside a guard.
|
||||
"""
|
||||
def in_guard?(%{__struct__: Macro.Env, context: context}), do: context == :guard
|
||||
|
||||
@doc """
|
||||
Returns whether the compilation environment is currently
|
||||
inside a match clause.
|
||||
"""
|
||||
def in_match?(%{__struct__: Macro.Env, context: context}), do: context == :match
|
||||
|
||||
@doc """
|
||||
Returns the environment stacktrace.
|
||||
"""
|
||||
def stacktrace(%{__struct__: Macro.Env} = env) do
|
||||
cond do
|
||||
is_nil(env.module) ->
|
||||
[{:elixir_compiler, :__FILE__, 1, relative_location(env)}]
|
||||
is_nil(env.function) ->
|
||||
[{env.module, :__MODULE__, 0, relative_location(env)}]
|
||||
true ->
|
||||
{name, arity} = env.function
|
||||
[{env.module, name, arity, relative_location(env)}]
|
||||
end
|
||||
end
|
||||
|
||||
defp relative_location(env) do
|
||||
[file: Path.relative_to_cwd(env.file), line: env.line]
|
||||
end
|
||||
end
|
||||
@@ -1,69 +0,0 @@
|
||||
defmodule Map do
|
||||
@moduledoc """
|
||||
A Dict implementation that works on maps.
|
||||
|
||||
Maps are key-value stores where keys are compared using
|
||||
the match operator (`===`). Maps can be created with
|
||||
the `%{}` special form defined in the `Kernel.SpecialForms`
|
||||
module.
|
||||
|
||||
For more information about the functions in this module and
|
||||
their APIs, please consult the `Dict` module.
|
||||
"""
|
||||
|
||||
use Dict
|
||||
|
||||
defdelegate [keys(map), values(map), size(map), merge(map1, map2), to_list(map)], to: :maps
|
||||
|
||||
@compile {:inline, fetch: 2, put: 3, delete: 2, has_key?: 2}
|
||||
|
||||
@doc """
|
||||
Returns a new empty map.
|
||||
"""
|
||||
def new, do: %{}
|
||||
|
||||
def has_key?(map, key), do: :maps.is_key(key, map)
|
||||
|
||||
def fetch(map, key), do: :maps.find(key, map)
|
||||
|
||||
def put(map, key, val) do
|
||||
:maps.put(key, val, map)
|
||||
end
|
||||
|
||||
def delete(map, key), do: :maps.remove(key, map)
|
||||
|
||||
def merge(map1, map2, callback) do
|
||||
:maps.fold fn k, v2, acc ->
|
||||
update(acc, k, v2, fn(v1) -> callback.(k, v1, v2) end)
|
||||
end, map1, map2
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a struct to map.
|
||||
|
||||
It accepts the struct module or a struct itself and
|
||||
simply removes the `__struct__` field from the struct.
|
||||
|
||||
## Example
|
||||
|
||||
defmodule User do
|
||||
defstruct [:name]
|
||||
end
|
||||
|
||||
Map.from_struct(User)
|
||||
#=> %{name: nil}
|
||||
|
||||
Map.from_struct(%User{name: "john"})
|
||||
#=> %{name: "john"}
|
||||
|
||||
"""
|
||||
def from_struct(struct) when is_atom(struct) do
|
||||
:maps.remove(:__struct__, struct.__struct__)
|
||||
end
|
||||
|
||||
def from_struct(%{__struct__: _} = struct) do
|
||||
:maps.remove(:__struct__, struct)
|
||||
end
|
||||
|
||||
def equal?(%{} = map1, %{} = map2), do: map1 === map2
|
||||
end
|
||||
@@ -1,972 +0,0 @@
|
||||
defmodule Module do
|
||||
@moduledoc ~S'''
|
||||
This module provides many functions to deal with modules during
|
||||
compilation time. It allows a developer to dynamically attach
|
||||
documentation, add, delete and register attributes and so forth.
|
||||
|
||||
After a module is compiled, using many of the functions in
|
||||
this module will raise errors, since it is out of their scope
|
||||
to inspect runtime data. Most of the runtime data can be inspected
|
||||
via the `__info__(attr)` function attached to each compiled module.
|
||||
|
||||
## Module attributes
|
||||
|
||||
Each module can be decorated with one or more attributes. The following ones
|
||||
are currently defined by Elixir:
|
||||
|
||||
* `@after_compile`
|
||||
|
||||
A hook that will be invoked right after the current module is compiled.
|
||||
|
||||
Accepts a module or a tuple `{<module>, <function atom>}`. The function
|
||||
must take two arguments: the module environment and its bytecode.
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__after_compile__/2`.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@after_compile __MODULE__
|
||||
|
||||
def __after_compile__(env, _bytecode) do
|
||||
IO.inspect env
|
||||
end
|
||||
end
|
||||
|
||||
* `@before_compile`
|
||||
|
||||
A hook that will be invoked before the module is compiled.
|
||||
|
||||
Accepts a module or a tuple `{<module>, <function/macro atom>}`. The
|
||||
function/macro must take one argument: the module environment. If it's a
|
||||
macro, its returned value will be injected at the end of the module definition
|
||||
before the compilation starts.
|
||||
|
||||
When just a module is provided, the function/macro is assumed to be
|
||||
`__before_compile__/1`.
|
||||
|
||||
Note: unlike `@after_compile`, the callback function/macro must
|
||||
be placed in a separate module (because when the callback is invoked,
|
||||
the current module does not yet exist).
|
||||
|
||||
### Example
|
||||
|
||||
defmodule A do
|
||||
defmacro __before_compile__(_env) do
|
||||
quote do
|
||||
def hello, do: "world"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule B do
|
||||
@before_compile A
|
||||
end
|
||||
|
||||
* `@behaviour` (notice the British spelling)
|
||||
|
||||
Specify an OTP or user-defined behaviour.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@behaviour gen_event
|
||||
|
||||
# ...
|
||||
end
|
||||
|
||||
* `@compile`
|
||||
|
||||
Define options for module compilation that are passed to the Erlang
|
||||
compiler.
|
||||
|
||||
Accepts an atom, a tuple, or a list of atoms and tuples.
|
||||
|
||||
See http://www.erlang.org/doc/man/compile.html for the list of supported
|
||||
options.
|
||||
|
||||
Several uses of `@compile` will accumulate instead of overriding
|
||||
previous ones.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@compile {:inline, myfun: 1}
|
||||
|
||||
def myfun(arg) do
|
||||
to_string(arg)
|
||||
end
|
||||
end
|
||||
|
||||
* `@doc`
|
||||
|
||||
Provide documentation for the function or macro that follows the
|
||||
attribute.
|
||||
|
||||
Accepts a string (often a heredoc) or `false` where `@doc false` will
|
||||
make the function/macro invisible to the documentation extraction tools
|
||||
like ExDoc.
|
||||
|
||||
Can be invoked more than once.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@doc "Hello world"
|
||||
def hello do
|
||||
"world"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sum.
|
||||
"""
|
||||
def sum(a, b) do
|
||||
a + b
|
||||
end
|
||||
end
|
||||
|
||||
* `@file`
|
||||
|
||||
Change the filename used in stacktraces for the function or macro that
|
||||
follows the attribute.
|
||||
|
||||
Accepts a string. Can be used more than once.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@doc "Hello world"
|
||||
@file "hello.ex"
|
||||
def hello do
|
||||
"world"
|
||||
end
|
||||
end
|
||||
|
||||
* `@moduledoc`
|
||||
|
||||
Provide documentation for the current module.
|
||||
|
||||
Accepts a string (which is often a heredoc) or `false` where
|
||||
`@moduledoc false` will make the module invisible to the
|
||||
documentation extraction tools like ExDoc.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@moduledoc """
|
||||
A very useful module
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
* `@on_definition`
|
||||
|
||||
A hook that will be invoked when each function or macro in the current
|
||||
module is defined. Useful when annotating functions.
|
||||
|
||||
Accepts a module or a tuple `{<module>, <function atom>}`. The function
|
||||
must take 6 arguments:
|
||||
|
||||
- the module environment
|
||||
- kind: `:def`, `:defp`, `:defmacro`, or `:defmacrop`
|
||||
- function/macro name
|
||||
- list of expanded arguments
|
||||
- list of expanded guards
|
||||
- expanded function body
|
||||
|
||||
Note the hook receives the expanded arguments and it is invoked before
|
||||
the function is stored in the module. So `Module.defines?/2` will return
|
||||
false for the first clause of every function.
|
||||
|
||||
If the function/macro being defined has multiple clauses, the hook will
|
||||
be called for each clause.
|
||||
|
||||
Unlike other hooks, `@on_definition` will only invoke functions
|
||||
and never macros. This is because the hook is invoked inside the context
|
||||
of the function (and nested function definitions are not allowed in
|
||||
Elixir).
|
||||
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__on_definition__/6`.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule H do
|
||||
def on_def(_env, kind, name, args, guards, body) do
|
||||
IO.puts "Defining #{kind} named #{name} with args:"
|
||||
IO.inspect args
|
||||
IO.puts "and guards"
|
||||
IO.inspect guards
|
||||
IO.puts "and body"
|
||||
IO.puts Macro.to_string(body)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule M do
|
||||
@on_definition {H, :on_def}
|
||||
|
||||
def hello(arg) when is_binary(arg) or is_list(arg) do
|
||||
"Hello" <> to_string(arg)
|
||||
end
|
||||
|
||||
def hello(_) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
* `@on_load`
|
||||
|
||||
A hook that will be invoked whenever the module is loaded.
|
||||
|
||||
Accepts a function atom of a function in the current module. The function
|
||||
must have arity 0 (no arguments) and has to return `:ok`, otherwise the
|
||||
loading of the module will be aborted.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@on_load :load_check
|
||||
|
||||
def load_check do
|
||||
if some_condition() do
|
||||
:ok
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def some_condition do
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
* `@vsn`
|
||||
|
||||
Specify the module version. Accepts any valid Elixir value.
|
||||
|
||||
### Example
|
||||
|
||||
defmodule M do
|
||||
@vsn "1.0"
|
||||
end
|
||||
|
||||
* `@external_resource`
|
||||
|
||||
Specify an external resource to the current module.
|
||||
|
||||
Many times a module embeds information from an external file. This
|
||||
attribute allows the module to annotate which external resources
|
||||
have been used.
|
||||
|
||||
Tools like Mix may use this information to ensure the module is
|
||||
recompiled in case any of the external resources change.
|
||||
|
||||
The following attributes are part of typespecs and are also reserved by
|
||||
Elixir (see `Kernel.Typespec` for more information about typespecs):
|
||||
|
||||
* `@type` - defines a type to be used in `@spec`
|
||||
* `@typep` - defines a private type to be used in `@spec`
|
||||
* `@opaque` - defines an opaque type to be used in `@spec`
|
||||
* `@spec` - provides a specification for a function
|
||||
* `@callback` - provides a specification for the behaviour callback
|
||||
|
||||
In addition to the built-in attributes outlined above, custom attributes may
|
||||
also be added. A custom attribute is any valid identifier prefixed with an
|
||||
`@` and followed by a valid Elixir value:
|
||||
|
||||
defmodule M do
|
||||
@custom_attr [some: "stuff"]
|
||||
end
|
||||
|
||||
For more advanced options available when defining custom attributes, see
|
||||
`register_attribute/3`.
|
||||
|
||||
## Runtime information about a module
|
||||
|
||||
It is possible to query a module at runtime to find out which functions and
|
||||
macros it defines, extract its docstrings, etc. See `__info__/1`.
|
||||
'''
|
||||
|
||||
@doc """
|
||||
Provides runtime information about functions and macros defined by the
|
||||
module, enables docstring extraction, etc.
|
||||
|
||||
Each module gets an `__info__/1` function when it's compiled. The function
|
||||
takes one of the following atoms:
|
||||
|
||||
* `:functions` - keyword list of public functions along with their arities
|
||||
|
||||
* `:macros` - keyword list of public macros along with their arities
|
||||
|
||||
* `:module` - module name (`Module == Module.__info__(:module)`)
|
||||
|
||||
In addition to the above, you may also pass to `__info__/1` any atom supported
|
||||
by Erlang's `module_info` function which also gets defined for each compiled
|
||||
module. See http://erlang.org/doc/reference_manual/modules.html#id69430 for
|
||||
more information.
|
||||
"""
|
||||
def __info__(kind)
|
||||
|
||||
@doc """
|
||||
Check if a module is open, i.e. it is currently being defined
|
||||
and its attributes and functions can be modified.
|
||||
"""
|
||||
def open?(module) do
|
||||
:elixir_module.is_open(module)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluates the quoted contents in the given module's context.
|
||||
|
||||
A list of environment options can also be given as argument.
|
||||
See `Code.eval_string/3` for more information.
|
||||
|
||||
Raises an error if the module was already compiled.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
contents = quote do: (def sum(a, b), do: a + b)
|
||||
Module.eval_quoted __MODULE__, contents
|
||||
end
|
||||
|
||||
Foo.sum(1, 2) #=> 3
|
||||
|
||||
For convenience, you can my pass `__ENV__` as argument and
|
||||
all options will be automatically extracted from the environment:
|
||||
|
||||
defmodule Foo do
|
||||
contents = quote do: (def sum(a, b), do: a + b)
|
||||
Module.eval_quoted __MODULE__, contents, [], __ENV__
|
||||
end
|
||||
|
||||
Foo.sum(1, 2) #=> 3
|
||||
|
||||
"""
|
||||
def eval_quoted(module, quoted, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_quoted(%Macro.Env{} = env, quoted, binding, opts) do
|
||||
eval_quoted(env.module, quoted, binding, Keyword.merge(Map.to_list(env), opts))
|
||||
end
|
||||
|
||||
def eval_quoted(module, quoted, binding, %Macro.Env{} = env) do
|
||||
eval_quoted(module, quoted, binding, Map.to_list(env))
|
||||
end
|
||||
|
||||
def eval_quoted(module, quoted, binding, opts) do
|
||||
assert_not_compiled!(:eval_quoted, module)
|
||||
:elixir_def.reset_last(module)
|
||||
{value, binding, _env, _scope} =
|
||||
:elixir.eval_quoted quoted, binding, Keyword.put(opts, :module, module)
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a module with the given name and defined by
|
||||
the given quoted expressions.
|
||||
|
||||
The line where the module is defined and its file **must**
|
||||
be passed as options.
|
||||
|
||||
## Examples
|
||||
|
||||
contents =
|
||||
quote do
|
||||
def world, do: true
|
||||
end
|
||||
|
||||
Module.create(Hello, contents, Macro.Env.location(__ENV__))
|
||||
|
||||
Hello.world #=> true
|
||||
|
||||
## Differences from `defmodule`
|
||||
|
||||
`Module.create` works similarly to `defmodule` and
|
||||
return the same results. While one could also use
|
||||
`defmodule` to define modules dynamically, this
|
||||
function is preferred when the module body is given
|
||||
by a quoted expression.
|
||||
|
||||
Another important distinction is that `Module.create`
|
||||
allows you to control the environment variables used
|
||||
when defining the module, while `defmodule` automatically
|
||||
shares the same environment.
|
||||
"""
|
||||
def create(module, quoted, opts)
|
||||
|
||||
def create(module, quoted, %Macro.Env{} = env) do
|
||||
create(module, quoted, Map.to_list(env))
|
||||
end
|
||||
|
||||
def create(module, quoted, opts) when is_atom(module) and is_list(opts) do
|
||||
unless Keyword.has_key?(opts, :file) do
|
||||
raise ArgumentError, "expected :file to be given as option"
|
||||
end
|
||||
:elixir_module.compile(module, quoted, [], :elixir.env_for_eval(opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates a list of aliases and returns a new alias.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.concat([Foo, Bar])
|
||||
Foo.Bar
|
||||
|
||||
iex> Module.concat([Foo, "Bar"])
|
||||
Foo.Bar
|
||||
|
||||
"""
|
||||
@spec concat([binary | atom]) :: atom
|
||||
def concat(list) when is_list(list) do
|
||||
:elixir_aliases.concat(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates two aliases and returns a new alias.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.concat(Foo, Bar)
|
||||
Foo.Bar
|
||||
|
||||
iex> Module.concat(Foo, "Bar")
|
||||
Foo.Bar
|
||||
|
||||
"""
|
||||
@spec concat(binary | atom, binary | atom) :: atom
|
||||
def concat(left, right) do
|
||||
:elixir_aliases.concat([left, right])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates a list of aliases and returns a new alias only
|
||||
if the alias was already referenced. If the alias was not
|
||||
referenced yet, fails with `ArgumentError`.
|
||||
It handles char lists, binaries and atoms.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.safe_concat([Unknown, Module])
|
||||
** (ArgumentError) argument error
|
||||
|
||||
iex> Module.safe_concat([List, Chars])
|
||||
List.Chars
|
||||
|
||||
"""
|
||||
@spec safe_concat([binary | atom]) :: atom | no_return
|
||||
def safe_concat(list) when is_list(list) do
|
||||
:elixir_aliases.safe_concat(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates two aliases and returns a new alias only
|
||||
if the alias was already referenced. If the alias was not
|
||||
referenced yet, fails with `ArgumentError`.
|
||||
It handles char lists, binaries and atoms.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.safe_concat(Unknown, Module)
|
||||
** (ArgumentError) argument error
|
||||
|
||||
iex> Module.safe_concat(List, Chars)
|
||||
List.Chars
|
||||
|
||||
"""
|
||||
@spec safe_concat(binary | atom, binary | atom) :: atom | no_return
|
||||
def safe_concat(left, right) do
|
||||
:elixir_aliases.safe_concat([left, right])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attaches documentation to a given function or type. It expects
|
||||
the module the function/type belongs to, the line (a non negative
|
||||
integer), the kind (`def` or `defmacro`), a tuple representing
|
||||
the function and its arity, the function signature (the signature
|
||||
should be omitted for types) and the documentation, which should
|
||||
be either a binary or a boolean.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.add_doc(__MODULE__, __ENV__.line + 1, :def, {:version, 0}, [], "Manually added docs")
|
||||
def version, do: 1
|
||||
end
|
||||
|
||||
"""
|
||||
def add_doc(module, line, kind, tuple, signature \\ [], doc)
|
||||
|
||||
def add_doc(_module, _line, kind, _tuple, _signature, doc) when kind in [:defp, :defmacrop, :typep] do
|
||||
if doc, do: {:error, :private_doc}, else: :ok
|
||||
end
|
||||
|
||||
def add_doc(module, line, kind, tuple, signature, doc) when
|
||||
kind in [:def, :defmacro, :type, :opaque] and (is_binary(doc) or is_boolean(doc) or doc == nil) do
|
||||
assert_not_compiled!(:add_doc, module)
|
||||
table = data_table_for(module)
|
||||
|
||||
{signature, _} = :lists.mapfoldl fn(x, acc) ->
|
||||
{simplify_signature(x, acc), acc + 1}
|
||||
end, 1, signature
|
||||
|
||||
case :ets.lookup(table, {:doc, tuple}) do
|
||||
[] ->
|
||||
:ets.insert(table, {{:doc, tuple}, line, kind, signature, doc})
|
||||
:ok
|
||||
[{doc_tuple, line, _old_kind, old_sign, old_doc}] ->
|
||||
:ets.insert(table, {
|
||||
doc_tuple,
|
||||
line,
|
||||
kind,
|
||||
merge_signatures(old_sign, signature, 1),
|
||||
if(is_nil(doc), do: old_doc, else: doc)
|
||||
})
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Simplify signatures to be stored in docs
|
||||
|
||||
defp simplify_signature({:\\, _, [left, right ]}, i) do
|
||||
{:\\, [], [simplify_signature(left, i), right]}
|
||||
end
|
||||
|
||||
defp simplify_signature({:%, _, [left, _]}, _i) when is_atom(left) do
|
||||
last = List.last(String.split(Atom.to_string(left), "."))
|
||||
atom = String.to_atom(downcase(last))
|
||||
{atom, [], nil}
|
||||
end
|
||||
|
||||
defp simplify_signature({:=, _, [_, right]}, i) do
|
||||
simplify_signature(right, i)
|
||||
end
|
||||
|
||||
defp simplify_signature({var, _, atom}, _i) when is_atom(atom) do
|
||||
case Atom.to_string(var) do
|
||||
"_" <> rest -> {String.to_atom(rest), [], Elixir}
|
||||
_ -> {var, [], nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp simplify_signature(other, i) when is_integer(other), do: {:"int#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_boolean(other), do: {:"bool#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_atom(other), do: {:"atom#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_list(other), do: {:"list#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_float(other), do: {:"float#{i}", [], Elixir}
|
||||
defp simplify_signature(other, i) when is_binary(other), do: {:"binary#{i}", [], Elixir}
|
||||
defp simplify_signature(_, i), do: {:"arg#{i}", [], Elixir}
|
||||
|
||||
defp downcase(<<c :: utf8, rest :: binary>>) when c >= ?A and c <= ?Z do
|
||||
<<c + 32 :: utf8, downcase(rest) :: binary>>
|
||||
end
|
||||
|
||||
defp downcase(<<c, rest :: binary>>) do
|
||||
<<c, downcase(rest) :: binary>>
|
||||
end
|
||||
|
||||
defp downcase(<<>>) do
|
||||
<<>>
|
||||
end
|
||||
|
||||
# Merge
|
||||
|
||||
defp merge_signatures([h1|t1], [h2|t2], i) do
|
||||
[merge_signature(h1, h2, i)|merge_signatures(t1, t2, i + 1)]
|
||||
end
|
||||
|
||||
defp merge_signatures([], [], _) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp merge_signature({:\\, line, [left, right]}, newer, i) do
|
||||
{:\\, line, [merge_signature(left, newer, i), right]}
|
||||
end
|
||||
|
||||
defp merge_signature(older, {:\\, _, [left, _]}, i) do
|
||||
merge_signature(older, left, i)
|
||||
end
|
||||
|
||||
# The older signature, when given, always have higher precedence
|
||||
defp merge_signature({_, _, nil} = older, _newer, _), do: older
|
||||
defp merge_signature(_older, {_, _, nil} = newer, _), do: newer
|
||||
|
||||
# Both are a guess, so check if they are the same guess
|
||||
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
|
||||
|
||||
# Otherwise, returns a generic guess
|
||||
defp merge_signature({_, line, _}, _newer, i), do: {:"arg#{i}", line, Elixir}
|
||||
|
||||
@doc """
|
||||
Checks if the module defines the given function or macro.
|
||||
Use `defines?/3` to assert for a specific type.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
Module.defines? __MODULE__, {:version, 0} #=> false
|
||||
def version, do: 1
|
||||
Module.defines? __MODULE__, {:version, 0} #=> true
|
||||
end
|
||||
|
||||
"""
|
||||
def defines?(module, tuple) when is_tuple(tuple) do
|
||||
assert_not_compiled!(:defines?, module)
|
||||
table = defs_table_for(module)
|
||||
:ets.lookup(table, tuple) != []
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the module defines a function or macro of the
|
||||
given `kind`. `kind` can be any of `:def`, `:defp`,
|
||||
`:defmacro` or `:defmacrop`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
Module.defines? __MODULE__, {:version, 0}, :defp #=> false
|
||||
def version, do: 1
|
||||
Module.defines? __MODULE__, {:version, 0}, :defp #=> false
|
||||
end
|
||||
|
||||
"""
|
||||
def defines?(module, tuple, kind) do
|
||||
assert_not_compiled!(:defines?, module)
|
||||
table = defs_table_for(module)
|
||||
case :ets.lookup(table, tuple) do
|
||||
[{_, ^kind, _, _, _, _, _}] -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return all functions defined in `module`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
def version, do: 1
|
||||
Module.definitions_in __MODULE__ #=> [{:version,0}]
|
||||
end
|
||||
|
||||
"""
|
||||
def definitions_in(module) do
|
||||
assert_not_compiled!(:definitions_in, module)
|
||||
table = defs_table_for(module)
|
||||
:lists.concat :ets.match(table, {:'$1', :_, :_, :_, :_, :_, :_})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all functions defined in `module`, according
|
||||
to its kind.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
def version, do: 1
|
||||
Module.definitions_in __MODULE__, :def #=> [{:version,0}]
|
||||
Module.definitions_in __MODULE__, :defp #=> []
|
||||
end
|
||||
|
||||
"""
|
||||
def definitions_in(module, kind) do
|
||||
assert_not_compiled!(:definitions_in, module)
|
||||
table = defs_table_for(module)
|
||||
:lists.concat :ets.match(table, {:'$1', kind, :_, :_, :_, :_, :_})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Makes the given functions in `module` overridable.
|
||||
An overridable function is lazily defined, allowing a
|
||||
developer to customize it. See `Kernel.defoverridable/1` for
|
||||
more information and documentation.
|
||||
"""
|
||||
def make_overridable(module, tuples) do
|
||||
assert_not_compiled!(:make_overridable, module)
|
||||
|
||||
for tuple <- tuples do
|
||||
case :elixir_def.lookup_definition(module, tuple) do
|
||||
false ->
|
||||
{name, arity} = tuple
|
||||
raise "Cannot make function #{name}/#{arity} overridable because it was not defined"
|
||||
clause ->
|
||||
:elixir_def.delete_definition(module, tuple)
|
||||
|
||||
neighbours = if loaded?(Module.LocalsTracker) do
|
||||
Module.LocalsTracker.yank(module, tuple)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
old = :elixir_def_overridable.overridable(module)
|
||||
merged = :orddict.update(tuple, fn({count, _, _, _}) ->
|
||||
{count + 1, clause, neighbours, false}
|
||||
end, {1, clause, neighbours, false}, old)
|
||||
:elixir_def_overridable.overridable(module, merged)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if `tuple` in `module` is marked as overridable.
|
||||
"""
|
||||
def overridable?(module, tuple) do
|
||||
!!List.keyfind(:elixir_def_overridable.overridable(module), tuple, 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts an Erlang attribute to the given module with the given
|
||||
key and value. The semantics of putting the attribute depends
|
||||
if the attribute was registered or not via `register_attribute/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.put_attribute __MODULE__, :custom_threshold_for_lib, 10
|
||||
end
|
||||
|
||||
"""
|
||||
def put_attribute(module, key, value) when is_atom(key) do
|
||||
assert_not_compiled!(:put_attribute, module)
|
||||
table = data_table_for(module)
|
||||
value = normalize_attribute(key, value)
|
||||
acc = :ets.lookup_element(table, {:elixir, :acc_attributes}, 2)
|
||||
|
||||
new =
|
||||
if :lists.member(key, acc) do
|
||||
case :ets.lookup(table, key) do
|
||||
[{^key, old}] -> [value|old]
|
||||
[] -> [value]
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
|
||||
:ets.insert(table, {key, new})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the given attribute from a module. If the attribute
|
||||
was marked with `accumulate` with `Module.register_attribute/3`,
|
||||
a list is always returned.
|
||||
|
||||
The `@` macro compiles to a call to this function. For example,
|
||||
the following code:
|
||||
|
||||
@foo
|
||||
|
||||
Expands close to:
|
||||
|
||||
Module.get_attribute(__MODULE__, :foo)
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
Module.put_attribute __MODULE__, :value, 1
|
||||
Module.get_attribute __MODULE__, :value #=> 1
|
||||
|
||||
Module.register_attribute __MODULE__, :value, accumulate: true
|
||||
Module.put_attribute __MODULE__, :value, 1
|
||||
Module.get_attribute __MODULE__, :value #=> [1]
|
||||
end
|
||||
|
||||
"""
|
||||
@spec get_attribute(atom, atom) :: term
|
||||
def get_attribute(module, key) do
|
||||
get_attribute(module, key, nil)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all attributes that match the given key.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.put_attribute __MODULE__, :custom_threshold_for_lib, 10
|
||||
Module.delete_attribute __MODULE__, :custom_threshold_for_lib
|
||||
end
|
||||
|
||||
"""
|
||||
@spec delete_attribute(atom, atom) :: :ok
|
||||
def delete_attribute(module, key) when is_atom(key) do
|
||||
assert_not_compiled!(:delete_attribute, module)
|
||||
table = data_table_for(module)
|
||||
:ets.delete(table, key)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers an attribute. By registering an attribute, a developer
|
||||
is able to customize how Elixir will store and accumulate the
|
||||
attribute values.
|
||||
|
||||
## Options
|
||||
|
||||
When registering an attribute, two options can be given:
|
||||
|
||||
* `:accumulate` - several calls to the same attribute will
|
||||
accumulate instead of override the previous one. New attributes
|
||||
are always added to the top of the accumulated list.
|
||||
|
||||
* `:persist` - the attribute will be persisted in the Erlang
|
||||
Abstract Format. Useful when interfacing with Erlang libraries.
|
||||
|
||||
By default, both options are `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.register_attribute __MODULE__,
|
||||
:custom_threshold_for_lib,
|
||||
accumulate: true, persist: false
|
||||
|
||||
@custom_threshold_for_lib 10
|
||||
@custom_threshold_for_lib 20
|
||||
@custom_threshold_for_lib #=> [20, 10]
|
||||
end
|
||||
|
||||
"""
|
||||
def register_attribute(module, new, opts) when is_atom(new) do
|
||||
assert_not_compiled!(:register_attribute, module)
|
||||
table = data_table_for(module)
|
||||
|
||||
if Keyword.get(opts, :persist) do
|
||||
old = :ets.lookup_element(table, {:elixir, :persisted_attributes}, 2)
|
||||
:ets.insert(table, {{:elixir, :persisted_attributes}, [new|old]})
|
||||
end
|
||||
|
||||
if Keyword.get(opts, :accumulate) do
|
||||
old = :ets.lookup_element(table, {:elixir, :acc_attributes}, 2)
|
||||
:ets.insert(table, {{:elixir, :acc_attributes}, [new|old]})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Split the given module name into binary parts.
|
||||
|
||||
## Examples
|
||||
|
||||
Module.split Very.Long.Module.Name.And.Even.Longer
|
||||
#=> ["Very", "Long", "Module", "Name", "And", "Even", "Longer"]
|
||||
|
||||
"""
|
||||
def split(module) do
|
||||
tl(String.split(String.Chars.to_string(module), "."))
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Used internally to compile documentation. This function
|
||||
# is private and must be used only internally.
|
||||
def compile_doc(env, kind, name, args, _guards, _body) do
|
||||
module = env.module
|
||||
line = env.line
|
||||
arity = length(args)
|
||||
pair = {name, arity}
|
||||
doc = get_attribute(module, :doc)
|
||||
|
||||
case add_doc(module, line, kind, pair, args, doc) do
|
||||
:ok ->
|
||||
:ok
|
||||
{:error, :private_doc} ->
|
||||
:elixir_errors.warn line, env.file,
|
||||
"function #{name}/#{arity} is private, " <>
|
||||
"@doc's are always discarded for private functions"
|
||||
end
|
||||
|
||||
delete_attribute(module, :doc)
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Used internally to compile types. This function
|
||||
# is private and must be used only internally.
|
||||
def store_typespec(module, key, value) when is_atom(key) do
|
||||
assert_not_compiled!(:put_attribute, module)
|
||||
table = data_table_for(module)
|
||||
|
||||
new =
|
||||
case :ets.lookup(table, key) do
|
||||
[{^key, old}] -> [value|old]
|
||||
[] -> [value]
|
||||
end
|
||||
|
||||
:ets.insert(table, {key, new})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def get_attribute(module, key, warn) when is_atom(key) and (is_list(warn) or is_nil(warn)) do
|
||||
assert_not_compiled!(:get_attribute, module)
|
||||
table = data_table_for(module)
|
||||
|
||||
case :ets.lookup(table, key) do
|
||||
[{^key, val}] -> val
|
||||
[] ->
|
||||
acc = :ets.lookup_element(table, {:elixir, :acc_attributes}, 2)
|
||||
|
||||
cond do
|
||||
:lists.member(key, acc) ->
|
||||
[]
|
||||
is_list(warn) ->
|
||||
:elixir_errors.warn warn_info(warn), "undefined module attribute @#{key}, " <>
|
||||
"please remove access to @#{key} or explicitly set it to nil before access"
|
||||
nil
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp warn_info([entry|_]) do
|
||||
opts = elem(entry, tuple_size(entry) - 1)
|
||||
Exception.format_file_line(Keyword.get(opts, :file), Keyword.get(opts, :line)) <> " "
|
||||
end
|
||||
|
||||
defp warn_info([]) do
|
||||
""
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp normalize_attribute(:on_load, atom) when is_atom(atom) do
|
||||
{atom, 0}
|
||||
end
|
||||
|
||||
defp normalize_attribute(:behaviour, atom) when is_atom(atom) do
|
||||
# Attempt to compile behaviour but ignore failure (will warn later)
|
||||
_ = Code.ensure_compiled(atom)
|
||||
atom
|
||||
end
|
||||
|
||||
defp normalize_attribute(:file, file) when is_binary(file) do
|
||||
file
|
||||
end
|
||||
|
||||
defp normalize_attribute(key, atom) when is_atom(atom) and
|
||||
key in [:before_compile, :after_compile, :on_definition] do
|
||||
{atom, :"__#{key}__"}
|
||||
end
|
||||
|
||||
defp normalize_attribute(key, _value) when key in [:type, :typep, :export_type, :opaque, :callback] do
|
||||
raise ArgumentError, "attributes type, typep, export_type, opaque and callback " <>
|
||||
"must be set via Kernel.Typespec"
|
||||
end
|
||||
|
||||
defp normalize_attribute(_key, value) do
|
||||
value
|
||||
end
|
||||
|
||||
defp data_table_for(module) do
|
||||
:elixir_module.data_table(module)
|
||||
end
|
||||
|
||||
defp defs_table_for(module) do
|
||||
:elixir_module.defs_table(module)
|
||||
end
|
||||
|
||||
defp assert_not_compiled!(fun, module) do
|
||||
open?(module) ||
|
||||
raise ArgumentError,
|
||||
"could not call #{fun} on module #{inspect module} because it was already compiled"
|
||||
end
|
||||
|
||||
defp loaded?(module), do: is_tuple :code.is_loaded(module)
|
||||
end
|
||||
@@ -1,368 +0,0 @@
|
||||
# This is a module Elixir responsible for tracking
|
||||
# calls in order to extract Elixir modules' behaviour
|
||||
# during compilation time.
|
||||
#
|
||||
# ## Implementation
|
||||
#
|
||||
# The implementation uses the digraph module to track
|
||||
# all dependencies. The graph starts with one main vertice:
|
||||
#
|
||||
# * `:local` - points to local functions
|
||||
#
|
||||
# We also have can the following vertices:
|
||||
#
|
||||
# * `Module` - a module that was invoked via an import
|
||||
# * `{name, arity}` - a local function/arity pair
|
||||
# * `{:import, name, arity}` - an invoked function/arity import
|
||||
#
|
||||
# Each of those vertices can associate to other vertices
|
||||
# as described below:
|
||||
#
|
||||
# * `Module`
|
||||
# * in neighbours: `{:import, name, arity}`
|
||||
#
|
||||
# * `{name, arity}`
|
||||
# * in neighbours: `:local`, `{name, arity}`
|
||||
# * out neighbours: `{:import, name, arity}`
|
||||
#
|
||||
# * `{:import, name, arity}`
|
||||
# * in neighbours: `{name, arity}`
|
||||
# * out neighbours: `Module`
|
||||
#
|
||||
# Note that since this is required for bootstrap, we can't use
|
||||
# any of the `GenServer.Behaviour` conveniences.
|
||||
defmodule Module.LocalsTracker do
|
||||
@moduledoc false
|
||||
|
||||
@timeout 30_000
|
||||
@behaviour :gen_server
|
||||
|
||||
@type ref :: pid | module
|
||||
@type name :: atom
|
||||
@type name_arity :: {name, arity}
|
||||
|
||||
@type local :: {name, arity}
|
||||
@type import :: {:import, name, arity}
|
||||
|
||||
# Public API
|
||||
|
||||
@doc """
|
||||
Returns all imported modules that had the given
|
||||
`{name, arity}` invoked.
|
||||
"""
|
||||
@spec imports_with_dispatch(ref, name_arity) :: [module]
|
||||
def imports_with_dispatch(ref, {name, arity}) do
|
||||
d = :gen_server.call(to_pid(ref), :digraph, @timeout)
|
||||
:digraph.out_neighbours(d, {:import, name, arity})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all locals that are reachable.
|
||||
|
||||
By default, all public functions are reachable.
|
||||
A private function is only reachable if it has
|
||||
a public function that it invokes directly.
|
||||
"""
|
||||
@spec reachable(ref) :: [local]
|
||||
def reachable(ref) do
|
||||
reachable_from(:gen_server.call(to_pid(ref), :digraph, @timeout), :local)
|
||||
end
|
||||
|
||||
defp reachable_from(d, starting) do
|
||||
:sets.to_list(reduce_reachable(d, starting, :sets.new))
|
||||
end
|
||||
|
||||
defp reduce_reachable(d, vertex, vertices) do
|
||||
neighbours = :digraph.out_neighbours(d, vertex)
|
||||
neighbours = (for {_, _} = t <- neighbours, do: t) |> :sets.from_list
|
||||
remaining = :sets.subtract(neighbours, vertices)
|
||||
vertices = :sets.union(neighbours, vertices)
|
||||
:sets.fold(&reduce_reachable(d, &1, &2), vertices, remaining)
|
||||
end
|
||||
|
||||
defp to_pid(pid) when is_pid(pid), do: pid
|
||||
defp to_pid(mod) when is_atom(mod) do
|
||||
table = :elixir_module.data_table(mod)
|
||||
[{_, val}] = :ets.lookup(table, {:elixir, :locals_tracker})
|
||||
val
|
||||
end
|
||||
|
||||
# Internal API
|
||||
|
||||
# Starts the tracker and returns its pid.
|
||||
@doc false
|
||||
def start_link do
|
||||
:gen_server.start_link(__MODULE__, [], [])
|
||||
end
|
||||
|
||||
# Adds a definition into the tracker. A public
|
||||
# definition is connected with the :local node
|
||||
# while a private one is left unreachable until
|
||||
# a call is made to.
|
||||
@doc false
|
||||
def add_definition(pid, kind, tuple) when kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
:gen_server.cast(pid, {:add_definition, kind, tuple})
|
||||
end
|
||||
|
||||
# Adds and tracks defaults for a definition into the tracker.
|
||||
@doc false
|
||||
def add_defaults(pid, kind, tuple, defaults) when kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
:gen_server.cast(pid, {:add_defaults, kind, tuple, defaults})
|
||||
end
|
||||
|
||||
# Adds a local dispatch to the given target.
|
||||
def add_local(pid, to) when is_tuple(to) do
|
||||
:gen_server.cast(pid, {:add_local, :local, to})
|
||||
end
|
||||
|
||||
# Adds a local dispatch from-to the given target.
|
||||
@doc false
|
||||
def add_local(pid, from, to) when is_tuple(from) and is_tuple(to) do
|
||||
:gen_server.cast(pid, {:add_local, from, to})
|
||||
end
|
||||
|
||||
# Adds a import dispatch to the given target.
|
||||
@doc false
|
||||
def add_import(pid, function, module, target) when is_atom(module) and is_tuple(target) do
|
||||
:gen_server.cast(pid, {:add_import, function, module, target})
|
||||
end
|
||||
|
||||
# Yanks a local node. Returns its in and out vertices in a tuple.
|
||||
@doc false
|
||||
def yank(pid, local) do
|
||||
:gen_server.call(to_pid(pid), {:yank, local}, @timeout)
|
||||
end
|
||||
|
||||
# Reattach a previously yanked node
|
||||
@doc false
|
||||
def reattach(pid, kind, tuple, neighbours) do
|
||||
:gen_server.cast(to_pid(pid), {:reattach, kind, tuple, neighbours})
|
||||
end
|
||||
|
||||
# Collecting all conflicting imports with the given functions
|
||||
@doc false
|
||||
def collect_imports_conflicts(pid, all_defined) do
|
||||
d = :gen_server.call(pid, :digraph, @timeout)
|
||||
|
||||
for {name, arity} <- all_defined,
|
||||
:digraph.in_neighbours(d, {:import, name, arity}) != [],
|
||||
n = :digraph.out_neighbours(d, {:import, name, arity}),
|
||||
n != [] do
|
||||
{n, name, arity}
|
||||
end
|
||||
end
|
||||
|
||||
# Collect all unused definitions based on the private
|
||||
# given also accounting the expected amount of default
|
||||
# clauses a private function have.
|
||||
@doc false
|
||||
def collect_unused_locals(ref, private) do
|
||||
d = :gen_server.call(to_pid(ref), :digraph, @timeout)
|
||||
{unreachable(d, private), collect_warnings(d, private)}
|
||||
end
|
||||
|
||||
defp unreachable(d, private) do
|
||||
unreachable = for {tuple, _, _} <- private, do: tuple
|
||||
|
||||
private =
|
||||
for {tuple, :defp, _} <- private do
|
||||
neighbours = :digraph.in_neighbours(d, tuple)
|
||||
neighbours = for {_, _} = t <- neighbours, do: t
|
||||
{tuple, :sets.from_list(neighbours)}
|
||||
end
|
||||
|
||||
reduce_unreachable(private, [], :sets.from_list(unreachable))
|
||||
end
|
||||
|
||||
defp reduce_unreachable([{vertex, callers}|t], acc, unreachable) do
|
||||
if :sets.is_subset(callers, unreachable) do
|
||||
reduce_unreachable(t, [{vertex, callers}|acc], unreachable)
|
||||
else
|
||||
reduce_unreachable(acc ++ t, [], :sets.del_element(vertex, unreachable))
|
||||
end
|
||||
end
|
||||
|
||||
defp reduce_unreachable([], _acc, unreachable) do
|
||||
:sets.to_list(unreachable)
|
||||
end
|
||||
|
||||
defp collect_warnings(d, private) do
|
||||
reachable = reachable_from(d, :local)
|
||||
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
|
||||
end
|
||||
|
||||
defp collect_warnings({tuple, kind, 0}, acc, reachable) do
|
||||
if :lists.member(tuple, reachable) do
|
||||
acc
|
||||
else
|
||||
[{:unused_def, tuple, kind}|acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_warnings({tuple, kind, default}, acc, reachable) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
|
||||
invoked = for {n, a} <- reachable, n == name, a in min..max, do: a
|
||||
|
||||
if invoked == [] do
|
||||
[{:unused_def, tuple, kind}|acc]
|
||||
else
|
||||
case :lists.min(invoked) - min do
|
||||
0 -> acc
|
||||
^default -> [{:unused_args, tuple}|acc]
|
||||
unused_args -> [{:unused_args, tuple, unused_args}|acc]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def cache_env(pid, env) do
|
||||
:gen_server.call(pid, {:cache_env, env}, @timeout)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def get_cached_env(pid, ref) do
|
||||
:gen_server.call(pid, {:get_cached_env, ref}, @timeout)
|
||||
end
|
||||
|
||||
# Stops the gen server
|
||||
@doc false
|
||||
def stop(pid) do
|
||||
:gen_server.cast(pid, :stop)
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
def init([]) do
|
||||
d = :digraph.new([:protected])
|
||||
:digraph.add_vertex(d, :local)
|
||||
{:ok, {d, []}}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call({:cache_env, env}, _from, {d, cache}) do
|
||||
case cache do
|
||||
[{i,^env}|_] ->
|
||||
{:reply, i, {d, cache}}
|
||||
t ->
|
||||
i = length(t)
|
||||
{:reply, i, {d, [{i,env}|t]}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:get_cached_env, ref}, _from, {_, cache} = state) do
|
||||
{^ref, env} = :lists.keyfind(ref, 1, cache)
|
||||
{:reply, env, state}
|
||||
end
|
||||
|
||||
def handle_call({:yank, local}, _from, {d, _} = state) do
|
||||
out_vertices = :digraph.out_neighbours(d, local)
|
||||
:digraph.del_edges(d, :digraph.out_edges(d, local))
|
||||
{:reply, {[], out_vertices}, state}
|
||||
end
|
||||
|
||||
def handle_call(:digraph, _from, {d, _} = state) do
|
||||
{:reply, d, state}
|
||||
end
|
||||
|
||||
def handle_call(request, _from, state) do
|
||||
{:stop, {:bad_call, request}, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_local, from, to}, {d, _} = state) do
|
||||
handle_add_local(d, from, to)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_import, function, module, {name, arity}}, {d, _} = state) do
|
||||
handle_import(d, function, module, name, arity)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_definition, kind, tuple}, {d, _} = state) do
|
||||
handle_add_definition(d, kind, tuple)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_defaults, kind, {name, arity}, defaults}, {d, _} = state) do
|
||||
for i <- :lists.seq(arity - defaults, arity - 1) do
|
||||
handle_add_definition(d, kind, {name, i})
|
||||
handle_add_local(d, {name, i}, {name, i + 1})
|
||||
end
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:reattach, _kind, tuple, {in_neigh, out_neigh}}, {d, _} = state) do
|
||||
for from <- in_neigh do
|
||||
:digraph.add_vertex(d, from)
|
||||
replace_edge!(d, from, tuple)
|
||||
end
|
||||
|
||||
for to <- out_neigh do
|
||||
:digraph.add_vertex(d, to)
|
||||
replace_edge!(d, tuple, to)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast(:stop, state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_cast(msg, state) do
|
||||
{:stop, {:bad_cast, msg}, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def terminate(_reason, _state) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc false
|
||||
def code_change(_old, state, _extra) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defp handle_import(d, function, module, name, arity) do
|
||||
:digraph.add_vertex(d, module)
|
||||
|
||||
tuple = {:import, name, arity}
|
||||
:digraph.add_vertex(d, tuple)
|
||||
replace_edge!(d, tuple, module)
|
||||
|
||||
if function != nil do
|
||||
replace_edge!(d, function, tuple)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp handle_add_local(d, from, to) do
|
||||
:digraph.add_vertex(d, to)
|
||||
replace_edge!(d, from, to)
|
||||
end
|
||||
|
||||
defp handle_add_definition(d, public, tuple) when public in [:def, :defmacro] do
|
||||
:digraph.add_vertex(d, tuple)
|
||||
replace_edge!(d, :local, tuple)
|
||||
end
|
||||
|
||||
defp handle_add_definition(d, private, tuple) when private in [:defp, :defmacrop] do
|
||||
:digraph.add_vertex(d, tuple)
|
||||
end
|
||||
|
||||
defp replace_edge!(d, from, to) do
|
||||
_ = unless :lists.member(to, :digraph.out_neighbours(d, from)) do
|
||||
[:"$e"|_] = :digraph.add_edge(d, from, to)
|
||||
end
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -1,259 +0,0 @@
|
||||
defmodule Node do
|
||||
@moduledoc """
|
||||
Functions related to VM nodes.
|
||||
|
||||
Some of the functions in this module are inlined by the compiler,
|
||||
similar to functions in the `Kernel` module and they are explicitly
|
||||
marked in their docs as "inlined by the compiler". For more information
|
||||
about inlined functions, check out the `Kernel` module.
|
||||
"""
|
||||
|
||||
@type t :: node
|
||||
|
||||
@doc """
|
||||
Turns a non-distributed node into a distributed node.
|
||||
|
||||
This functionality starts the `:net_kernel` and other
|
||||
related processes.
|
||||
"""
|
||||
@spec start(node, :longnames | :shortnames, non_neg_integer) ::
|
||||
{:ok, pid} | {:error, term}
|
||||
def start(name, type \\ :longnames, tick_time \\ 15000) do
|
||||
:net_kernel.start([name, type, tick_time])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Turns a distributed node into a non-distributed node.
|
||||
|
||||
For other nodes in the network, this is the same as the node going down.
|
||||
Only possible when the node was started with `Node.start/3`, otherwise
|
||||
returns `{:error, :not_allowed}`. Returns `{:error, :not_found}` if the
|
||||
local node is not alive.
|
||||
"""
|
||||
@spec stop() :: :ok | {:error, :not_allowed | :not_found}
|
||||
def stop() do
|
||||
:net_kernel.stop()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current node.
|
||||
|
||||
It returns the same as the built-in `node()`.
|
||||
"""
|
||||
@spec self :: t
|
||||
def self do
|
||||
:erlang.node()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if the local node is alive.
|
||||
|
||||
That is, if the node can be part of a distributed system.
|
||||
"""
|
||||
@spec alive? :: boolean
|
||||
def alive? do
|
||||
:erlang.is_alive()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all visible nodes in the system, excluding
|
||||
the local node.
|
||||
|
||||
Same as `list(:visible)`.
|
||||
"""
|
||||
@spec list :: [t]
|
||||
def list do
|
||||
:erlang.nodes()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of nodes according to argument given.
|
||||
|
||||
The result returned when the argument is a list, is the list of nodes
|
||||
satisfying the disjunction(s) of the list elements.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#nodes-1 for more info.
|
||||
"""
|
||||
@typep state :: :visible | :hidden | :connected | :this | :known
|
||||
@spec list(state | [state]) :: [t]
|
||||
def list(args) do
|
||||
:erlang.nodes(args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Monitors the status of the node.
|
||||
|
||||
If `flag` is `true`, monitoring is turned on.
|
||||
If `flag` is `false`, monitoring is turned off.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor_node-2 for more info.
|
||||
"""
|
||||
@spec monitor(t, boolean) :: true
|
||||
def monitor(node, flag) do
|
||||
:erlang.monitor_node(node, flag)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Behaves as `monitor/2` except that it allows an extra
|
||||
option to be given, namely `:allow_passive_connect`.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor_node-3 for more info.
|
||||
"""
|
||||
@spec monitor(t, boolean, [:allow_passive_connect]) :: true
|
||||
def monitor(node, flag, options) do
|
||||
:erlang.monitor_node(node, flag, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Tries to set up a connection to node.
|
||||
|
||||
Returns `:pang` if it fails, or `:pong` if it is successful.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Node.ping(:unknown_node)
|
||||
:pang
|
||||
|
||||
"""
|
||||
@spec ping(t) :: :pong | :pang
|
||||
def ping(node) do
|
||||
:net_adm.ping(node)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Forces the disconnection of a node.
|
||||
|
||||
This will appear to the `node` as if the local node has crashed.
|
||||
This function is mainly used in the Erlang network authentication
|
||||
protocols. Returns `true` if disconnection succeeds, otherwise `false`.
|
||||
If the local node is not alive, the function returns `:ignored`.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#disconnect_node-1 for more info.
|
||||
"""
|
||||
@spec disconnect(t) :: boolean | :ignored
|
||||
def disconnect(node) do
|
||||
:erlang.disconnect_node(node)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Establishes a connection to `node`.
|
||||
|
||||
Returns `true` if successful, `false` if not, and the atom
|
||||
`:ignored` if the local node is not alive.
|
||||
|
||||
See http://erlang.org/doc/man/net_kernel.html#connect_node-1 for more info.
|
||||
"""
|
||||
@spec connect(t) :: boolean | :ignored
|
||||
def connect(node) do
|
||||
:net_kernel.connect_node(node)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`
|
||||
on `node`. If `node` does not exist, a useless pid is returned.
|
||||
|
||||
Check http://www.erlang.org/doc/man/erlang.html#spawn-2 for
|
||||
the list of available options.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(t, (() -> any)) :: pid
|
||||
def spawn(node, fun) do
|
||||
:erlang.spawn(node, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`
|
||||
on `node`.
|
||||
|
||||
If `node` does not exist, a useless pid is returned. Check
|
||||
http://www.erlang.org/doc/man/erlang.html#spawn_opt-3 for the list of
|
||||
available options.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(t, (() -> any), Process.spawn_opts) :: pid | {pid, reference}
|
||||
def spawn(node, fun, opts) do
|
||||
:erlang.spawn_opt(node, fun, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of
|
||||
`module.function(args)` on `node`.
|
||||
|
||||
If `node` does not exist, a useless pid is returned. Check
|
||||
http://www.erlang.org/doc/man/erlang.html#spawn-4 for the list of
|
||||
available options.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(t, module, atom, [any]) :: pid
|
||||
def spawn(node, module, fun, args) do
|
||||
:erlang.spawn(node, module, fun, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of
|
||||
`module.function(args)` on `node`.
|
||||
|
||||
If `node` does not exist, a useless pid is returned. Check
|
||||
http://www.erlang.org/doc/man/erlang.html#spawn_opt-5 for the list of
|
||||
available options.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(t, module, atom, [any], Process.spawn_opts) :: pid | {pid, reference}
|
||||
def spawn(node, module, fun, args, opts) do
|
||||
:erlang.spawn_opt(node, module, fun, args, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new linked process started by the application of `fun` on `node`.
|
||||
|
||||
A link is created between the calling process and the new process, atomically.
|
||||
If `node` does not exist, a useless pid is returned (and due to the link, an exit
|
||||
signal with exit reason `:noconnection` will be received).
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn_link(t, (() -> any)) :: pid
|
||||
def spawn_link(node, fun) do
|
||||
:erlang.spawn_link(node, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new linked process started by the application of
|
||||
`module.function(args)` on `node`.
|
||||
|
||||
A link is created between the calling process and the new process, atomically.
|
||||
If `node` does not exist, a useless pid is returned (and due to the link, an exit
|
||||
signal with exit reason `:noconnection` will be received).
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn_link(t, module, atom, [any]) :: pid
|
||||
def spawn_link(node, module, fun, args) do
|
||||
:erlang.spawn_link(node, module, fun, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the magic cookie of `node` to the atom `cookie`.
|
||||
|
||||
The default node is `Node.self`, the local node. If `node` is the local node,
|
||||
the function also sets the cookie of all other unknown nodes to `cookie`.
|
||||
|
||||
This function will raise `FunctionClauseError` if the given `node` is not alive.
|
||||
"""
|
||||
def set_cookie(node \\ Node.self, cookie) when is_atom(cookie) do
|
||||
:erlang.set_cookie(node, cookie)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the magic cookie of the local node.
|
||||
|
||||
Returns the cookie if the node is alive, otherwise `:nocookie`.
|
||||
"""
|
||||
def get_cookie() do
|
||||
:erlang.get_cookie()
|
||||
end
|
||||
end
|
||||
@@ -1,486 +0,0 @@
|
||||
defmodule OptionParser do
|
||||
@moduledoc """
|
||||
This module contains functions to parse command line arguments.
|
||||
"""
|
||||
|
||||
@type argv :: [String.t]
|
||||
@type parsed :: Keyword.t
|
||||
@type errors :: [{String.t, String.t | nil}]
|
||||
@type options :: [switches: Keyword.t, strict: Keyword.t, aliases: Keyword.t]
|
||||
|
||||
@doc """
|
||||
Parses `argv` into a keywords list.
|
||||
|
||||
It returns the parsed values, remaining arguments and the
|
||||
invalid options.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> OptionParser.parse(["--debug"])
|
||||
{[debug: true], [], []}
|
||||
|
||||
iex> OptionParser.parse(["--source", "lib"])
|
||||
{[source: "lib"], [], []}
|
||||
|
||||
iex> OptionParser.parse(["--source-path", "lib", "test/enum_test.exs", "--verbose"])
|
||||
{[source_path: "lib", verbose: true], ["test/enum_test.exs"], []}
|
||||
|
||||
By default, Elixir will try to automatically parse switches.
|
||||
Switches without an argument, like `--debug` will automatically
|
||||
be set to true. Switches followed by a value will be assigned
|
||||
to the value, always as strings.
|
||||
|
||||
Note Elixir also converts the switches to underscore atoms, as
|
||||
`--source-path` becomes `:source_path`, to better suit Elixir
|
||||
conventions. This means that option names on the command line cannot contain
|
||||
underscores; such options will be reported as `:undefined` (in strict mode)
|
||||
or `:invalid` (in basic mode).
|
||||
|
||||
## Switches
|
||||
|
||||
Many times though, it is better to explicitly list the available
|
||||
switches and their formats. The switches can be specified via two
|
||||
different options:
|
||||
|
||||
* `:strict` - the switches are strict. Any switch that does not
|
||||
exist in the switch list is treated as an error.
|
||||
|
||||
* `:switches` - defines some switches. Switches that does not
|
||||
exist in the switch list are still attempted to be parsed.
|
||||
|
||||
Note only `:strict` or `:switches` may be given at once.
|
||||
|
||||
For each switch, the following types are supported:
|
||||
|
||||
* `:boolean` - marks the given switch as a boolean. Boolean switches
|
||||
never consume the following value unless it is `true` or
|
||||
`false`.
|
||||
* `:integer` - parses the switch as an integer.
|
||||
* `:float` - parses the switch as a float.
|
||||
* `:string` - returns the switch as a string.
|
||||
|
||||
If a switch can't be parsed or is not specified in the strict case,
|
||||
the option is returned in the invalid options list (third element
|
||||
of the returned tuple).
|
||||
|
||||
The following extra "types" are supported:
|
||||
|
||||
* `:keep` - keeps duplicated items in the list instead of overriding
|
||||
|
||||
Examples:
|
||||
|
||||
iex> OptionParser.parse(["--unlock", "path/to/file"], strict: [unlock: :boolean])
|
||||
{[unlock: true], ["path/to/file"], []}
|
||||
|
||||
iex> OptionParser.parse(["--unlock", "--limit", "0", "path/to/file"],
|
||||
...> strict: [unlock: :boolean, limit: :integer])
|
||||
{[unlock: true, limit: 0], ["path/to/file"], []}
|
||||
|
||||
iex> OptionParser.parse(["--limit", "3"], strict: [limit: :integer])
|
||||
{[limit: 3], [], []}
|
||||
|
||||
iex> OptionParser.parse(["--limit", "xyz"], strict: [limit: :integer])
|
||||
{[], [], [{"--limit", "xyz"}]}
|
||||
|
||||
iex> OptionParser.parse(["--unknown", "xyz"], strict: [])
|
||||
{[], ["xyz"], [{"--unknown", nil}]}
|
||||
|
||||
iex> OptionParser.parse(["--limit", "3", "--unknown", "xyz"],
|
||||
...> switches: [limit: :integer])
|
||||
{[limit: 3, unknown: "xyz"], [], []}
|
||||
|
||||
## Negation switches
|
||||
|
||||
In case a switch is declared as boolean, it may be passed as `--no-SWITCH`
|
||||
which will set the option to false:
|
||||
|
||||
iex> OptionParser.parse(["--no-op", "path/to/file"], switches: [op: :boolean])
|
||||
{[op: false], ["path/to/file"], []}
|
||||
|
||||
## Aliases
|
||||
|
||||
A set of aliases can be given as options too:
|
||||
|
||||
iex> OptionParser.parse(["-d"], aliases: [d: :debug])
|
||||
{[debug: true], [], []}
|
||||
|
||||
"""
|
||||
@spec parse(argv, options) :: {parsed, argv, errors}
|
||||
def parse(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
||||
do_parse(argv, compile_config(opts), [], [], [], true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Similar to `parse/2` but only parses the head of `argv`;
|
||||
as soon as it finds a non-switch, it stops parsing.
|
||||
|
||||
See `parse/2` for more information.
|
||||
|
||||
## Example
|
||||
|
||||
iex> OptionParser.parse_head(["--source", "lib", "test/enum_test.exs", "--verbose"])
|
||||
{[source: "lib"], ["test/enum_test.exs", "--verbose"], []}
|
||||
|
||||
iex> OptionParser.parse_head(["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"])
|
||||
{[verbose: true, source: "lib"], ["test/enum_test.exs", "--unlock"], []}
|
||||
|
||||
"""
|
||||
@spec parse_head(argv, options) :: {parsed, argv, errors}
|
||||
def parse_head(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
||||
do_parse(argv, compile_config(opts), [], [], [], false)
|
||||
end
|
||||
|
||||
defp do_parse([], _config, opts, args, invalid, _all?) do
|
||||
{Enum.reverse(opts), Enum.reverse(args), Enum.reverse(invalid)}
|
||||
end
|
||||
|
||||
defp do_parse(argv, {aliases, switches, strict}=config, opts, args, invalid, all?) do
|
||||
case next(argv, aliases, switches, strict) do
|
||||
{:ok, option, value, rest} ->
|
||||
# the option exist and it was successfully parsed
|
||||
kinds = List.wrap Keyword.get(switches, option)
|
||||
new_opts = do_store_option(opts, option, value, kinds)
|
||||
do_parse(rest, config, new_opts, args, invalid, all?)
|
||||
|
||||
{:invalid, option, value, rest} ->
|
||||
# the option exist but it has wrong value
|
||||
do_parse(rest, config, opts, args, [{option, value}|invalid], all?)
|
||||
|
||||
{:undefined, option, _value, rest} ->
|
||||
# the option does not exist (for strict cases)
|
||||
do_parse(rest, config, opts, args, [{option, nil}|invalid], all?)
|
||||
|
||||
{:error, ["--"|rest]} ->
|
||||
{Enum.reverse(opts), Enum.reverse(args, rest), Enum.reverse(invalid)}
|
||||
|
||||
{:error, [arg|rest]=remaining_args} ->
|
||||
# there is no option
|
||||
if all? do
|
||||
do_parse(rest, config, opts, [arg|args], invalid, all?)
|
||||
else
|
||||
{Enum.reverse(opts), Enum.reverse(args, remaining_args), Enum.reverse(invalid)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Low-level function that parses one option.
|
||||
|
||||
It accepts the same options as `parse/2` and `parse_head/2`
|
||||
as both functions are built on top of next. This function
|
||||
may return:
|
||||
|
||||
* `{:ok, key, value, rest}` - the option `key` with `value` was
|
||||
successfully parsed
|
||||
|
||||
* `{:invalid, key, value, rest}` - the option `key` is invalid with `value`
|
||||
(returned when the switch type does not match the one given via the
|
||||
command line)
|
||||
|
||||
* `{:undefined, key, value, rest}` - the option `key` is undefined
|
||||
(returned on strict cases and the switch is unknown)
|
||||
|
||||
* `{:error, rest}` - there are no switches at the top of the given argv
|
||||
"""
|
||||
|
||||
@spec next(argv, options) ::
|
||||
{:ok, key :: atom, value :: term, argv} |
|
||||
{:invalid, String.t, String.t | nil, argv} |
|
||||
{:undefined, String.t, String.t | nil, argv} |
|
||||
{:error, argv}
|
||||
|
||||
def next(argv, opts \\ []) when is_list(argv) and is_list(opts) do
|
||||
{aliases, switches, strict} = compile_config(opts)
|
||||
next(argv, aliases, switches, strict)
|
||||
end
|
||||
|
||||
defp next([], _aliases, _switches, _strict) do
|
||||
{:error, []}
|
||||
end
|
||||
|
||||
defp next(["--"|_]=argv, _aliases, _switches, _strict) do
|
||||
{:error, argv}
|
||||
end
|
||||
|
||||
defp next(["-"|_]=argv, _aliases, _switches, _strict) do
|
||||
{:error, argv}
|
||||
end
|
||||
|
||||
defp next(["- " <> _|_]=argv, _aliases, _switches, _strict) do
|
||||
{:error, argv}
|
||||
end
|
||||
|
||||
defp next(["-" <> option|rest], aliases, switches, strict) do
|
||||
{option, value} = split_option(option)
|
||||
opt_name_bin = "-" <> option
|
||||
tagged = tag_option(option, switches, aliases)
|
||||
|
||||
if strict and not option_defined?(tagged, switches) do
|
||||
{:undefined, opt_name_bin, value, rest}
|
||||
else
|
||||
{opt_name, kinds, value} = normalize_option(tagged, value, switches)
|
||||
{value, kinds, rest} = normalize_value(value, kinds, rest, strict)
|
||||
case validate_option(value, kinds) do
|
||||
{:ok, new_value} -> {:ok, opt_name, new_value, rest}
|
||||
:invalid -> {:invalid, opt_name_bin, value, rest}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp next(argv, _aliases, _switches, _strict) do
|
||||
{:error, argv}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a key-value enumerable and convert it to argv.
|
||||
|
||||
Keys must be atoms. Keys with nil value are discarded,
|
||||
boolean values are converted to `--key` or `--no-key`
|
||||
and all other values are converted using `to_string/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> OptionParser.to_argv([foo_bar: "baz"])
|
||||
["--foo-bar", "baz"]
|
||||
|
||||
iex> OptionParser.to_argv([bool: true, bool: false, discarded: nil])
|
||||
["--bool", "--no-bool"]
|
||||
|
||||
"""
|
||||
@spec to_argv(Enumerable.t) :: argv
|
||||
def to_argv(enum) do
|
||||
Enum.flat_map(enum, fn
|
||||
{_key, nil} -> []
|
||||
{key, true} -> [to_switch(key)]
|
||||
{key, false} -> [to_switch(key, "--no-")]
|
||||
{key, value} -> [to_switch(key), to_string(value)]
|
||||
end)
|
||||
end
|
||||
|
||||
defp to_switch(key, prefix \\ "--") when is_atom(key) do
|
||||
prefix <> String.replace(Atom.to_string(key), "_", "-")
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Splits a string into argv chunks.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> OptionParser.split("foo bar")
|
||||
["foo", "bar"]
|
||||
|
||||
iex> OptionParser.split("foo \"bar baz\"")
|
||||
["foo", "bar baz"]
|
||||
"""
|
||||
@spec split(String.t) :: argv
|
||||
def split(string) do
|
||||
do_split(strip_leading_spaces(string), "", [], nil)
|
||||
end
|
||||
|
||||
# If we have a escaped quote, simply remove the escape
|
||||
defp do_split(<<?\\, quote, t :: binary>>, buffer, acc, quote),
|
||||
do: do_split(t, <<buffer::binary, quote>>, acc, quote)
|
||||
|
||||
# If we have a quote and we were not in a quote, start one
|
||||
defp do_split(<<quote, t :: binary>>, buffer, acc, nil) when quote in [?", ?'],
|
||||
do: do_split(t, buffer, acc, quote)
|
||||
|
||||
# If we have a quote and we were inside it, close it
|
||||
defp do_split(<<quote, t :: binary>>, buffer, acc, quote),
|
||||
do: do_split(t, buffer, acc, nil)
|
||||
|
||||
# If we have a escaped quote/space, simply remove the escape as long as we are not inside a quote
|
||||
defp do_split(<<?\\, h, t :: binary>>, buffer, acc, nil) when h in [?\s, ?', ?"],
|
||||
do: do_split(t, <<buffer::binary, h>>, acc, nil)
|
||||
|
||||
# If we have space and we are outside of a quote, start new segment
|
||||
defp do_split(<<?\s, t :: binary>>, buffer, acc, nil),
|
||||
do: do_split(strip_leading_spaces(t), "", [buffer|acc], nil)
|
||||
|
||||
# All other characters are moved to buffer
|
||||
defp do_split(<<h, t::binary>>, buffer, acc, quote) do
|
||||
do_split(t, <<buffer::binary, h>>, acc, quote)
|
||||
end
|
||||
|
||||
# Finish the string expecting a nil marker
|
||||
defp do_split(<<>>, "", acc, nil),
|
||||
do: Enum.reverse(acc)
|
||||
|
||||
defp do_split(<<>>, buffer, acc, nil),
|
||||
do: Enum.reverse([buffer|acc])
|
||||
|
||||
# Otherwise raise
|
||||
defp do_split(<<>>, _, _acc, marker) do
|
||||
raise "argv string did not terminate properly, a #{<<marker>>} was opened but never closed"
|
||||
end
|
||||
|
||||
defp strip_leading_spaces(" " <> t), do: strip_leading_spaces(t)
|
||||
defp strip_leading_spaces(t), do: t
|
||||
|
||||
## Helpers
|
||||
|
||||
defp compile_config(opts) do
|
||||
aliases = opts[:aliases] || []
|
||||
|
||||
{switches, strict} = cond do
|
||||
s = opts[:switches] ->
|
||||
{s, false}
|
||||
s = opts[:strict] ->
|
||||
{s, true}
|
||||
true ->
|
||||
{[], false}
|
||||
end
|
||||
|
||||
{aliases, switches, strict}
|
||||
end
|
||||
|
||||
defp validate_option(value, kinds) do
|
||||
{is_invalid, value} = cond do
|
||||
:invalid in kinds ->
|
||||
{true, value}
|
||||
:boolean in kinds ->
|
||||
case value do
|
||||
t when t in [true, "true"] -> {nil, true}
|
||||
f when f in [false, "false"] -> {nil, false}
|
||||
_ -> {true, value}
|
||||
end
|
||||
:integer in kinds ->
|
||||
case Integer.parse(value) do
|
||||
{value, ""} -> {nil, value}
|
||||
_ -> {true, value}
|
||||
end
|
||||
:float in kinds ->
|
||||
case Float.parse(value) do
|
||||
{value, ""} -> {nil, value}
|
||||
_ -> {true, value}
|
||||
end
|
||||
true ->
|
||||
{nil, value}
|
||||
end
|
||||
|
||||
if is_invalid do
|
||||
:invalid
|
||||
else
|
||||
{:ok, value}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_store_option(dict, option, value, kinds) do
|
||||
cond do
|
||||
:keep in kinds ->
|
||||
[{option, value}|dict]
|
||||
true ->
|
||||
[{option, value}|Keyword.delete(dict, option)]
|
||||
end
|
||||
end
|
||||
|
||||
defp tag_option(<<?-, option :: binary>>, switches, _aliases) do
|
||||
get_negated(option, switches)
|
||||
end
|
||||
|
||||
defp tag_option(option, _switches, aliases) when is_binary(option) do
|
||||
opt = get_option(option)
|
||||
if alias = aliases[opt] do
|
||||
{:default, alias}
|
||||
else
|
||||
:unknown
|
||||
end
|
||||
end
|
||||
|
||||
defp option_defined?(:unknown, _switches) do
|
||||
false
|
||||
end
|
||||
|
||||
defp option_defined?({:negated, option}, switches) do
|
||||
Keyword.has_key?(switches, option)
|
||||
end
|
||||
|
||||
defp option_defined?({:default, option}, switches) do
|
||||
Keyword.has_key?(switches, option)
|
||||
end
|
||||
|
||||
defp normalize_option(:unknown, value, _switches) do
|
||||
{nil, [:invalid], value}
|
||||
end
|
||||
|
||||
defp normalize_option({:negated, option}, value, switches) do
|
||||
if value do
|
||||
{option, [:invalid], value}
|
||||
else
|
||||
{option, List.wrap(switches[option]), false}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_option({:default, option}, value, switches) do
|
||||
{option, List.wrap(switches[option]), value}
|
||||
end
|
||||
|
||||
defp normalize_value(nil, kinds, t, strict) do
|
||||
nil_or_true = if strict, do: nil, else: true
|
||||
cond do
|
||||
:boolean in kinds ->
|
||||
{true, kinds, t}
|
||||
value_in_tail?(t) ->
|
||||
[h|t] = t
|
||||
{h, kinds, t}
|
||||
kinds == [] ->
|
||||
{nil_or_true, kinds, t}
|
||||
true ->
|
||||
{nil, [:invalid], t}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_value(value, kinds, t, _) do
|
||||
{value, kinds, t}
|
||||
end
|
||||
|
||||
defp value_in_tail?(["-"|_]), do: true
|
||||
defp value_in_tail?(["- " <> _|_]), do: true
|
||||
defp value_in_tail?(["-" <> _|_]), do: false
|
||||
defp value_in_tail?([]), do: false
|
||||
defp value_in_tail?(_), do: true
|
||||
|
||||
defp split_option(option) do
|
||||
case :binary.split(option, "=") do
|
||||
[h] -> {h, nil}
|
||||
[h, t] -> {h, t}
|
||||
end
|
||||
end
|
||||
|
||||
defp to_underscore(option), do: to_underscore(option, <<>>)
|
||||
|
||||
defp to_underscore("_" <> _rest, _acc), do: nil
|
||||
|
||||
defp to_underscore("-" <> rest, acc),
|
||||
do: to_underscore(rest, acc <> "_")
|
||||
|
||||
defp to_underscore(<<c>> <> rest, acc),
|
||||
do: to_underscore(rest, <<acc::binary, c>>)
|
||||
|
||||
defp to_underscore(<<>>, acc), do: acc
|
||||
|
||||
defp get_option(option) do
|
||||
if str = to_underscore(option) do
|
||||
String.to_atom(str)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_negated("no-" <> rest = original, switches) do
|
||||
cond do
|
||||
(negated = get_option(rest)) && :boolean in List.wrap(switches[negated]) ->
|
||||
{:negated, negated}
|
||||
option = get_option(original) ->
|
||||
{:default, option}
|
||||
true ->
|
||||
:unknown
|
||||
end
|
||||
end
|
||||
|
||||
defp get_negated(rest, _switches) do
|
||||
if option = get_option(rest) do
|
||||
{:default, option}
|
||||
else
|
||||
:unknown
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,660 +0,0 @@
|
||||
defmodule Path do
|
||||
@moduledoc """
|
||||
This module provides conveniences for manipulating or
|
||||
retrieving file system paths.
|
||||
|
||||
The functions in this module may receive a char data as
|
||||
argument (i.e. a string or a list of characters / string)
|
||||
and will always return a string (encoded in UTF-8).
|
||||
|
||||
The majority of the functions in this module do not
|
||||
interact with the file system, except for a few functions
|
||||
that require it (like `wildcard/1` and `expand/1`).
|
||||
"""
|
||||
|
||||
alias :filename, as: FN
|
||||
@type t :: :unicode.chardata()
|
||||
|
||||
@doc """
|
||||
Converts the given path to an absolute one. Unlike
|
||||
`expand/1`, no attempt is made to resolve `..`, `.` or `~`.
|
||||
|
||||
## Unix examples
|
||||
|
||||
Path.absname("foo")
|
||||
#=> "/usr/local/foo"
|
||||
|
||||
Path.absname("../x")
|
||||
#=> "/usr/local/../x"
|
||||
|
||||
## Windows
|
||||
|
||||
Path.absname("foo").
|
||||
"D:/usr/local/foo"
|
||||
Path.absname("../x").
|
||||
"D:/usr/local/../x"
|
||||
|
||||
"""
|
||||
@spec absname(t) :: binary
|
||||
def absname(path) do
|
||||
absname(path, System.cwd!)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a path from `relative_to` to `path`. If `path` is already
|
||||
an absolute path, `relative_to` is ignored. See also `relative_to/2`.
|
||||
|
||||
Unlike `expand/2`, no attempt is made to
|
||||
resolve `..`, `.` or `~`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.absname("foo", "bar")
|
||||
"bar/foo"
|
||||
|
||||
iex> Path.absname("../x", "bar")
|
||||
"bar/../x"
|
||||
|
||||
"""
|
||||
@spec absname(t, t) :: binary
|
||||
def absname(path, relative_to) do
|
||||
path = IO.chardata_to_string(path)
|
||||
case type(path) do
|
||||
:relative -> absname_join(relative_to, path)
|
||||
:absolute -> absname_join([path])
|
||||
:volumerelative ->
|
||||
relative_to = IO.chardata_to_string(relative_to)
|
||||
absname_vr(split(path), split(relative_to), relative_to)
|
||||
end
|
||||
end
|
||||
|
||||
# Absolute path on current drive
|
||||
defp absname_vr(["/"|rest], [volume|_], _relative),
|
||||
do: absname_join([volume|rest])
|
||||
|
||||
# Relative to current directory on current drive.
|
||||
defp absname_vr([<<x, ?:>>|rest], [<<x, _ :: binary>>|_], relative),
|
||||
do: absname(absname_join(rest), relative)
|
||||
|
||||
# Relative to current directory on another drive.
|
||||
defp absname_vr([<<x, ?:>>|name], _, _relative) do
|
||||
cwd =
|
||||
case :file.get_cwd([x, ?:]) do
|
||||
{:ok, dir} -> IO.chardata_to_string(dir)
|
||||
{:error, _} -> <<x, ?:, ?/>>
|
||||
end
|
||||
absname(absname_join(name), cwd)
|
||||
end
|
||||
|
||||
# Joins a list
|
||||
defp absname_join([name1, name2|rest]), do:
|
||||
absname_join([absname_join(name1, name2)|rest])
|
||||
defp absname_join([name]), do:
|
||||
do_absname_join(IO.chardata_to_string(name), <<>>, [], major_os_type())
|
||||
|
||||
# Joins two paths
|
||||
defp absname_join(left, right),
|
||||
do: do_absname_join(IO.chardata_to_string(left), relative(right), [], major_os_type())
|
||||
|
||||
defp do_absname_join(<<uc_letter, ?:, rest :: binary>>, relativename, [], :win32) when uc_letter in ?A..?Z, do:
|
||||
do_absname_join(rest, relativename, [?:, uc_letter+?a-?A], :win32)
|
||||
defp do_absname_join(<<?\\, rest :: binary>>, relativename, result, :win32), do:
|
||||
do_absname_join(<<?/, rest :: binary>>, relativename, result, :win32)
|
||||
defp do_absname_join(<<?/, rest :: binary>>, relativename, [?., ?/|result], os_type), do:
|
||||
do_absname_join(rest, relativename, [?/|result], os_type)
|
||||
defp do_absname_join(<<?/, rest :: binary>>, relativename, [?/|result], os_type), do:
|
||||
do_absname_join(rest, relativename, [?/|result], os_type)
|
||||
defp do_absname_join(<<>>, <<>>, result, os_type), do:
|
||||
IO.iodata_to_binary(reverse_maybe_remove_dirsep(result, os_type))
|
||||
defp do_absname_join(<<>>, relativename, [?:|rest], :win32), do:
|
||||
do_absname_join(relativename, <<>>, [?:|rest], :win32)
|
||||
defp do_absname_join(<<>>, relativename, [?/|result], os_type), do:
|
||||
do_absname_join(relativename, <<>>, [?/|result], os_type)
|
||||
defp do_absname_join(<<>>, relativename, result, os_type), do:
|
||||
do_absname_join(relativename, <<>>, [?/|result], os_type)
|
||||
defp do_absname_join(<<char, rest :: binary>>, relativename, result, os_type), do:
|
||||
do_absname_join(rest, relativename, [char|result], os_type)
|
||||
|
||||
defp reverse_maybe_remove_dirsep([?/, ?:, letter], :win32), do:
|
||||
[letter, ?:, ?/]
|
||||
defp reverse_maybe_remove_dirsep([?/], _), do:
|
||||
[?/]
|
||||
defp reverse_maybe_remove_dirsep([?/|name], _), do:
|
||||
:lists.reverse(name)
|
||||
defp reverse_maybe_remove_dirsep(name, _), do:
|
||||
:lists.reverse(name)
|
||||
|
||||
@doc """
|
||||
Converts the path to an absolute one and expands
|
||||
any `.` and `..` characters and a leading `~`.
|
||||
|
||||
## Examples
|
||||
|
||||
Path.expand("/foo/bar/../bar")
|
||||
"/foo/bar"
|
||||
|
||||
"""
|
||||
@spec expand(t) :: binary
|
||||
def expand(path) do
|
||||
expand_dot absname(expand_home(path), System.cwd!)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Expands the path relative to the path given as the second argument
|
||||
expanding any `.` and `..` characters. If the path is already an
|
||||
absolute path, `relative_to` is ignored.
|
||||
|
||||
Note, that this function treats `path` with a leading `~` as
|
||||
an absolute one.
|
||||
|
||||
The second argument is first expanded to an absolute path.
|
||||
|
||||
## Examples
|
||||
|
||||
# Assuming that the absolute path to baz is /quux/baz
|
||||
Path.expand("foo/bar/../bar", "baz")
|
||||
#=> "/quux/baz/foo/bar"
|
||||
|
||||
Path.expand("foo/bar/../bar", "/baz")
|
||||
"/baz/foo/bar"
|
||||
Path.expand("/foo/bar/../bar", "/baz")
|
||||
"/foo/bar"
|
||||
|
||||
"""
|
||||
@spec expand(t, t) :: binary
|
||||
def expand(path, relative_to) do
|
||||
expand_dot absname(absname(expand_home(path), expand_home(relative_to)), System.cwd!)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the path type.
|
||||
|
||||
## Unix examples
|
||||
|
||||
Path.type("/") #=> :absolute
|
||||
Path.type("/usr/local/bin") #=> :absolute
|
||||
Path.type("usr/local/bin") #=> :relative
|
||||
Path.type("../usr/local/bin") #=> :relative
|
||||
Path.type("~/file") #=> :relative
|
||||
|
||||
## Windows examples
|
||||
|
||||
Path.type("D:/usr/local/bin") #=> :absolute
|
||||
Path.type("usr/local/bin") #=> :relative
|
||||
Path.type("D:bar.ex") #=> :volumerelative
|
||||
Path.type("/bar/foo.ex") #=> :volumerelative
|
||||
|
||||
"""
|
||||
@spec type(t) :: :absolute | :relative | :volumerelative
|
||||
def type(name) when is_list(name) or is_binary(name) do
|
||||
pathtype(name, major_os_type) |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Forces the path to be a relative path.
|
||||
|
||||
## Unix examples
|
||||
|
||||
Path.relative("/usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("../usr/local/bin") #=> "../usr/local/bin"
|
||||
|
||||
## Windows examples
|
||||
|
||||
Path.relative("D:/usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("D:bar.ex") #=> "bar.ex"
|
||||
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
|
||||
|
||||
"""
|
||||
@spec relative(t) :: binary
|
||||
def relative(name) do
|
||||
relative(name, major_os_type())
|
||||
end
|
||||
|
||||
defp relative(name, os_type) do
|
||||
pathtype(name, os_type)
|
||||
|> elem(1)
|
||||
|> IO.chardata_to_string
|
||||
end
|
||||
|
||||
defp pathtype(name, os_type) do
|
||||
case os_type do
|
||||
:win32 -> win32_pathtype(name)
|
||||
_ -> unix_pathtype(name)
|
||||
end
|
||||
end
|
||||
|
||||
defp unix_pathtype(<<?/, relative :: binary>>), do:
|
||||
{:absolute, relative}
|
||||
defp unix_pathtype([?/|relative]), do:
|
||||
{:absolute, relative}
|
||||
defp unix_pathtype([list|rest]) when is_list(list), do:
|
||||
unix_pathtype(list ++ rest)
|
||||
defp unix_pathtype(relative), do:
|
||||
{:relative, relative}
|
||||
|
||||
@slash [?/, ?\\]
|
||||
|
||||
defp win32_pathtype([list|rest]) when is_list(list), do:
|
||||
win32_pathtype(list++rest)
|
||||
defp win32_pathtype([char, list|rest]) when is_list(list), do:
|
||||
win32_pathtype([char|list++rest])
|
||||
defp win32_pathtype(<<c1, c2, relative :: binary>>) when c1 in @slash and c2 in @slash, do:
|
||||
{:absolute, relative}
|
||||
defp win32_pathtype(<<c, relative :: binary>>) when c in @slash, do:
|
||||
{:volumerelative, relative}
|
||||
defp win32_pathtype(<<_letter, ?:, c, relative :: binary>>) when c in @slash, do:
|
||||
{:absolute, relative}
|
||||
defp win32_pathtype(<<_letter, ?:, relative :: binary>>), do:
|
||||
{:volumerelative, relative}
|
||||
|
||||
defp win32_pathtype([c1, c2 | relative]) when c1 in @slash and c2 in @slash, do:
|
||||
{:absolute, relative}
|
||||
defp win32_pathtype([c | relative]) when c in @slash, do:
|
||||
{:volumerelative, relative}
|
||||
defp win32_pathtype([c1, c2, list|rest]) when is_list(list), do:
|
||||
win32_pathtype([c1, c2|list++rest])
|
||||
defp win32_pathtype([_letter, ?:, c | relative]) when c in @slash, do:
|
||||
{:absolute, relative}
|
||||
defp win32_pathtype([_letter, ?: | relative]), do:
|
||||
{:volumerelative, relative}
|
||||
defp win32_pathtype(relative), do:
|
||||
{:relative, relative}
|
||||
|
||||
@doc """
|
||||
Returns the given `path` relative to the given `from` path.
|
||||
In other words, it tries to strip the `from` prefix from `path`.
|
||||
|
||||
This function does not query the file system, so it assumes
|
||||
no symlinks in between the paths.
|
||||
|
||||
In case a direct relative path cannot be found, it returns
|
||||
the original path.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/usr/local")
|
||||
"foo"
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/")
|
||||
"usr/local/foo"
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/etc")
|
||||
"/usr/local/foo"
|
||||
|
||||
"""
|
||||
@spec relative_to(t, t) :: binary
|
||||
def relative_to(path, from) do
|
||||
path = IO.chardata_to_string(path)
|
||||
relative_to(split(path), split(from), path)
|
||||
end
|
||||
|
||||
defp relative_to([h|t1], [h|t2], original) do
|
||||
relative_to(t1, t2, original)
|
||||
end
|
||||
|
||||
defp relative_to([_|_] = l1, [], _original) do
|
||||
join(l1)
|
||||
end
|
||||
|
||||
defp relative_to(_, _, original) do
|
||||
original
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convenience to get the path relative to the current working
|
||||
directory. If, for some reason, the current working directory
|
||||
cannot be retrieved, returns the full path.
|
||||
"""
|
||||
@spec relative_to_cwd(t) :: binary
|
||||
def relative_to_cwd(path) do
|
||||
case :file.get_cwd do
|
||||
{:ok, base} -> relative_to(path, IO.chardata_to_string(base))
|
||||
_ -> path
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last component of the path or the path
|
||||
itself if it does not contain any directory separators.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.basename("foo")
|
||||
"foo"
|
||||
|
||||
iex> Path.basename("foo/bar")
|
||||
"bar"
|
||||
|
||||
iex> Path.basename("/")
|
||||
""
|
||||
|
||||
"""
|
||||
@spec basename(t) :: binary
|
||||
def basename(path) do
|
||||
FN.basename(IO.chardata_to_string(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last component of `path` with the `extension`
|
||||
stripped. This function should be used to remove a specific
|
||||
extension which may, or may not, be there.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.basename("~/foo/bar.ex", ".ex")
|
||||
"bar"
|
||||
|
||||
iex> Path.basename("~/foo/bar.exs", ".ex")
|
||||
"bar.exs"
|
||||
|
||||
iex> Path.basename("~/foo/bar.old.ex", ".ex")
|
||||
"bar.old"
|
||||
|
||||
"""
|
||||
@spec basename(t, t) :: binary
|
||||
def basename(path, extension) do
|
||||
FN.basename(IO.chardata_to_string(path), IO.chardata_to_string(extension))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the directory component of `path`.
|
||||
|
||||
## Examples
|
||||
|
||||
Path.dirname("/foo/bar.ex")
|
||||
#=> "/foo"
|
||||
Path.dirname("/foo/bar/baz.ex")
|
||||
#=> "/foo/bar"
|
||||
|
||||
"""
|
||||
@spec dirname(t) :: binary
|
||||
def dirname(path) do
|
||||
FN.dirname(IO.chardata_to_string(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the extension of the last component of `path`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.extname("foo.erl")
|
||||
".erl"
|
||||
|
||||
iex> Path.extname("~/foo/bar")
|
||||
""
|
||||
|
||||
"""
|
||||
@spec extname(t) :: binary
|
||||
def extname(path) do
|
||||
FN.extension(IO.chardata_to_string(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the `path` with the `extension` stripped.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.rootname("/foo/bar")
|
||||
"/foo/bar"
|
||||
|
||||
iex> Path.rootname("/foo/bar.ex")
|
||||
"/foo/bar"
|
||||
|
||||
"""
|
||||
@spec rootname(t) :: binary
|
||||
def rootname(path) do
|
||||
FN.rootname(IO.chardata_to_string(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the `path` with the `extension` stripped. This function should be used to
|
||||
remove a specific extension which might, or might not, be there.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.rootname("/foo/bar.erl", ".erl")
|
||||
"/foo/bar"
|
||||
|
||||
iex> Path.rootname("/foo/bar.erl", ".ex")
|
||||
"/foo/bar.erl"
|
||||
|
||||
"""
|
||||
@spec rootname(t, t) :: binary
|
||||
def rootname(path, extension) do
|
||||
FN.rootname(IO.chardata_to_string(path), IO.chardata_to_string(extension))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Joins a list of strings.
|
||||
|
||||
This function should be used to convert a list of strings to a path.
|
||||
Note that any trailing slash is removed on join.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.join(["~", "foo"])
|
||||
"~/foo"
|
||||
|
||||
iex> Path.join(["foo"])
|
||||
"foo"
|
||||
|
||||
iex> Path.join(["/", "foo", "bar/"])
|
||||
"/foo/bar"
|
||||
|
||||
"""
|
||||
@spec join([t]) :: binary
|
||||
def join([name1, name2|rest]), do:
|
||||
join([join(name1, name2)|rest])
|
||||
def join([name]), do:
|
||||
name
|
||||
|
||||
@doc """
|
||||
Joins two paths.
|
||||
|
||||
The right path will always be expanded to its relative format
|
||||
and any trailing slash is removed on join.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.join("foo", "bar")
|
||||
"foo/bar"
|
||||
|
||||
"""
|
||||
@spec join(t, t) :: binary
|
||||
def join(left, right) do
|
||||
left = IO.chardata_to_string(left)
|
||||
os_type = major_os_type()
|
||||
do_join(left, right, os_type) |> remove_dirsep(os_type)
|
||||
end
|
||||
|
||||
defp do_join("", right, os_type), do: relative(right, os_type)
|
||||
defp do_join(left, "", _os_type), do: left
|
||||
defp do_join(left, right, os_type), do: remove_dirsep(left, os_type) <> "/" <> relative(right, os_type)
|
||||
|
||||
defp remove_dirsep("", _os_type), do: ""
|
||||
defp remove_dirsep(bin, os_type) do
|
||||
last = :binary.last(bin)
|
||||
if last == ?/ or (last == ?\\ and os_type == :win32) do
|
||||
binary_part(bin, 0, byte_size(bin) - 1)
|
||||
else
|
||||
bin
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Splits the path into a list at the path separator.
|
||||
|
||||
If an empty string is given, returns an empty list.
|
||||
|
||||
On Windows, path is split on both "\" and "/" separators
|
||||
and the driver letter, if there is one, is always returned
|
||||
in lowercase.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.split("")
|
||||
[]
|
||||
|
||||
iex> Path.split("foo")
|
||||
["foo"]
|
||||
|
||||
iex> Path.split("/foo/bar")
|
||||
["/", "foo", "bar"]
|
||||
|
||||
"""
|
||||
@spec split(t) :: [binary]
|
||||
|
||||
# Work around a bug in Erlang on UNIX
|
||||
def split(""), do: []
|
||||
|
||||
def split(path) do
|
||||
FN.split(IO.chardata_to_string(path))
|
||||
end
|
||||
|
||||
defmodule Wildcard do
|
||||
@moduledoc false
|
||||
|
||||
def read_link_info(file) do
|
||||
call({:read_link_info, file})
|
||||
end
|
||||
|
||||
# For compatibility with buggy Erlang 17.1.
|
||||
def read_file_info(file) do
|
||||
call({:read_link_info, file})
|
||||
end
|
||||
|
||||
def list_dir(dir) do
|
||||
case call({:list_dir, dir}) do
|
||||
{:ok, files} ->
|
||||
{:ok, for(file <- files, hd(file) != ?., do: file)}
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, call: 1}
|
||||
|
||||
defp call(tuple) do
|
||||
x = :erlang.dt_spread_tag(true)
|
||||
y = :gen_server.call(:file_server_2, tuple)
|
||||
:erlang.dt_restore_tag(x)
|
||||
y
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Traverses paths according to the given `glob` expression.
|
||||
|
||||
The wildcard looks like an ordinary path, except that certain
|
||||
"wildcard characters" are interpreted in a special way. The
|
||||
following characters are special:
|
||||
|
||||
* `?` - matches one character
|
||||
|
||||
* `*` - matches any number of characters up to the end of the filename, the
|
||||
next dot, or the next slash
|
||||
|
||||
* `**` - two adjacent `*`'s used as a single pattern will match all
|
||||
files and zero or more directories and subdirectories
|
||||
|
||||
* `[char1,char2,...]` - matches any of the characters listed; two
|
||||
characters separated by a hyphen will match a range of characters
|
||||
|
||||
* `{item1,item2,...}` - matches one of the alternatives
|
||||
|
||||
Other characters represent themselves. Only paths that have
|
||||
exactly the same character in the same position will match. Note
|
||||
that matching is case-sensitive; i.e. "a" will not match "A".
|
||||
|
||||
By default, the patterns `*` and `?` do not match files starting
|
||||
with a dot `.` unless `match_dot: true` is given.
|
||||
|
||||
## Examples
|
||||
|
||||
Imagine you have a directory called `projects` with three Elixir projects
|
||||
inside of it: `elixir`, `ex_doc` and `dynamo`. You can find all `.beam` files
|
||||
inside the `ebin` directory of each project as follows:
|
||||
|
||||
Path.wildcard("projects/*/ebin/**/*.beam")
|
||||
|
||||
If you want to search for both `.beam` and `.app` files, you could do:
|
||||
|
||||
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
|
||||
|
||||
"""
|
||||
@spec wildcard(t) :: [binary]
|
||||
def wildcard(glob, opts \\ []) do
|
||||
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
|
||||
glob
|
||||
|> chardata_to_list()
|
||||
|> :filelib.wildcard(mod)
|
||||
|> Enum.map(&IO.chardata_to_string/1)
|
||||
end
|
||||
|
||||
# expand_dot the given path by expanding "..", "." and "~".
|
||||
|
||||
defp chardata_to_list(chardata) do
|
||||
case :unicode.characters_to_list(chardata) do
|
||||
result when is_list(result) ->
|
||||
result
|
||||
|
||||
{:error, encoded, rest} ->
|
||||
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid
|
||||
|
||||
{:incomplete, encoded, rest} ->
|
||||
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :incomplete
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_home(type) do
|
||||
case IO.chardata_to_string(type) do
|
||||
"~" <> rest -> resolve_home(rest)
|
||||
rest -> rest
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_home(""), do: System.user_home!
|
||||
|
||||
defp resolve_home(rest) do
|
||||
case {rest, major_os_type} do
|
||||
{"\\" <> _, :win32} ->
|
||||
System.user_home! <> rest
|
||||
{"/" <> _, _} ->
|
||||
System.user_home! <> rest
|
||||
_ -> rest
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_dot(<<"/../", rest::binary>>),
|
||||
do: expand_dot("/" <> rest)
|
||||
defp expand_dot(<<letter, ":/../", rest::binary>>) when letter in ?a..?z,
|
||||
do: expand_dot(<<letter, ":/", rest::binary>>)
|
||||
defp expand_dot("/.."),
|
||||
do: "/"
|
||||
defp expand_dot(<<letter, ":/..">>) when letter in ?a..?z,
|
||||
do: expand_dot(<<letter, ":/">>)
|
||||
defp expand_dot(path),
|
||||
do: expand_dot(:binary.split(path, "/", [:global]), [])
|
||||
|
||||
defp expand_dot([".."|t], [_, _|acc]) do
|
||||
expand_dot t, acc
|
||||
end
|
||||
|
||||
defp expand_dot(["."|t], acc) do
|
||||
expand_dot t, acc
|
||||
end
|
||||
|
||||
defp expand_dot([h|t], acc) do
|
||||
expand_dot t, ["/", h|acc]
|
||||
end
|
||||
|
||||
defp expand_dot([], ["/"|acc]) do
|
||||
IO.iodata_to_binary(:lists.reverse(acc))
|
||||
end
|
||||
|
||||
defp major_os_type do
|
||||
:os.type |> elem(0)
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
defmodule Port do
|
||||
@moduledoc """
|
||||
Functions related to Erlang ports.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#open_port-2.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def open(name, settings) do
|
||||
:erlang.open_port(name, settings)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_close-1.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def close(port) do
|
||||
:erlang.port_close(port)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_command-2.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def command(port, data, options \\ []) do
|
||||
:erlang.port_command(port, data, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_connect-2.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def connect(port, pid) do
|
||||
:erlang.port_connect(port, pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_control-3.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def control(port, operation, data) do
|
||||
:erlang.port_control(port, operation, data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_call-3.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def call(port, operation, data) do
|
||||
:erlang.port_call(port, operation, data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_info-1.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def info(port) do
|
||||
:erlang.port_info(port)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_info-2.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def info(port, item) do
|
||||
:erlang.port_info(port, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#ports-0.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
def list do
|
||||
:erlang.ports
|
||||
end
|
||||
end
|
||||
@@ -1,389 +0,0 @@
|
||||
defmodule Process do
|
||||
@moduledoc """
|
||||
Conveniences for working with processes and the process dictionary.
|
||||
|
||||
Besides the functions available in this module, the `Kernel` module
|
||||
exposes and auto-imports some basic functionality related to processes
|
||||
available through the functions:
|
||||
|
||||
* `Kernel.spawn/1` and `Kernel.spawn/3`
|
||||
* `Kernel.spawn_link/1` and `Kernel.spawn_link/3`
|
||||
* `Kernel.spawn_monitor/1` and `Kernel.spawn_monitor/3`
|
||||
* `Kernel.self/0`
|
||||
* `Kernel.send/2`
|
||||
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns true if the process exists and is alive, that is,
|
||||
is not exiting and has not exited. Otherwise, returns false.
|
||||
|
||||
`pid` must refer to a process at the local node.
|
||||
"""
|
||||
@spec alive?(pid) :: boolean
|
||||
def alive?(pid) do
|
||||
:erlang.is_process_alive(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all key-values in the dictionary.
|
||||
"""
|
||||
@spec get :: [{term, term}]
|
||||
def get do
|
||||
:erlang.get()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value for the given `key`.
|
||||
"""
|
||||
@spec get(term) :: term
|
||||
@spec get(term, default :: term) :: term
|
||||
def get(key, default \\ nil) do
|
||||
case :erlang.get(key) do
|
||||
:undefined ->
|
||||
default
|
||||
value ->
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all keys that have the given `value`.
|
||||
"""
|
||||
@spec get_keys(term) :: [term]
|
||||
def get_keys(value) do
|
||||
:erlang.get_keys(value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores the given key-value in the process dictionary.
|
||||
"""
|
||||
@spec put(term, term) :: term | nil
|
||||
def put(key, value) do
|
||||
nillify :erlang.put(key, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the given `key` from the dictionary.
|
||||
"""
|
||||
@spec delete(term) :: term | nil
|
||||
def delete(key) do
|
||||
nillify :erlang.erase(key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an exit signal with the given reason to the pid.
|
||||
|
||||
The following behaviour applies if reason is any term except `:normal` or `:kill`:
|
||||
|
||||
1. If pid is not trapping exits, pid will exit with the given reason.
|
||||
|
||||
2. If pid is trapping exits, the exit signal is transformed into a message
|
||||
`{:EXIT, from, reason}` and delivered to the message queue of pid.
|
||||
|
||||
3. If reason is the atom `:normal`, pid will not exit. If it is trapping
|
||||
exits, the exit signal is transformed into a message `{:EXIT, from,
|
||||
:normal}` and delivered to its message queue.
|
||||
|
||||
4. If reason is the atom `:kill`, that is if `exit(pid, :kill)` is called,
|
||||
an untrappable exit signal is sent to pid which will unconditionally
|
||||
exit with exit reason `:killed`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
Process.exit(pid, :kill)
|
||||
|
||||
"""
|
||||
@spec exit(pid, term) :: true
|
||||
def exit(pid, reason) do
|
||||
:erlang.exit(pid, reason)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends a message to the given process.
|
||||
|
||||
If the option `:noconnect` is used and sending the message would require an
|
||||
auto-connection to another node the message is not sent and `:noconnect` is
|
||||
returned.
|
||||
|
||||
If the option `:nosuspend` is used and sending the message would cause the
|
||||
sender to be suspended the message is not sent and `:nosuspend` is returned.
|
||||
|
||||
Otherwise the message is sent and `:ok` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Process.send({:name, :node_does_not_exist}, :hi, [:noconnect])
|
||||
:noconnect
|
||||
|
||||
"""
|
||||
@spec send(dest, msg, [option]) :: result when
|
||||
dest: pid | port | atom | {atom, node},
|
||||
msg: any,
|
||||
option: :noconnect | :nosuspend,
|
||||
result: :ok | :noconnect | :nosuspend
|
||||
def send(dest, msg, options) do
|
||||
:erlang.send(dest, msg, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends `msg` to `dest` after `time` milliseconds.
|
||||
|
||||
If `dest` is a pid, it must be the pid of a local process, dead or alive.
|
||||
If `dest` is an atom, it must be the name of a registered process
|
||||
which is looked up at the time of delivery. No error is given if the name does
|
||||
not refer to a process.
|
||||
|
||||
This function returns a timer reference, which can be read or canceled with
|
||||
`:erlang.read_timer/1`, `:erlang.start_timer/3` and `:erlang.cancel_timer/1`.
|
||||
Note `time` cannot be greater than `4294967295`.
|
||||
|
||||
Finally, the timer will be automatically canceled if the given `dest` is a pid
|
||||
which is not alive or when the given pid exits. Note that timers will not be
|
||||
automatically canceled when `dest` is an atom (as the atom resolution is done
|
||||
on delivery).
|
||||
"""
|
||||
@spec send_after(pid | atom, term, non_neg_integer) :: reference
|
||||
def send_after(dest, msg, time) do
|
||||
:erlang.send_after(time, dest, msg)
|
||||
end
|
||||
|
||||
@type spawn_opt :: :link | :monitor | {:priority, :low | :normal | :high} |
|
||||
{:fullsweep_after, non_neg_integer} |
|
||||
{:min_heap_size, non_neg_integer} |
|
||||
{:min_bin_vheap_size, non_neg_integer}
|
||||
@type spawn_opts :: [spawn_opt]
|
||||
|
||||
@doc """
|
||||
Spawns the given module and function passing the given args
|
||||
according to the given options.
|
||||
|
||||
The result depends on the given options. In particular,
|
||||
if `:monitor` is given as an option, it will return a tuple
|
||||
containing the pid and the monitoring reference, otherwise
|
||||
just the spawned process pid.
|
||||
|
||||
It also accepts extra options, for the list of available options
|
||||
check http://www.erlang.org/doc/man/erlang.html#spawn_opt-4
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn((() -> any), spawn_opts) :: pid | {pid, reference}
|
||||
def spawn(fun, opts) do
|
||||
:erlang.spawn_opt(fun, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns the given module and function passing the given args
|
||||
according to the given options.
|
||||
|
||||
The result depends on the given options. In particular,
|
||||
if `:monitor` is given as an option, it will return a tuple
|
||||
containing the pid and the monitoring reference, otherwise
|
||||
just the spawned process pid.
|
||||
|
||||
It also accepts extra options, for the list of available options
|
||||
check http://www.erlang.org/doc/man/erlang.html#spawn_opt-4
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec spawn(module, atom, list, spawn_opts) :: pid | {pid, reference}
|
||||
def spawn(mod, fun, args, opts) do
|
||||
:erlang.spawn_opt(mod, fun, args, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
The calling process starts monitoring the item given.
|
||||
It returns the monitor reference.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor-2 for more info.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec monitor(pid | {reg_name :: atom, node :: atom} | reg_name :: atom) :: reference
|
||||
def monitor(item) do
|
||||
:erlang.monitor(:process, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
If `monitor_ref` is a reference which the calling process
|
||||
obtained by calling `monitor/1`, this monitoring is turned off.
|
||||
If the monitoring is already turned off, nothing happens.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#demonitor-2 for more info.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec demonitor(reference) :: true
|
||||
@spec demonitor(reference, options :: [:flush | :info]) :: boolean
|
||||
def demonitor(monitor_ref, options \\ []) do
|
||||
:erlang.demonitor(monitor_ref, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of process identifiers corresponding to all the
|
||||
processes currently existing on the local node.
|
||||
|
||||
Note that a process that is exiting, exists but is not alive, i.e.,
|
||||
`alive?/1` will return `false` for a process that is exiting,
|
||||
but its process identifier will be part of the result returned.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#processes-0 for more info.
|
||||
"""
|
||||
@spec list :: [pid]
|
||||
def list do
|
||||
:erlang.processes()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a link between the calling process and another process
|
||||
(or port) `pid`, if there is not such a link already.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#link-1 for more info.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec link(pid | port) :: true
|
||||
def link(pid) do
|
||||
:erlang.link(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the link, if there is one, between the calling process and
|
||||
the process or port referred to by `pid`. Returns `true` and does not
|
||||
fail, even if there is no link or `id` does not exist
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#unlink-1 for more info.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec unlink(pid | port) :: true
|
||||
def unlink(pid) do
|
||||
:erlang.unlink(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Associates the name with a pid or a port identifier. `name`, which must
|
||||
be an atom, can be used instead of the pid / port identifier with the
|
||||
`Kernel.send/2` function.
|
||||
|
||||
`Process.register/2` will fail with `ArgumentError` if the pid supplied
|
||||
is no longer alive, (check with `alive?/1`) or if the name is
|
||||
already registered (check with `whereis/1`).
|
||||
"""
|
||||
@spec register(pid | port, atom) :: true
|
||||
def register(pid, name) when not name in [nil, false, true] do
|
||||
:erlang.register(name, pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the registered name, associated with a pid or a port identifier.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#unregister-1 for more info.
|
||||
"""
|
||||
@spec unregister(atom) :: true
|
||||
def unregister(name) do
|
||||
:erlang.unregister(name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid or port identifier with the registered name.
|
||||
Returns `nil` if the name is not registered.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#whereis-1 for more info.
|
||||
"""
|
||||
@spec whereis(atom) :: pid | port | nil
|
||||
def whereis(name) do
|
||||
nillify :erlang.whereis(name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the pid of the group leader for the process which evaluates the function.
|
||||
"""
|
||||
@spec group_leader :: pid
|
||||
def group_leader do
|
||||
:erlang.group_leader
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the group leader of `pid` to `leader`. Typically, this is used when a processes
|
||||
started from a certain shell should have a group leader other than `:init`.
|
||||
"""
|
||||
@spec group_leader(pid, leader :: pid) :: true
|
||||
def group_leader(pid, leader) do
|
||||
:erlang.group_leader(leader, pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of names which have been registered using `register/2`.
|
||||
"""
|
||||
@spec registered :: [atom]
|
||||
def registered do
|
||||
:erlang.registered()
|
||||
end
|
||||
|
||||
@typep process_flag :: :trap_exit | :error_handler | :min_heap_size |
|
||||
:min_bin_vheap_size | :priority | :save_calls |
|
||||
:sensitive
|
||||
@doc """
|
||||
Sets certain flags for the process which calls this function.
|
||||
Returns the old value of the flag.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_flag-2 for more info.
|
||||
"""
|
||||
@spec flag(process_flag, term) :: term
|
||||
def flag(flag, value) do
|
||||
:erlang.process_flag(flag, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets certain flags for the process `pid`, in the same manner as `flag/2`.
|
||||
Returns the old value of the flag. The allowed values for `flag` are
|
||||
only a subset of those allowed in `flag/2`, namely: `save_calls`.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_flag-3 for more info.
|
||||
"""
|
||||
@spec flag(pid, :save_calls, non_neg_integer) :: non_neg_integer
|
||||
def flag(pid, flag, value) do
|
||||
:erlang.process_flag(pid, flag, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about the process identified by `pid` or `nil` if the process
|
||||
is not alive.
|
||||
Use this only for debugging information.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_info-1 for more info.
|
||||
"""
|
||||
@spec info(pid) :: Keyword.t
|
||||
def info(pid) do
|
||||
nillify :erlang.process_info(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about the process identified by `pid`
|
||||
or `nil` if the process is not alive.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_info-2 for more info.
|
||||
"""
|
||||
@spec info(pid, atom) :: {atom, term} | nil
|
||||
def info(pid, spec)
|
||||
|
||||
def info(pid, :registered_name) do
|
||||
case :erlang.process_info(pid, :registered_name) do
|
||||
:undefined -> nil
|
||||
[] -> {:registered_name, []}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def info(pid, spec) when is_atom(spec) do
|
||||
nillify :erlang.process_info(pid, spec)
|
||||
end
|
||||
|
||||
@compile {:inline, nillify: 1}
|
||||
defp nillify(:undefined), do: nil
|
||||
defp nillify(other), do: other
|
||||
end
|
||||
@@ -1,639 +0,0 @@
|
||||
defmodule Protocol do
|
||||
@moduledoc """
|
||||
Functions for working with protocols.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Defines a new protocol function.
|
||||
|
||||
Protocols do not allow functions to be defined directly, instead, the
|
||||
regular `Kernel.def/*` macros are replaced by this macro which
|
||||
defines the protocol functions with the appropriate callbacks.
|
||||
"""
|
||||
defmacro def(signature)
|
||||
|
||||
defmacro def({_, _, args}) when args == [] or is_atom(args) do
|
||||
raise ArgumentError, "protocol functions expect at least one argument"
|
||||
end
|
||||
|
||||
defmacro def({name, _, args}) when is_atom(name) and is_list(args) do
|
||||
arity = length(args)
|
||||
|
||||
type_args = for _ <- :lists.seq(2, arity), do: quote(do: term)
|
||||
type_args = [quote(do: t) | type_args]
|
||||
|
||||
call_args = for i <- :lists.seq(2, arity),
|
||||
do: {String.to_atom(<<?x, i + 64>>), [], __MODULE__}
|
||||
call_args = [quote(do: t) | call_args]
|
||||
|
||||
quote do
|
||||
name = unquote(name)
|
||||
arity = unquote(arity)
|
||||
|
||||
@functions [{name, arity}|@functions]
|
||||
|
||||
# Generate a fake definition with the user
|
||||
# signature that will be used by docs
|
||||
Kernel.def unquote(name)(unquote_splicing(args))
|
||||
|
||||
# Generate the actual implementation
|
||||
Kernel.def unquote(name)(unquote_splicing(call_args)) do
|
||||
impl_for!(t).unquote(name)(unquote_splicing(call_args))
|
||||
end
|
||||
|
||||
# Convert the spec to callback if possible,
|
||||
# otherwise generate a dummy callback
|
||||
Protocol.__spec__?(__MODULE__, name, arity) ||
|
||||
@callback unquote(name)(unquote_splicing(type_args)) :: term
|
||||
end
|
||||
end
|
||||
|
||||
defmacro def(_) do
|
||||
raise ArgumentError, "invalid args for def inside defprotocol"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the given module is loaded and is protocol.
|
||||
|
||||
Returns `:ok` if so, otherwise raises ArgumentError.
|
||||
"""
|
||||
@spec assert_protocol!(module) :: :ok | no_return
|
||||
def assert_protocol!(module) do
|
||||
assert_protocol!(module, "")
|
||||
end
|
||||
|
||||
defp assert_protocol!(module, extra) do
|
||||
case Code.ensure_compiled(module) do
|
||||
{:module, ^module} -> :ok
|
||||
_ -> raise ArgumentError, "#{inspect module} is not available" <> extra
|
||||
end
|
||||
|
||||
try do
|
||||
module.__protocol__(:name)
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
raise ArgumentError, "#{inspect module} is not a protocol" <> extra
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the given module is loaded and is an implementation
|
||||
of the given protocol.
|
||||
|
||||
Returns `:ok` if so, otherwise raises ArgumentError.
|
||||
"""
|
||||
@spec assert_impl!(module, module) :: :ok | no_return
|
||||
def assert_impl!(protocol, base) do
|
||||
assert_impl!(protocol, base, "")
|
||||
end
|
||||
|
||||
defp assert_impl!(protocol, base, extra) do
|
||||
impl = Module.concat(protocol, base)
|
||||
|
||||
case Code.ensure_compiled(impl) do
|
||||
{:module, ^impl} -> :ok
|
||||
_ -> raise ArgumentError,
|
||||
"#{inspect impl} is not available" <> extra
|
||||
end
|
||||
|
||||
try do
|
||||
impl.__impl__(:protocol)
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
raise ArgumentError,
|
||||
"#{inspect impl} is not an implementation of a protocol" <> extra
|
||||
else
|
||||
^protocol ->
|
||||
:ok
|
||||
other ->
|
||||
raise ArgumentError,
|
||||
"expected #{inspect impl} to be an implementation of #{inspect protocol}, got: #{inspect other}" <> extra
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Derive the `protocol` for `module` with the given options.
|
||||
"""
|
||||
defmacro derive(protocol, module, options \\ []) do
|
||||
quote do
|
||||
module = unquote(module)
|
||||
Protocol.__derive__([{unquote(protocol), unquote(options)}], module, __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
## Consolidation
|
||||
|
||||
@doc """
|
||||
Extract all protocols from the given paths.
|
||||
|
||||
The paths can be either a char list or a string. Internally
|
||||
they are worked on as char lists, so passing them as lists
|
||||
avoid extra conversion.
|
||||
|
||||
Does not load any of the protocols.
|
||||
|
||||
## Examples
|
||||
|
||||
# Get Elixir's ebin and retrieve all protocols
|
||||
iex> path = :code.lib_dir(:elixir, :ebin)
|
||||
iex> mods = Protocol.extract_protocols([path])
|
||||
iex> Enumerable in mods
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_protocols([char_list | String.t]) :: [atom]
|
||||
def extract_protocols(paths) do
|
||||
extract_matching_by_attribute paths, 'Elixir.',
|
||||
fn module, attributes ->
|
||||
case attributes[:protocol] do
|
||||
[fallback_to_any: _, consolidated: _] -> module
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extract all types implemented for the given protocol from
|
||||
the given paths.
|
||||
|
||||
The paths can be either a char list or a string. Internally
|
||||
they are worked on as char lists, so passing them as lists
|
||||
avoid extra conversion.
|
||||
|
||||
Does not load any of the implementations.
|
||||
|
||||
## Examples
|
||||
|
||||
# Get Elixir's ebin and retrieve all protocols
|
||||
iex> path = :code.lib_dir(:elixir, :ebin)
|
||||
iex> mods = Protocol.extract_impls(Enumerable, [path])
|
||||
iex> List in mods
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_impls(module, [char_list | String.t]) :: [atom]
|
||||
def extract_impls(protocol, paths) when is_atom(protocol) do
|
||||
prefix = Atom.to_char_list(protocol) ++ '.'
|
||||
extract_matching_by_attribute paths, prefix, fn
|
||||
_mod, attributes ->
|
||||
case attributes[:impl] do
|
||||
[protocol: ^protocol, for: for] -> for
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_matching_by_attribute(paths, prefix, callback) do
|
||||
for path <- paths,
|
||||
file <- list_dir(path),
|
||||
mod = extract_from_file(path, file, prefix, callback),
|
||||
do: mod
|
||||
end
|
||||
|
||||
defp list_dir(path) when is_list(path) do
|
||||
case :file.list_dir(path) do
|
||||
{:ok, files} -> files
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp list_dir(path), do: list_dir(to_char_list(path))
|
||||
|
||||
defp extract_from_file(path, file, prefix, callback) do
|
||||
if :lists.prefix(prefix, file) and :filename.extension(file) == '.beam' do
|
||||
extract_from_beam(:filename.join(path, file), callback)
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_from_beam(file, callback) do
|
||||
case :beam_lib.chunks(file, [:attributes]) do
|
||||
{:ok, {module, [attributes: attributes]}} ->
|
||||
callback.(module, attributes)
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop if_ok(expr, call) do
|
||||
quote do
|
||||
case unquote(expr) do
|
||||
{:ok, var} -> unquote(Macro.pipe(quote(do: var), call, 0))
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the protocol was consolidated.
|
||||
"""
|
||||
@spec consolidated?(module) :: boolean
|
||||
def consolidated?(protocol) do
|
||||
protocol.__info__(:attributes)[:protocol][:consolidated]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a protocol and a list of implementations and
|
||||
consolidates the given protocol.
|
||||
|
||||
Consolidation happens by changing the protocol `impl_for`
|
||||
in the abstract format to have fast lookup rules. Usually
|
||||
the list of implementations to use during consolidation
|
||||
are retrieved with the help of `extract_impls/2`.
|
||||
|
||||
It returns the updated version of the protocol bytecode.
|
||||
A given bytecode or protocol implementation can be checked
|
||||
to be consolidated or not by analyzing the protocol
|
||||
attribute:
|
||||
|
||||
Protocol.consolidated?(Enumerable)
|
||||
|
||||
If the first element of the tuple is true, it means
|
||||
the protocol was consolidated.
|
||||
|
||||
This function does not load the protocol at any point
|
||||
nor loads the new bytecode for the compiled module.
|
||||
However each implementation must be available and
|
||||
it will be loaded.
|
||||
"""
|
||||
@spec consolidate(module, [module]) ::
|
||||
{:ok, binary} |
|
||||
{:error, :not_a_protocol} |
|
||||
{:error, :no_beam_info}
|
||||
def consolidate(protocol, types) when is_atom(protocol) do
|
||||
beam_protocol(protocol)
|
||||
|> if_ok(change_debug_info types)
|
||||
|> if_ok(compile)
|
||||
end
|
||||
|
||||
@docs_chunk 'ExDc'
|
||||
|
||||
defp beam_protocol(protocol) do
|
||||
chunk_ids = [:abstract_code, :attributes, @docs_chunk]
|
||||
opts = [:allow_missing_chunks]
|
||||
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
|
||||
{:ok, {^protocol, [{:abstract_code, {_raw, abstract_code}},
|
||||
{:attributes, attributes},
|
||||
{@docs_chunk, docs}]}} ->
|
||||
case attributes[:protocol] do
|
||||
[fallback_to_any: any, consolidated: _] ->
|
||||
{:ok, {protocol, any, abstract_code, docs}}
|
||||
_ ->
|
||||
{:error, :not_a_protocol}
|
||||
end
|
||||
_ ->
|
||||
{:error, :no_beam_info}
|
||||
end
|
||||
end
|
||||
|
||||
defp beam_file(module) when is_atom(module) do
|
||||
case :code.which(module) do
|
||||
atom when is_atom(atom) -> module
|
||||
file -> file
|
||||
end
|
||||
end
|
||||
|
||||
# Change the debug information to the optimized
|
||||
# impl_for/1 dispatch version.
|
||||
defp change_debug_info({protocol, any, code, docs}, types) do
|
||||
types = if any, do: types, else: List.delete(types, Any)
|
||||
all = [Any] ++ for {_guard, mod} <- builtin, do: mod
|
||||
structs = types -- all
|
||||
case change_impl_for(code, protocol, types, structs, false, []) do
|
||||
{:ok, ret} -> {:ok, {ret, docs}}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp change_impl_for([{:attribute, line, :protocol, opts}|t], protocol, types, structs, _, acc) do
|
||||
opts = [fallback_to_any: opts[:fallback_to_any], consolidated: true]
|
||||
change_impl_for(t, protocol, types, structs, true,
|
||||
[{:attribute, line, :protocol, opts}|acc])
|
||||
end
|
||||
|
||||
defp change_impl_for([{:function, line, :impl_for, 1, _}|t], protocol, types, structs, is_protocol, acc) do
|
||||
fallback = if Any in types, do: load_impl(protocol, Any), else: nil
|
||||
|
||||
clauses = for {guard, mod} <- builtin,
|
||||
mod in types,
|
||||
do: builtin_clause_for(mod, guard, protocol, line)
|
||||
|
||||
clauses = [struct_clause_for(line)|clauses] ++
|
||||
[fallback_clause_for(fallback, protocol, line)]
|
||||
|
||||
change_impl_for(t, protocol, types, structs, is_protocol,
|
||||
[{:function, line, :impl_for, 1, clauses}|acc])
|
||||
end
|
||||
|
||||
defp change_impl_for([{:function, line, :struct_impl_for, 1, _}|t], protocol, types, structs, is_protocol, acc) do
|
||||
fallback = if Any in types, do: load_impl(protocol, Any), else: nil
|
||||
clauses = for struct <- structs, do: each_struct_clause_for(struct, protocol, line)
|
||||
clauses = clauses ++ [fallback_clause_for(fallback, protocol, line)]
|
||||
|
||||
change_impl_for(t, protocol, types, structs, is_protocol,
|
||||
[{:function, line, :struct_impl_for, 1, clauses}|acc])
|
||||
end
|
||||
|
||||
defp change_impl_for([h|t], protocol, info, types, is_protocol, acc) do
|
||||
change_impl_for(t, protocol, info, types, is_protocol, [h|acc])
|
||||
end
|
||||
|
||||
defp change_impl_for([], protocol, _info, _types, is_protocol, acc) do
|
||||
if is_protocol do
|
||||
{:ok, {protocol, Enum.reverse(acc)}}
|
||||
else
|
||||
{:error, :not_a_protocol}
|
||||
end
|
||||
end
|
||||
|
||||
defp builtin_clause_for(mod, guard, protocol, line) do
|
||||
{:clause, line,
|
||||
[{:var, line, :x}],
|
||||
[[{:call, line,
|
||||
{:remote, line, {:atom, line, :erlang}, {:atom, line, guard}},
|
||||
[{:var, line, :x}],
|
||||
}]],
|
||||
[{:atom, line, load_impl(protocol, mod)}]}
|
||||
end
|
||||
|
||||
defp struct_clause_for(line) do
|
||||
{:clause, line,
|
||||
[{:map, line, [
|
||||
{:map_field_exact, line, {:atom, line, :__struct__}, {:var, line, :x}}
|
||||
]}],
|
||||
[[{:call, line,
|
||||
{:remote, line, {:atom, line, :erlang}, {:atom, line, :is_atom}},
|
||||
[{:var, line, :x}],
|
||||
}]],
|
||||
[{:call, line,
|
||||
{:atom, line, :struct_impl_for},
|
||||
[{:var, line, :x}]}]}
|
||||
end
|
||||
|
||||
defp each_struct_clause_for(other, protocol, line) do
|
||||
{:clause, line, [{:atom, line, other}], [],
|
||||
[{:atom, line, load_impl(protocol, other)}]}
|
||||
end
|
||||
|
||||
defp fallback_clause_for(value, _protocol, line) do
|
||||
{:clause, line, [{:var, line, :_}], [],
|
||||
[{:atom, line, value}]}
|
||||
end
|
||||
|
||||
defp load_impl(protocol, for) do
|
||||
Module.concat(protocol, for).__impl__(:target)
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
defp compile({{protocol, code}, docs}) do
|
||||
opts = if Code.compiler_options[:debug_info], do: [:debug_info], else: []
|
||||
{:ok, ^protocol, binary, _warnings} = :compile.forms(code, [:return|opts])
|
||||
unless docs == :missing_chunk do
|
||||
binary = :elixir_module.add_beam_chunk(binary, @docs_chunk, docs)
|
||||
end
|
||||
{:ok, binary}
|
||||
end
|
||||
|
||||
## Definition callbacks
|
||||
|
||||
@doc false
|
||||
def __protocol__(name, [do: block]) do
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
# We don't allow function definition inside protocols
|
||||
import Kernel, except: [
|
||||
defmacrop: 1, defmacrop: 2, defmacro: 1, defmacro: 2,
|
||||
defp: 1, defp: 2, def: 1, def: 2
|
||||
]
|
||||
|
||||
# Import the new dsl that holds the new def
|
||||
import Protocol, only: [def: 1]
|
||||
|
||||
# Compile with debug info for consolidation
|
||||
@compile :debug_info
|
||||
|
||||
# Set up a clear slate to store defined functions
|
||||
@functions []
|
||||
@fallback_to_any false
|
||||
|
||||
# Invoke the user given block
|
||||
unquote(block)
|
||||
|
||||
# Finalize expansion
|
||||
unquote(after_defprotocol)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp after_defprotocol do
|
||||
quote bind_quoted: [builtin: builtin] do
|
||||
@spec impl_for(term) :: atom() | nil
|
||||
Kernel.def impl_for(data)
|
||||
|
||||
# Define the implementation for structs.
|
||||
#
|
||||
# It simply delegates to struct_impl_for which is then
|
||||
# optimized during protocol consolidation.
|
||||
Kernel.def impl_for(%{__struct__: struct}) when :erlang.is_atom(struct) do
|
||||
struct_impl_for(struct)
|
||||
end
|
||||
|
||||
# Define the implementation for builtins.
|
||||
for {guard, mod} <- builtin do
|
||||
target = Module.concat(__MODULE__, mod)
|
||||
|
||||
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
|
||||
case impl_for?(unquote(target)) do
|
||||
true -> unquote(target).__impl__(:target)
|
||||
false -> any_impl_for
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec impl_for!(term) :: atom() | no_return()
|
||||
Kernel.def impl_for!(data) do
|
||||
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
|
||||
end
|
||||
|
||||
# Internal handler for Any
|
||||
if @fallback_to_any do
|
||||
Kernel.defp any_impl_for do
|
||||
case impl_for?(__MODULE__.Any) do
|
||||
true -> __MODULE__.Any.__impl__(:target)
|
||||
false -> nil
|
||||
end
|
||||
end
|
||||
else
|
||||
Kernel.defp any_impl_for, do: nil
|
||||
end
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
target = Module.concat(__MODULE__, struct)
|
||||
case impl_for?(target) do
|
||||
true -> target.__impl__(:target)
|
||||
false -> any_impl_for
|
||||
end
|
||||
end
|
||||
|
||||
# Check if compilation is available internally
|
||||
Kernel.defp impl_for?(target) do
|
||||
Code.ensure_compiled?(target) and
|
||||
function_exported?(target, :__impl__, 1)
|
||||
end
|
||||
|
||||
# Inline any and struct implementations
|
||||
@compile {:inline, any_impl_for: 0, struct_impl_for: 1, impl_for?: 1}
|
||||
|
||||
unless Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
|
||||
@type t :: term
|
||||
end
|
||||
|
||||
# Store information as an attribute so it
|
||||
# can be read without loading the module.
|
||||
Module.register_attribute(__MODULE__, :protocol, persist: true)
|
||||
@protocol [fallback_to_any: !!@fallback_to_any, consolidated: false]
|
||||
|
||||
@doc false
|
||||
@spec __protocol__(:name) :: __MODULE__
|
||||
@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@functions))
|
||||
Kernel.def __protocol__(:name), do: __MODULE__
|
||||
Kernel.def __protocol__(:functions), do: unquote(:lists.sort(@functions))
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __functions_spec__([]),
|
||||
do: []
|
||||
def __functions_spec__([h|t]),
|
||||
do: [:lists.foldl(&{:|, [], [&1, &2]}, h, t), quote(do: ...)]
|
||||
|
||||
@doc false
|
||||
def __impl__(protocol, opts) do
|
||||
do_defimpl(protocol, :lists.keysort(1, opts))
|
||||
end
|
||||
|
||||
defp do_defimpl(protocol, [do: block, for: for]) when is_list(for) do
|
||||
for f <- for, do: do_defimpl(protocol, [do: block, for: f])
|
||||
end
|
||||
|
||||
defp do_defimpl(protocol, [do: block, for: for]) do
|
||||
# Unquote the implementation just later
|
||||
# when all variables will already be injected
|
||||
# into the module body.
|
||||
__impl__ =
|
||||
quote unquote: false do
|
||||
@doc false
|
||||
@spec __impl__(:for) :: unquote(for)
|
||||
@spec __impl__(:target) :: __MODULE__
|
||||
@spec __impl__(:protocol) :: unquote(protocol)
|
||||
def __impl__(:for), do: unquote(for)
|
||||
def __impl__(:target), do: __MODULE__
|
||||
def __impl__(:protocol), do: unquote(protocol)
|
||||
end
|
||||
|
||||
quote do
|
||||
protocol = unquote(protocol)
|
||||
for = unquote(for)
|
||||
name = Module.concat(protocol, for)
|
||||
|
||||
Protocol.assert_protocol!(protocol)
|
||||
|
||||
defmodule name do
|
||||
@behaviour protocol
|
||||
@protocol protocol
|
||||
@for for
|
||||
|
||||
unquote(block)
|
||||
|
||||
Module.register_attribute(__MODULE__, :impl, persist: true)
|
||||
@impl [protocol: @protocol, for: @for]
|
||||
|
||||
unquote(__impl__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
|
||||
struct =
|
||||
if for == env.module do
|
||||
Module.get_attribute(for, :struct) ||
|
||||
raise "struct is not defined for #{inspect for}"
|
||||
else
|
||||
for.__struct__
|
||||
end
|
||||
|
||||
:lists.foreach(fn
|
||||
proto when is_atom(proto) ->
|
||||
derive(proto, for, struct, [], env)
|
||||
{proto, opts} when is_atom(proto) ->
|
||||
derive(proto, for, struct, opts, env)
|
||||
end, :lists.flatten(derives))
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp derive(protocol, for, struct, opts, env) do
|
||||
extra = ", cannot derive #{inspect protocol} for #{inspect for}"
|
||||
assert_protocol!(protocol, extra)
|
||||
assert_impl!(protocol, Map, extra)
|
||||
|
||||
# Clean up variables from eval context
|
||||
env = %{env | vars: [], export_vars: nil}
|
||||
args = [for, struct, opts]
|
||||
impl = Module.concat(protocol, Map)
|
||||
|
||||
:elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn
|
||||
mod, fun, args ->
|
||||
if function_exported?(mod, fun, length(args)) do
|
||||
apply(mod, fun, args)
|
||||
else
|
||||
Module.create(Module.concat(protocol, for), quote do
|
||||
Module.register_attribute(__MODULE__, :impl, persist: true)
|
||||
@impl [protocol: unquote(protocol), for: unquote(for)]
|
||||
|
||||
@doc false
|
||||
@spec __impl__(:target) :: unquote(impl)
|
||||
@spec __impl__(:protocol) :: unquote(protocol)
|
||||
@spec __impl__(:for) :: unquote(for)
|
||||
def __impl__(:target), do: unquote(impl)
|
||||
def __impl__(:protocol), do: unquote(protocol)
|
||||
def __impl__(:for), do: unquote(for)
|
||||
end, Macro.Env.location(env))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __spec__?(module, name, arity) do
|
||||
signature = {name, arity}
|
||||
specs = Module.get_attribute(module, :spec)
|
||||
|
||||
found =
|
||||
for {:spec, expr, caller} <- specs,
|
||||
Kernel.Typespec.spec_to_signature(expr) == signature do
|
||||
Kernel.Typespec.define_spec(:callback, expr, caller)
|
||||
true
|
||||
end
|
||||
|
||||
found != []
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp builtin do
|
||||
[is_tuple: Tuple,
|
||||
is_atom: Atom,
|
||||
is_list: List,
|
||||
is_map: Map,
|
||||
is_bitstring: BitString,
|
||||
is_integer: Integer,
|
||||
is_float: Float,
|
||||
is_function: Function,
|
||||
is_pid: PID,
|
||||
is_port: Port,
|
||||
is_reference: Reference]
|
||||
end
|
||||
end
|
||||
@@ -1,125 +0,0 @@
|
||||
defmodule Range do
|
||||
@moduledoc """
|
||||
Defines a Range.
|
||||
|
||||
A Range is represented internally as a struct. However,
|
||||
the most common form of creating and matching on ranges
|
||||
is via the `../2` macro, auto-imported from Kernel:
|
||||
|
||||
iex> range = 1..3
|
||||
1..3
|
||||
iex> first .. last = range
|
||||
iex> first
|
||||
1
|
||||
iex> last
|
||||
3
|
||||
|
||||
"""
|
||||
|
||||
defstruct first: nil, last: nil
|
||||
|
||||
@type t :: %Range{}
|
||||
@type t(first, last) :: %Range{first: first, last: last}
|
||||
|
||||
@doc """
|
||||
Creates a new range.
|
||||
"""
|
||||
def new(first, last) do
|
||||
%Range{first: first, last: last}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the given argument is a range.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Range.range?(1..3)
|
||||
true
|
||||
|
||||
iex> Range.range?(0)
|
||||
false
|
||||
|
||||
"""
|
||||
def range?(%Range{}), do: true
|
||||
def range?(_), do: false
|
||||
end
|
||||
|
||||
defprotocol Range.Iterator do
|
||||
@moduledoc """
|
||||
A protocol used for iterating range elements.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns the function that calculates the next item.
|
||||
"""
|
||||
def next(first, range)
|
||||
|
||||
@doc """
|
||||
Count how many items are in the range.
|
||||
"""
|
||||
def count(first, range)
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: Range do
|
||||
def reduce(first .. last = range, acc, fun) do
|
||||
reduce(first, last, acc, fun, Range.Iterator.next(first, range), last >= first)
|
||||
end
|
||||
|
||||
defp reduce(_x, _y, {:halt, acc}, _fun, _next, _up) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp reduce(x, y, {:suspend, acc}, fun, next, up) do
|
||||
{:suspended, acc, &reduce(x, y, &1, fun, next, up)}
|
||||
end
|
||||
|
||||
defp reduce(x, y, {:cont, acc}, fun, next, true) when x <= y do
|
||||
reduce(next.(x), y, fun.(x, acc), fun, next, true)
|
||||
end
|
||||
|
||||
defp reduce(x, y, {:cont, acc}, fun, next, false) when x >= y do
|
||||
reduce(next.(x), y, fun.(x, acc), fun, next, false)
|
||||
end
|
||||
|
||||
defp reduce(_, _, {:cont, acc}, _fun, _next, _up) do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
def member?(first .. last, value) do
|
||||
if first <= last do
|
||||
{:ok, first <= value and value <= last}
|
||||
else
|
||||
{:ok, last <= value and value <= first}
|
||||
end
|
||||
end
|
||||
|
||||
def count(first .. _ = range) do
|
||||
{:ok, Range.Iterator.count(first, range)}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Range.Iterator, for: Integer do
|
||||
def next(first, _ .. last) when is_integer(last) do
|
||||
if last >= first do
|
||||
&(&1 + 1)
|
||||
else
|
||||
&(&1 - 1)
|
||||
end
|
||||
end
|
||||
|
||||
def count(first, _ .. last) when is_integer(last) do
|
||||
if last >= first do
|
||||
last - first + 1
|
||||
else
|
||||
first - last + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Range do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(first .. last, opts) do
|
||||
concat [to_doc(first, opts), "..", to_doc(last, opts)]
|
||||
end
|
||||
end
|
||||
@@ -1,335 +0,0 @@
|
||||
defmodule Record do
|
||||
@moduledoc """
|
||||
Module to work, define and import records.
|
||||
|
||||
Records are simply tuples where the first element is an atom:
|
||||
|
||||
iex> Record.is_record {User, "john", 27}
|
||||
true
|
||||
|
||||
This module provides conveniences for working with records at
|
||||
compilation time, where compile-time field names are used to
|
||||
manipulate the tuples, providing fast operations on top of
|
||||
the tuples compact structure.
|
||||
|
||||
In Elixir, records are used mostly in two situations:
|
||||
|
||||
1. to work with short, internal data
|
||||
2. to interface with Erlang records
|
||||
|
||||
The macros `defrecord/3` and `defrecordp/3` can be used to create
|
||||
records while `extract/2` can be used to extract records from Erlang
|
||||
files.
|
||||
|
||||
## Types
|
||||
|
||||
Types can be defined for tuples with the `record/2` macro (only available
|
||||
in typespecs). Like with the generated record macros it will expand to
|
||||
a tuple.
|
||||
|
||||
defmodule MyModule do
|
||||
require Record
|
||||
Record.defrecord :user name: "john", age: 25
|
||||
|
||||
@type user :: record(:user, name: String.t, age: integer)
|
||||
# expands to: `@type user :: {:user, String.t, integer}`
|
||||
end
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Extracts record information from an Erlang file.
|
||||
|
||||
Returns a quoted expression containing the fields as a list
|
||||
of tuples. It expects the record name to be an atom and the
|
||||
library path to be a string at expansion time.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
|
||||
[size: :undefined, type: :undefined, access: :undefined, atime: :undefined,
|
||||
mtime: :undefined, ctime: :undefined, mode: :undefined, links: :undefined,
|
||||
major_device: :undefined, minor_device: :undefined, inode: :undefined,
|
||||
uid: :undefined, gid: :undefined]
|
||||
|
||||
"""
|
||||
defmacro extract(name, opts) when is_atom(name) and is_list(opts) do
|
||||
Macro.escape Record.Extractor.extract(name, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the given `data` is a record of `kind`.
|
||||
|
||||
This is implemented as a macro so it can be used in guard clauses.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> record = {User, "john", 27}
|
||||
iex> Record.is_record(record, User)
|
||||
true
|
||||
|
||||
"""
|
||||
defmacro is_record(data, kind) do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true ->
|
||||
quote do
|
||||
is_tuple(unquote(data)) and tuple_size(unquote(data)) > 0
|
||||
and :erlang.element(1, unquote(data)) == unquote(kind)
|
||||
end
|
||||
false ->
|
||||
quote do
|
||||
result = unquote(data)
|
||||
is_tuple(result) and tuple_size(result) > 0
|
||||
and :erlang.element(1, result) == unquote(kind)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the given `data` is a record.
|
||||
|
||||
This is implemented as a macro so it can be used in guard clauses.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> record = {User, "john", 27}
|
||||
iex> Record.is_record(record)
|
||||
true
|
||||
iex> tuple = {}
|
||||
iex> Record.is_record(tuple)
|
||||
false
|
||||
|
||||
"""
|
||||
defmacro is_record(data) do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true ->
|
||||
quote do
|
||||
is_tuple(unquote(data)) and tuple_size(unquote(data)) > 0
|
||||
and is_atom(:erlang.element(1, unquote(data)))
|
||||
end
|
||||
false ->
|
||||
quote do
|
||||
result = unquote(data)
|
||||
is_tuple(result) and tuple_size(result) > 0
|
||||
and is_atom(:erlang.element(1, result))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a set of macros to create and access a record.
|
||||
|
||||
The macros are going to have `name`, a tag (which defaults)
|
||||
to the name if none is given, and a set of fields given by
|
||||
`kv`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule User do
|
||||
require Record
|
||||
Record.defrecord :user, [name: "meg", age: "25"]
|
||||
end
|
||||
|
||||
In the example above, a set of macros named `user` but with different
|
||||
arities will be defined to manipulate the underlying record:
|
||||
|
||||
# To create records
|
||||
record = user() #=> {:user, "meg", 25}
|
||||
record = user(age: 26) #=> {:user, "meg", 26}
|
||||
|
||||
# To get a field from the record
|
||||
user(record, :name) #=> "meg"
|
||||
|
||||
# To update the record
|
||||
user(record, age: 26) #=> {:user, "meg", 26}
|
||||
|
||||
# Convert a record to a keyword list
|
||||
user(record) #=> [name: "meg", age: 26]
|
||||
|
||||
By default, Elixir uses the record name as the first element of
|
||||
the tuple (the tag). But it can be changed to something else:
|
||||
|
||||
defmodule User do
|
||||
require Record
|
||||
Record.defrecord :user, User, name: nil
|
||||
end
|
||||
|
||||
require User
|
||||
User.user() #=> {User, nil}
|
||||
|
||||
"""
|
||||
defmacro defrecord(name, tag \\ nil, kv) do
|
||||
quote bind_quoted: [name: name, tag: tag, kv: kv] do
|
||||
tag = tag || name
|
||||
fields = Record.__fields__(:defrecord, kv)
|
||||
|
||||
defmacro(unquote(name)(args \\ [])) do
|
||||
Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
|
||||
end
|
||||
|
||||
defmacro(unquote(name)(record, args)) do
|
||||
Record.__access__(unquote(tag), unquote(fields), record, args, __CALLER__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `defrecord/3` but generates private macros.
|
||||
"""
|
||||
defmacro defrecordp(name, tag \\ nil, kv) do
|
||||
quote bind_quoted: [name: name, tag: tag, kv: kv] do
|
||||
tag = tag || name
|
||||
fields = Record.__fields__(:defrecordp, kv)
|
||||
|
||||
defmacrop(unquote(name)(args \\ [])) do
|
||||
Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
|
||||
end
|
||||
|
||||
defmacrop(unquote(name)(record, args)) do
|
||||
Record.__access__(unquote(tag), unquote(fields), record, args, __CALLER__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Normalizes of record fields to have default values.
|
||||
@doc false
|
||||
def __fields__(type, fields) do
|
||||
:lists.map(fn
|
||||
{key, val} when is_atom(key) ->
|
||||
try do
|
||||
Macro.escape(val)
|
||||
rescue
|
||||
e in [ArgumentError] ->
|
||||
raise ArgumentError, "invalid value for record field #{key}, " <> Exception.message(e)
|
||||
else
|
||||
val -> {key, val}
|
||||
end
|
||||
key when is_atom(key) ->
|
||||
{key, nil}
|
||||
other ->
|
||||
raise ArgumentError, "#{type} fields must be atoms, got: #{inspect other}"
|
||||
end, fields)
|
||||
end
|
||||
|
||||
# Callback invoked from record/0 and record/1 macros.
|
||||
@doc false
|
||||
def __access__(atom, fields, args, caller) do
|
||||
cond do
|
||||
is_atom(args) ->
|
||||
index(atom, fields, args)
|
||||
Keyword.keyword?(args) ->
|
||||
create(atom, fields, args, caller)
|
||||
true ->
|
||||
case Macro.expand(args, caller) do
|
||||
{:{}, _, [^atom|list]} when length(list) == length(fields) ->
|
||||
record = List.to_tuple([atom|list])
|
||||
Macro.escape(Record.__keyword__(atom, fields, record))
|
||||
{^atom, arg} when length(fields) == 1 ->
|
||||
Macro.escape(Record.__keyword__(atom, fields, {atom, arg}))
|
||||
_ ->
|
||||
quote do: Record.__keyword__(unquote(atom), unquote(fields), unquote(args))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Callback invoked from the record/2 macro.
|
||||
@doc false
|
||||
def __access__(atom, fields, record, args, caller) do
|
||||
cond do
|
||||
is_atom(args) ->
|
||||
get(atom, fields, record, args)
|
||||
Keyword.keyword?(args) ->
|
||||
update(atom, fields, record, args, caller)
|
||||
true ->
|
||||
msg = "expected arguments to be a compile time atom or keywords, got: #{Macro.to_string args}"
|
||||
raise ArgumentError, msg
|
||||
end
|
||||
end
|
||||
|
||||
# Gets the index of field.
|
||||
defp index(atom, fields, field) do
|
||||
if index = find_index(fields, field, 0) do
|
||||
index - 1 # Convert to Elixir index
|
||||
else
|
||||
raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect field}"
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a new record with the given default fields and keyword values.
|
||||
defp create(atom, fields, keyword, caller) do
|
||||
in_match = Macro.Env.in_match?(caller)
|
||||
|
||||
{match, remaining} =
|
||||
Enum.map_reduce(fields, keyword, fn({field, default}, each_keyword) ->
|
||||
new_fields =
|
||||
case Keyword.has_key?(each_keyword, field) do
|
||||
true -> Keyword.get(each_keyword, field)
|
||||
false ->
|
||||
case in_match do
|
||||
true -> {:_, [], nil}
|
||||
false -> Macro.escape(default)
|
||||
end
|
||||
end
|
||||
|
||||
{new_fields, Keyword.delete(each_keyword, field)}
|
||||
end)
|
||||
|
||||
case remaining do
|
||||
[] ->
|
||||
{:{}, [], [atom|match]}
|
||||
_ ->
|
||||
keys = for {key, _} <- remaining, do: key
|
||||
raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect hd(keys)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Updates a record given by var with the given keyword.
|
||||
defp update(atom, fields, var, keyword, caller) do
|
||||
if Macro.Env.in_match?(caller) do
|
||||
raise ArgumentError, "cannot invoke update style macro inside match"
|
||||
end
|
||||
|
||||
Enum.reduce keyword, var, fn({key, value}, acc) ->
|
||||
index = find_index(fields, key, 0)
|
||||
if index do
|
||||
quote do
|
||||
:erlang.setelement(unquote(index), unquote(acc), unquote(value))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect key}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Gets a record key from the given var.
|
||||
defp get(atom, fields, var, key) do
|
||||
index = find_index(fields, key, 0)
|
||||
if index do
|
||||
quote do
|
||||
:erlang.element(unquote(index), unquote(var))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect key}"
|
||||
end
|
||||
end
|
||||
|
||||
defp find_index([{k, _}|_], k, i), do: i + 2
|
||||
defp find_index([{_, _}|t], k, i), do: find_index(t, k, i + 1)
|
||||
defp find_index([], _k, _i), do: nil
|
||||
|
||||
# Returns a keyword list of the record
|
||||
@doc false
|
||||
def __keyword__(atom, fields, record) do
|
||||
if is_record(record, atom) do
|
||||
[_tag|values] = Tuple.to_list(record)
|
||||
join_keyword(fields, values, [])
|
||||
else
|
||||
msg = "expected argument to be a literal atom, literal keyword or a #{inspect atom} record, got runtime: #{inspect record}"
|
||||
raise ArgumentError, msg
|
||||
end
|
||||
end
|
||||
|
||||
defp join_keyword([{field, _default}|fields], [value|values], acc),
|
||||
do: join_keyword(fields, values, [{field, value}| acc])
|
||||
defp join_keyword([], [], acc),
|
||||
do: :lists.reverse(acc)
|
||||
end
|
||||
@@ -1,92 +0,0 @@
|
||||
defmodule Record.Extractor do
|
||||
@moduledoc false
|
||||
|
||||
# Retrieve a record definition from an Erlang file using
|
||||
# the same lookup as the *include* attribute from Erlang modules.
|
||||
def extract(name, from: file) when is_binary(file) do
|
||||
file = String.to_char_list(file)
|
||||
|
||||
realfile =
|
||||
case :code.where_is_file(file) do
|
||||
:non_existing -> file
|
||||
realfile -> realfile
|
||||
end
|
||||
|
||||
extract_record(name, realfile)
|
||||
end
|
||||
|
||||
# Retrieve a record definition from an Erlang file using
|
||||
# the same lookup as the *include_lib* attribute from Erlang modules.
|
||||
def extract(name, from_lib: file) when is_binary(file) do
|
||||
[app|path] = :filename.split(String.to_char_list(file))
|
||||
|
||||
case :code.lib_dir(List.to_atom(app)) do
|
||||
{:error, _} ->
|
||||
raise ArgumentError, "lib file #{file} could not be found"
|
||||
libpath ->
|
||||
extract_record name, :filename.join([libpath|path])
|
||||
end
|
||||
end
|
||||
|
||||
# Retrieve the record with the given name from the given file
|
||||
defp extract_record(name, file) do
|
||||
form = read_file(file)
|
||||
records = extract_records(form)
|
||||
if record = List.keyfind(records, name, 0) do
|
||||
parse_record(record, form)
|
||||
else
|
||||
raise ArgumentError, "no record #{name} found at #{file}"
|
||||
end
|
||||
end
|
||||
|
||||
# Parse the given file and extract all existent records.
|
||||
defp extract_records(form) do
|
||||
for {:attribute, _, :record, record} <- form, do: record
|
||||
end
|
||||
|
||||
# Read a file and return its abstract syntax form that also
|
||||
# includes record and other preprocessor modules. This is done
|
||||
# by using Erlang's epp_dodger.
|
||||
defp read_file(file) do
|
||||
case :epp_dodger.quick_parse_file(file) do
|
||||
{:ok, form} ->
|
||||
form
|
||||
other ->
|
||||
raise "error parsing file #{file}, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a tuple with name and fields and returns a
|
||||
# list of tuples where the first element is the field
|
||||
# and the second is its default value.
|
||||
defp parse_record({_name, fields}, form) do
|
||||
cons = List.foldr fields, {nil, 0}, fn f, acc ->
|
||||
{:cons, 0, parse_field(f), acc}
|
||||
end
|
||||
eval_record(cons, form)
|
||||
end
|
||||
|
||||
defp parse_field({:typed_record_field, record_field, _type}) do
|
||||
parse_field(record_field)
|
||||
end
|
||||
|
||||
defp parse_field({:record_field, _, key}) do
|
||||
{:tuple, 0, [key, {:atom, 0, :undefined}]}
|
||||
end
|
||||
|
||||
defp parse_field({:record_field, _, key, value}) do
|
||||
{:tuple, 0, [key, value]}
|
||||
end
|
||||
|
||||
defp eval_record(cons, form) do
|
||||
form = form ++
|
||||
[ {:function, 0, :hello, 0, [
|
||||
{:clause, 0, [], [], [ cons ]} ]} ]
|
||||
|
||||
{:function, 0, :hello, 0, [
|
||||
{:clause, 0, [], [], [ record_ast ]} ]} = :erl_expand_records.module(form, []) |> List.last
|
||||
|
||||
{:value, record, _} = :erl_eval.expr(record_ast, [])
|
||||
record
|
||||
end
|
||||
end
|
||||
@@ -1,625 +0,0 @@
|
||||
defmodule Regex do
|
||||
@moduledoc ~S"""
|
||||
Regular expressions for Elixir built on top of Erlang's `re` module.
|
||||
|
||||
As the `re` module, Regex is based on PCRE
|
||||
(Perl Compatible Regular Expressions). More information can be
|
||||
found in the [`re` documentation](http://www.erlang.org/doc/man/re.html).
|
||||
|
||||
Regular expressions in Elixir can be created using `Regex.compile!/2`
|
||||
or using the special form with [`~r`](Kernel.html#sigil_r/2):
|
||||
|
||||
# A simple regular expressions that matches foo anywhere in the string
|
||||
~r/foo/
|
||||
|
||||
# A regular expression with case insensitive and unicode options
|
||||
~r/foo/iu
|
||||
|
||||
A Regex is represented internally as the `Regex` struct. Therefore,
|
||||
`%Regex{}` can be used whenever there is a need to match on them.
|
||||
|
||||
## Modifiers
|
||||
|
||||
The modifiers available when creating a Regex are:
|
||||
|
||||
* `unicode` (u) - enables unicode specific patterns like `\p` and changes
|
||||
modifiers like `\w`, `\W`, `\s` and friends to also match on unicode.
|
||||
It expects valid unicode strings to be given on match
|
||||
|
||||
* `caseless` (i) - add case insensitivity
|
||||
|
||||
* `dotall` (s) - causes dot to match newlines and also set newline to
|
||||
anycrlf; the new line setting can be overridden by setting `(*CR)` or
|
||||
`(*LF)` or `(*CRLF)` or `(*ANY)` according to re documentation
|
||||
|
||||
* `multiline` (m) - causes `^` and `$` to mark the beginning and end of
|
||||
each line; use `\A` and `\z` to match the end or beginning of the string
|
||||
|
||||
* `extended` (x) - whitespace characters are ignored except when escaped
|
||||
and allow `#` to delimit comments
|
||||
|
||||
* `firstline` (f) - forces the unanchored pattern to match before or at the
|
||||
first newline, though the matched text may continue over the newline
|
||||
|
||||
* `ungreedy` (r) - inverts the "greediness" of the regexp
|
||||
|
||||
The options not available are:
|
||||
|
||||
* `anchored` - not available, use `^` or `\A` instead
|
||||
* `dollar_endonly` - not available, use `\z` instead
|
||||
* `no_auto_capture` - not available, use `?:` instead
|
||||
* `newline` - not available, use `(*CR)` or `(*LF)` or `(*CRLF)` or
|
||||
`(*ANYCRLF)` or `(*ANY)` at the beginning of the regexp according to the
|
||||
re documentation
|
||||
|
||||
## Captures
|
||||
|
||||
Many functions in this module allows what to capture in a regex
|
||||
match via the `:capture` option. The supported values are:
|
||||
|
||||
* `:all` - all captured subpatterns including the complete matching string
|
||||
(this is the default)
|
||||
|
||||
* `:first` - only the first captured subpattern, which is always the
|
||||
complete matching part of the string; all explicitly captured subpatterns
|
||||
are discarded
|
||||
|
||||
* `:all_but_first`- all but the first matching subpattern, i.e. all
|
||||
explicitly captured subpatterns, but not the complete matching part of
|
||||
the string
|
||||
|
||||
* `:none` - do not return matching subpatterns at all
|
||||
|
||||
* `:all_names` - captures all names in the Regex
|
||||
|
||||
* `list(binary)` - a list of named captures to capture
|
||||
|
||||
"""
|
||||
|
||||
defstruct re_pattern: nil, source: "", opts: ""
|
||||
|
||||
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary}
|
||||
|
||||
defmodule CompileError do
|
||||
defexception message: "regex could not be compiled"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the regular expression.
|
||||
|
||||
The given options can either be a binary with the characters
|
||||
representing the same regex options given to the `~r` sigil,
|
||||
or a list of options, as expected by the [Erlang `re` docs](http://www.erlang.org/doc/man/re.html).
|
||||
|
||||
It returns `{:ok, regex}` in case of success,
|
||||
`{:error, reason}` otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.compile("foo")
|
||||
{:ok, ~r"foo"}
|
||||
|
||||
iex> Regex.compile("*foo")
|
||||
{:error, {'nothing to repeat', 0}}
|
||||
|
||||
"""
|
||||
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, any}
|
||||
def compile(source, options \\ "")
|
||||
|
||||
def compile(source, options) when is_binary(options) do
|
||||
case translate_options(options, []) do
|
||||
{:error, rest} ->
|
||||
{:error, {:invalid_option, rest}}
|
||||
|
||||
translated_options ->
|
||||
compile(source, translated_options, options)
|
||||
end
|
||||
end
|
||||
|
||||
def compile(source, options) when is_list(options) do
|
||||
compile(source, options, "")
|
||||
end
|
||||
|
||||
defp compile(source, opts, doc_opts) when is_binary(source) do
|
||||
case :re.compile(source, opts) do
|
||||
{:ok, re_pattern} ->
|
||||
{:ok, %Regex{re_pattern: re_pattern, source: source, opts: doc_opts}}
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the regular expression according to the given options.
|
||||
Fails with `Regex.CompileError` if the regex cannot be compiled.
|
||||
"""
|
||||
@spec compile(binary, binary | [term]) :: t
|
||||
def compile!(source, options \\ "") do
|
||||
case compile(source, options) do
|
||||
{:ok, regex} -> regex
|
||||
{:error, {reason, at}} -> raise Regex.CompileError, message: "#{reason} at position #{at}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a boolean indicating whether there was a match or not.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.match?(~r/foo/, "foo")
|
||||
true
|
||||
|
||||
iex> Regex.match?(~r/foo/, "bar")
|
||||
false
|
||||
|
||||
"""
|
||||
@spec match?(t, String.t) :: boolean
|
||||
def match?(%Regex{re_pattern: compiled}, string) when is_binary(string) do
|
||||
:re.run(string, compiled, [{:capture, :none}]) == :match
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the given argument is a regex.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.regex?(~r/foo/)
|
||||
true
|
||||
|
||||
iex> Regex.regex?(0)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec regex?(t) :: true
|
||||
@spec regex?(any) :: false
|
||||
def regex?(%Regex{}), do: true
|
||||
def regex?(_), do: false
|
||||
|
||||
@doc """
|
||||
Runs the regular expression against the given string until the first match.
|
||||
It returns a list with all captures or `nil` if no match occurred.
|
||||
|
||||
## Options
|
||||
|
||||
* `:return` - set to `:index` to return indexes. Defaults to `:binary`.
|
||||
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
|
||||
to see the possible capture values.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.run(~r/c(d)/, "abcd")
|
||||
["cd", "d"]
|
||||
|
||||
iex> Regex.run(~r/e/, "abcd")
|
||||
nil
|
||||
|
||||
iex> Regex.run(~r/c(d)/, "abcd", return: :index)
|
||||
[{2,2},{3,1}]
|
||||
|
||||
"""
|
||||
@spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
|
||||
def run(regex, string, options \\ [])
|
||||
|
||||
def run(%Regex{re_pattern: compiled}, string, options) when is_binary(string) do
|
||||
return = Keyword.get(options, :return, :binary)
|
||||
captures = Keyword.get(options, :capture, :all)
|
||||
|
||||
case :re.run(string, compiled, [{:capture, captures, return}]) do
|
||||
:nomatch -> nil
|
||||
:match -> []
|
||||
{:match, results} -> results
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the given captures as a map or `nil` if no captures are
|
||||
found. The option `:return` can be set to `:index` to get indexes
|
||||
back.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.named_captures(~r/c(?<foo>d)/, "abcd")
|
||||
%{"foo" => "d"}
|
||||
|
||||
iex> Regex.named_captures(~r/a(?<foo>b)c(?<bar>d)/, "abcd")
|
||||
%{"bar" => "d", "foo" => "b"}
|
||||
|
||||
iex> Regex.named_captures(~r/a(?<foo>b)c(?<bar>d)/, "efgh")
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec named_captures(t, String.t, [term]) :: map | nil
|
||||
def named_captures(regex, string, options \\ []) when is_binary(string) do
|
||||
names = names(regex)
|
||||
options = Keyword.put(options, :capture, names)
|
||||
results = run(regex, string, options)
|
||||
if results, do: Enum.zip(names, results) |> Enum.into(%{})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the underlying `re_pattern` in the regular expression.
|
||||
"""
|
||||
@spec re_pattern(t) :: term
|
||||
def re_pattern(%Regex{re_pattern: compiled}) do
|
||||
compiled
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the regex source as a binary.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.source(~r(foo))
|
||||
"foo"
|
||||
|
||||
"""
|
||||
@spec source(t) :: String.t
|
||||
def source(%Regex{source: source}) do
|
||||
source
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the regex options as a string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.opts(~r(foo)m)
|
||||
"m"
|
||||
|
||||
"""
|
||||
@spec opts(t) :: String.t
|
||||
def opts(%Regex{opts: opts}) do
|
||||
opts
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of names in the regex.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.names(~r/(?<foo>bar)/)
|
||||
["foo"]
|
||||
|
||||
"""
|
||||
@spec names(t) :: [String.t]
|
||||
def names(%Regex{re_pattern: re_pattern}) do
|
||||
{:namelist, names} = :re.inspect(re_pattern, :namelist)
|
||||
names
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `run/3`, but scans the target several times collecting all
|
||||
matches of the regular expression. A list of lists is returned,
|
||||
where each entry in the primary list represents a match and each
|
||||
entry in the secondary list represents the captured contents.
|
||||
|
||||
## Options
|
||||
|
||||
* `:return` - set to `:index` to return indexes. Defaults to `:binary`.
|
||||
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
|
||||
to see the possible capture values.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.scan(~r/c(d|e)/, "abcd abce")
|
||||
[["cd", "d"], ["ce", "e"]]
|
||||
|
||||
iex> Regex.scan(~r/c(?:d|e)/, "abcd abce")
|
||||
[["cd"], ["ce"]]
|
||||
|
||||
iex> Regex.scan(~r/e/, "abcd")
|
||||
[]
|
||||
|
||||
"""
|
||||
@spec scan(t, String.t, [term]) :: [[String.t]]
|
||||
def scan(regex, string, options \\ [])
|
||||
|
||||
def scan(%Regex{re_pattern: compiled}, string, options) when is_binary(string) do
|
||||
return = Keyword.get(options, :return, :binary)
|
||||
captures = Keyword.get(options, :capture, :all)
|
||||
options = [{:capture, captures, return}, :global]
|
||||
|
||||
case :re.run(string, compiled, options) do
|
||||
:match -> []
|
||||
:nomatch -> []
|
||||
{:match, results} -> results
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Splits the given target into the number of parts specified.
|
||||
|
||||
## Options
|
||||
|
||||
* `:parts` - when specified, splits the string into the given number of
|
||||
parts. If not specified, `:parts` defaults to `:infinity`, which will
|
||||
split the string into the maximum number of parts possible based on the
|
||||
given pattern.
|
||||
|
||||
* `:trim` - when true, remove blank strings from the result.
|
||||
|
||||
* `:on` - specifies which captures and order to split the string
|
||||
on. Check the moduledoc for `Regex` to see the possible capture
|
||||
values. Defaults to `:first` which means captures inside the
|
||||
Regex does not affect the split result.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.split(~r/-/, "a-b-c")
|
||||
["a","b","c"]
|
||||
|
||||
iex> Regex.split(~r/-/, "a-b-c", [parts: 2])
|
||||
["a","b-c"]
|
||||
|
||||
iex> Regex.split(~r/-/, "abc")
|
||||
["abc"]
|
||||
|
||||
iex> Regex.split(~r//, "abc")
|
||||
["a", "b", "c", ""]
|
||||
|
||||
iex> Regex.split(~r/a(?<second>b)c/, "abc")
|
||||
["", ""]
|
||||
|
||||
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second])
|
||||
["a", "c"]
|
||||
|
||||
"""
|
||||
@spec split(t, String.t, [term]) :: [String.t]
|
||||
def split(regex, string, options \\ [])
|
||||
|
||||
def split(%Regex{}, "", _opts), do: [""]
|
||||
|
||||
def split(%Regex{re_pattern: compiled}, string, opts) when is_binary(string) do
|
||||
on = Keyword.get(opts, :on, :first)
|
||||
case :re.run(string, compiled, [:global, capture: on]) do
|
||||
{:match, matches} ->
|
||||
do_split(matches, string, 0,
|
||||
parts_to_index(Keyword.get(opts, :parts, :infinity)),
|
||||
Keyword.get(opts, :trim, false))
|
||||
:match ->
|
||||
[string]
|
||||
:nomatch ->
|
||||
[string]
|
||||
end
|
||||
end
|
||||
|
||||
defp parts_to_index(:infinity), do: 0
|
||||
defp parts_to_index(n) when is_integer(n) and n > 0, do: n
|
||||
|
||||
defp do_split(_, string, offset, _counter, true) when byte_size(string) <= offset,
|
||||
do: []
|
||||
|
||||
defp do_split(_, string, offset, 1, _trim),
|
||||
do: [binary_part(string, offset, byte_size(string) - offset)]
|
||||
|
||||
defp do_split([], string, offset, _counter, _trim),
|
||||
do: [binary_part(string, offset, byte_size(string) - offset)]
|
||||
|
||||
defp do_split([[{pos, _}|h]|t], string, offset, counter, trim) when pos - offset < 0,
|
||||
do: do_split([h|t], string, offset, counter, trim)
|
||||
|
||||
defp do_split([[]|t], string, offset, counter, trim),
|
||||
do: do_split(t, string, offset, counter, trim)
|
||||
|
||||
defp do_split([[{pos, length}|h]|t], string, offset, counter, trim) do
|
||||
new_offset = pos + length
|
||||
keep = pos - offset
|
||||
|
||||
if keep == 0 and (length == 0 or trim) do
|
||||
do_split([h|t], string, new_offset, counter, trim)
|
||||
else
|
||||
<<_::binary-size(offset), part::binary-size(keep), _::binary>> = string
|
||||
[part|do_split([h|t], string, new_offset, counter - 1, trim)]
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Receives a regex, a binary and a replacement, returns a new
|
||||
binary where the all matches are replaced by replacement.
|
||||
|
||||
The replacement can be either a string or a function. The string
|
||||
is used as a replacement for every match and it allows specific
|
||||
captures to be accessed via `\N`, where `N` is the capture. In
|
||||
case `\0` is used, the whole match is inserted.
|
||||
|
||||
When the replacement is a function, the function may have arity
|
||||
N where each argument maps to a capture, with the first argument
|
||||
being the whole match. If the function expects more arguments
|
||||
than captures found, the remaining arguments will receive `""`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:global` - when `false`, replaces only the first occurrence
|
||||
(defaults to true)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.replace(~r/d/, "abc", "d")
|
||||
"abc"
|
||||
|
||||
iex> Regex.replace(~r/b/, "abc", "d")
|
||||
"adc"
|
||||
|
||||
iex> Regex.replace(~r/b/, "abc", "[\\0]")
|
||||
"a[b]c"
|
||||
|
||||
iex> Regex.replace(~r/a(b|d)c/, "abcadc", "[\\1]")
|
||||
"[b][d]"
|
||||
|
||||
iex> Regex.replace(~r/a(b|d)c/, "abcadc", fn _, x -> "[#{x}]" end)
|
||||
"[b][d]"
|
||||
|
||||
"""
|
||||
@spec replace(t, String.t, String.t | (... -> String.t), [term]) :: String.t
|
||||
def replace(regex, string, replacement, options \\ [])
|
||||
|
||||
def replace(regex, string, replacement, options) when is_binary(replacement) do
|
||||
do_replace(regex, string, precompile_replacement(replacement), options)
|
||||
end
|
||||
|
||||
def replace(regex, string, replacement, options) when is_function(replacement) do
|
||||
{:arity, arity} = :erlang.fun_info(replacement, :arity)
|
||||
do_replace(regex, string, {replacement, arity}, options)
|
||||
end
|
||||
|
||||
defp do_replace(%Regex{re_pattern: compiled}, string, replacement, options) do
|
||||
opts = if Keyword.get(options, :global) != false, do: [:global], else: []
|
||||
opts = [{:capture, :all, :index}|opts]
|
||||
|
||||
case :re.run(string, compiled, opts) do
|
||||
:nomatch ->
|
||||
string
|
||||
{:match, [mlist|t]} when is_list(mlist) ->
|
||||
apply_list(string, replacement, [mlist|t]) |> IO.iodata_to_binary
|
||||
{:match, slist} ->
|
||||
apply_list(string, replacement, [slist]) |> IO.iodata_to_binary
|
||||
end
|
||||
end
|
||||
|
||||
defp precompile_replacement(""),
|
||||
do: []
|
||||
|
||||
defp precompile_replacement(<<?\\, x, rest :: binary>>) when x < ?0 or x > ?9 do
|
||||
case precompile_replacement(rest) do
|
||||
[head | t] when is_binary(head) ->
|
||||
[<<x, head :: binary>> | t]
|
||||
other ->
|
||||
[<<x>> | other]
|
||||
end
|
||||
end
|
||||
|
||||
defp precompile_replacement(<<?\\, rest :: binary>>) when byte_size(rest) > 0 do
|
||||
{ns, rest} = pick_int(rest)
|
||||
[List.to_integer(ns) | precompile_replacement(rest)]
|
||||
end
|
||||
|
||||
defp precompile_replacement(<<x, rest :: binary>>) do
|
||||
case precompile_replacement(rest) do
|
||||
[head | t] when is_binary(head) ->
|
||||
[<<x, head :: binary>> | t]
|
||||
other ->
|
||||
[<<x>> | other]
|
||||
end
|
||||
end
|
||||
|
||||
defp pick_int(<<x, rest :: binary>>) when x in ?0..?9 do
|
||||
{found, rest} = pick_int(rest)
|
||||
{[x|found], rest}
|
||||
end
|
||||
|
||||
defp pick_int(bin) do
|
||||
{[], bin}
|
||||
end
|
||||
|
||||
defp apply_list(string, replacement, list) do
|
||||
apply_list(string, string, 0, replacement, list)
|
||||
end
|
||||
|
||||
defp apply_list(_, "", _, _, []) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp apply_list(_, string, _, _, []) do
|
||||
string
|
||||
end
|
||||
|
||||
defp apply_list(whole, string, pos, replacement, [[{mpos, _} | _] | _] = list) when mpos > pos do
|
||||
length = mpos - pos
|
||||
<<untouched :: binary-size(length), rest :: binary>> = string
|
||||
[untouched | apply_list(whole, rest, mpos, replacement, list)]
|
||||
end
|
||||
|
||||
defp apply_list(whole, string, pos, replacement, [[{mpos, length} | _] = head | tail]) when mpos == pos do
|
||||
<<_ :: size(length)-binary, rest :: binary>> = string
|
||||
new_data = apply_replace(whole, replacement, head)
|
||||
[new_data | apply_list(whole, rest, pos + length, replacement, tail)]
|
||||
end
|
||||
|
||||
defp apply_replace(string, {fun, arity}, indexes) do
|
||||
apply(fun, get_indexes(string, indexes, arity))
|
||||
end
|
||||
|
||||
defp apply_replace(_, [bin], _) when is_binary(bin) do
|
||||
bin
|
||||
end
|
||||
|
||||
defp apply_replace(string, repl, indexes) do
|
||||
indexes = List.to_tuple(indexes)
|
||||
|
||||
for part <- repl do
|
||||
cond do
|
||||
is_binary(part) ->
|
||||
part
|
||||
part > tuple_size(indexes) ->
|
||||
""
|
||||
true ->
|
||||
get_index(string, elem(indexes, part))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_index(_string, {pos, _len}) when pos < 0 do
|
||||
""
|
||||
end
|
||||
|
||||
defp get_index(string, {pos, len}) do
|
||||
<<_ :: size(pos)-binary, res :: size(len)-binary, _ :: binary>> = string
|
||||
res
|
||||
end
|
||||
|
||||
defp get_indexes(_string, _, 0) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp get_indexes(string, [], arity) do
|
||||
[""|get_indexes(string, [], arity - 1)]
|
||||
end
|
||||
|
||||
defp get_indexes(string, [h|t], arity) do
|
||||
[get_index(string, h)|get_indexes(string, t, arity - 1)]
|
||||
end
|
||||
|
||||
{:ok, pattern} = :re.compile(~S"[.^$*+?()[{\\\|\s#]", [:unicode])
|
||||
@escape_pattern pattern
|
||||
|
||||
@doc ~S"""
|
||||
Escapes a string to be literally matched in a regex.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.escape(".")
|
||||
"\\."
|
||||
|
||||
iex> Regex.escape("\\what if")
|
||||
"\\\\what\\ if"
|
||||
|
||||
"""
|
||||
@spec escape(String.t) :: String.t
|
||||
def escape(string) when is_binary(string) do
|
||||
:re.replace(string, @escape_pattern, "\\\\&", [:global, {:return, :binary}])
|
||||
end
|
||||
|
||||
# Helpers
|
||||
|
||||
@doc false
|
||||
# Unescape map function used by Macro.unescape_string.
|
||||
def unescape_map(?f), do: ?\f
|
||||
def unescape_map(?n), do: ?\n
|
||||
def unescape_map(?r), do: ?\r
|
||||
def unescape_map(?t), do: ?\t
|
||||
def unescape_map(?v), do: ?\v
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(_), do: false
|
||||
|
||||
# Private Helpers
|
||||
|
||||
defp translate_options(<<?u, t :: binary>>, acc), do: translate_options(t, [:unicode, :ucp|acc])
|
||||
defp translate_options(<<?i, t :: binary>>, acc), do: translate_options(t, [:caseless|acc])
|
||||
defp translate_options(<<?x, t :: binary>>, acc), do: translate_options(t, [:extended|acc])
|
||||
defp translate_options(<<?f, t :: binary>>, acc), do: translate_options(t, [:firstline|acc])
|
||||
defp translate_options(<<?r, t :: binary>>, acc), do: translate_options(t, [:ungreedy|acc])
|
||||
defp translate_options(<<?s, t :: binary>>, acc), do: translate_options(t, [:dotall, {:newline, :anycrlf}|acc])
|
||||
defp translate_options(<<?m, t :: binary>>, acc), do: translate_options(t, [:multiline|acc])
|
||||
defp translate_options(<<>>, acc), do: acc
|
||||
defp translate_options(rest, _acc), do: {:error, rest}
|
||||
end
|
||||
@@ -1,336 +0,0 @@
|
||||
defmodule Set do
|
||||
@moduledoc ~S"""
|
||||
This module specifies the Set API expected to be
|
||||
implemented by different representations.
|
||||
|
||||
It also provides functions that redirect to the
|
||||
underlying Set, allowing a developer to work with
|
||||
different Set implementations using one API.
|
||||
|
||||
To create a new set, use the `new` functions defined
|
||||
by each set type:
|
||||
|
||||
HashSet.new #=> creates an empty HashSet
|
||||
|
||||
In the examples below, `set_impl` means a specific
|
||||
`Set` implementation, for example `HashSet`.
|
||||
|
||||
## Protocols
|
||||
|
||||
Sets are required to implement both `Enumerable` and `Collectable`
|
||||
protocols.
|
||||
|
||||
## Match
|
||||
|
||||
Sets are required to implement all operations using the match (`===`)
|
||||
operator.
|
||||
"""
|
||||
|
||||
use Behaviour
|
||||
|
||||
@type value :: any
|
||||
@type values :: [ value ]
|
||||
@type t :: map
|
||||
|
||||
defcallback new :: t
|
||||
defcallback delete(t, value) :: t
|
||||
defcallback difference(t, t) :: t
|
||||
defcallback disjoint?(t, t) :: boolean
|
||||
defcallback equal?(t, t) :: boolean
|
||||
defcallback intersection(t, t) :: t
|
||||
defcallback member?(t, value) :: boolean
|
||||
defcallback put(t, value) :: t
|
||||
defcallback size(t) :: non_neg_integer
|
||||
defcallback subset?(t, t) :: boolean
|
||||
defcallback to_list(t) :: list()
|
||||
defcallback union(t, t) :: t
|
||||
|
||||
defmacrop target(set) do
|
||||
quote do
|
||||
case unquote(set) do
|
||||
%{__struct__: x} when is_atom(x) ->
|
||||
x
|
||||
x ->
|
||||
unsupported_set(x)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes `value` from `set`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> s = Enum.into([1, 2, 3], set_impl.new)
|
||||
iex> Set.delete(s, 4) |> Enum.sort
|
||||
[1, 2, 3]
|
||||
|
||||
iex> s = Enum.into([1, 2, 3], set_impl.new)
|
||||
iex> Set.delete(s, 2) |> Enum.sort
|
||||
[1, 3]
|
||||
|
||||
"""
|
||||
@spec delete(t, value) :: t
|
||||
def delete(set, value) do
|
||||
target(set).delete(set, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a set that is `set1` without the members of `set2`.
|
||||
|
||||
Notice this function is polymorphic as it calculates the difference
|
||||
for of any type. Each set implementation also provides a `difference`
|
||||
function, but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.difference(Enum.into([1,2], set_impl.new), Enum.into([2,3,4], set_impl.new)) |> Enum.sort
|
||||
[1]
|
||||
|
||||
"""
|
||||
@spec difference(t, t) :: t
|
||||
def difference(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.difference(set1, set2)
|
||||
else
|
||||
target2.reduce(set2, {:cont, set1}, fn v, acc ->
|
||||
{:cont, target1.delete(acc, v)}
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if `set1` and `set2` have no members in common.
|
||||
|
||||
Notice this function is polymorphic as it checks for disjoint sets of
|
||||
any type. Each set implementation also provides a `disjoint?` function,
|
||||
but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.disjoint?(Enum.into([1, 2], set_impl.new), Enum.into([3, 4], set_impl.new))
|
||||
true
|
||||
|
||||
iex> Set.disjoint?(Enum.into([1, 2], set_impl.new), Enum.into([2, 3], set_impl.new))
|
||||
false
|
||||
|
||||
"""
|
||||
@spec disjoint?(t, t) :: boolean
|
||||
def disjoint?(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.disjoint?(set1, set2)
|
||||
else
|
||||
target2.reduce(set2, {:cont, true}, fn member, acc ->
|
||||
case target1.member?(set1, member) do
|
||||
false -> {:cont, acc}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec empty(t) :: t
|
||||
def empty(set) do
|
||||
target(set).empty(set)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if two sets are equal using `===`.
|
||||
|
||||
Notice this function is polymorphic as it compares sets of
|
||||
any type. Each set implementation also provides an `equal?`
|
||||
function, but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.equal?(Enum.into([1, 2], set_impl.new), Enum.into([2, 1, 1], set_impl.new))
|
||||
true
|
||||
|
||||
iex> Set.equal?(Enum.into([1, 2], set_impl.new), Enum.into([3, 4], set_impl.new))
|
||||
false
|
||||
|
||||
"""
|
||||
@spec equal?(t, t) :: boolean
|
||||
def equal?(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
cond do
|
||||
target1 == target2 ->
|
||||
target1.equal?(set1, set2)
|
||||
|
||||
target1.size(set1) == target2.size(set2) ->
|
||||
do_subset?(target1, target2, set1, set2)
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a set containing only members in common between `set1` and `set2`.
|
||||
|
||||
Notice this function is polymorphic as it calculates the intersection of
|
||||
any type. Each set implementation also provides a `intersection` function,
|
||||
but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.intersection(Enum.into([1,2], set_impl.new), Enum.into([2,3,4], set_impl.new)) |> Enum.sort
|
||||
[2]
|
||||
|
||||
iex> Set.intersection(Enum.into([1,2], set_impl.new), Enum.into([3,4], set_impl.new)) |> Enum.sort
|
||||
[]
|
||||
|
||||
"""
|
||||
@spec intersection(t, t) :: t
|
||||
def intersection(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.intersection(set1, set2)
|
||||
else
|
||||
target1.reduce(set1, {:cont, target1.new}, fn v, acc ->
|
||||
{:cont, if(target2.member?(set2, v), do: target1.put(acc, v), else: acc)}
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if `set` contains `value`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.member?(Enum.into([1, 2, 3], set_impl.new), 2)
|
||||
true
|
||||
|
||||
iex> Set.member?(Enum.into([1, 2, 3], set_impl.new), 4)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec member?(t, value) :: boolean
|
||||
def member?(set, value) do
|
||||
target(set).member?(set, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inserts `value` into `set` if it does not already contain it.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.put(Enum.into([1, 2, 3], set_impl.new), 3) |> Enum.sort
|
||||
[1, 2, 3]
|
||||
|
||||
iex> Set.put(Enum.into([1, 2, 3], set_impl.new), 4) |> Enum.sort
|
||||
[1, 2, 3, 4]
|
||||
|
||||
"""
|
||||
@spec put(t, value) :: t
|
||||
def put(set, value) do
|
||||
target(set).put(set, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of elements in `set`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.size(Enum.into([1, 2, 3], set_impl.new))
|
||||
3
|
||||
|
||||
"""
|
||||
@spec size(t) :: non_neg_integer
|
||||
def size(set) do
|
||||
target(set).size(set)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if `set1`'s members are all contained in `set2`.
|
||||
|
||||
Notice this function is polymorphic as it checks the subset for
|
||||
any type. Each set implementation also provides a `subset?` function,
|
||||
but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.subset?(Enum.into([1, 2], set_impl.new), Enum.into([1, 2, 3], set_impl.new))
|
||||
true
|
||||
|
||||
iex> Set.subset?(Enum.into([1, 2, 3], set_impl.new), Enum.into([1, 2], set_impl.new))
|
||||
false
|
||||
|
||||
"""
|
||||
@spec subset?(t, t) :: boolean
|
||||
def subset?(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.subset?(set1, set2)
|
||||
else
|
||||
do_subset?(target1, target2, set1, set2)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts `set` to a list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> set_impl.to_list(Enum.into([1, 2, 3], set_impl.new)) |> Enum.sort
|
||||
[1,2,3]
|
||||
|
||||
"""
|
||||
@spec to_list(t) :: list
|
||||
def to_list(set) do
|
||||
target(set).to_list(set)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a set containing all members of `set1` and `set2`.
|
||||
|
||||
Notice this function is polymorphic as it calculates the union of
|
||||
any type. Each set implementation also provides a `union` function,
|
||||
but they can only work with sets of the same type.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Set.union(Enum.into([1,2], set_impl.new), Enum.into([2,3,4], set_impl.new)) |> Enum.sort
|
||||
[1,2,3,4]
|
||||
|
||||
"""
|
||||
@spec union(t, t) :: t
|
||||
def union(set1, set2) do
|
||||
target1 = target(set1)
|
||||
target2 = target(set2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.union(set1, set2)
|
||||
else
|
||||
target2.reduce(set2, {:cont, set1}, fn v, acc ->
|
||||
{:cont, target1.put(acc, v)}
|
||||
end) |> elem(1)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_subset?(target1, target2, set1, set2) do
|
||||
target1.reduce(set1, {:cont, true}, fn member, acc ->
|
||||
case target2.member?(set2, member) do
|
||||
true -> {:cont, acc}
|
||||
_ -> {:halt, false}
|
||||
end
|
||||
end) |> elem(1)
|
||||
end
|
||||
|
||||
defp unsupported_set(set) do
|
||||
raise ArgumentError, "unsupported set: #{inspect set}"
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
defmodule Stream.Reducers do
|
||||
# Collection of reducers shared by Enum and Stream.
|
||||
@moduledoc false
|
||||
|
||||
defmacro chunk(n, step, limit, f \\ nil) do
|
||||
quote do
|
||||
fn entry, acc(h, {buffer, count}, t) ->
|
||||
buffer = [entry|buffer]
|
||||
count = count + 1
|
||||
|
||||
new =
|
||||
if count >= unquote(limit) do
|
||||
left = count - unquote(step)
|
||||
{Enum.take(buffer, left), left}
|
||||
else
|
||||
{buffer, count}
|
||||
end
|
||||
|
||||
if count == unquote(n) do
|
||||
cont_with_acc(unquote(f), :lists.reverse(buffer), h, new, t)
|
||||
else
|
||||
{:cont, acc(h, new, t)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro chunk_by(callback, f \\ nil) do
|
||||
quote do
|
||||
fn
|
||||
entry, acc(h, {buffer, value}, t) ->
|
||||
new_value = unquote(callback).(entry)
|
||||
if new_value == value do
|
||||
{:cont, acc(h, {[entry|buffer], value}, t)}
|
||||
else
|
||||
cont_with_acc(unquote(f), :lists.reverse(buffer), h, {[entry], new_value}, t)
|
||||
end
|
||||
entry, acc(h, nil, t) ->
|
||||
{:cont, acc(h, {[entry], unquote(callback).(entry)}, t)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro drop(f \\ nil) do
|
||||
quote do
|
||||
fn
|
||||
_entry, acc(h, n, t) when n > 0 ->
|
||||
{:cont, acc(h, n-1, t)}
|
||||
entry, acc(h, n, t) ->
|
||||
cont_with_acc(unquote(f), entry, h, n, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro drop_while(callback, f \\ nil) do
|
||||
quote do
|
||||
fn entry, acc(h, bool, t) = orig ->
|
||||
if bool and unquote(callback).(entry) do
|
||||
{:cont, orig}
|
||||
else
|
||||
cont_with_acc(unquote(f), entry, h, false, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro filter(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
if unquote(callback).(entry) do
|
||||
cont(unquote(f), entry, acc)
|
||||
else
|
||||
{:cont, acc}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro filter_map(filter, mapper, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
if unquote(filter).(entry) do
|
||||
cont(unquote(f), unquote(mapper).(entry), acc)
|
||||
else
|
||||
{:cont, acc}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro map(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
cont(unquote(f), unquote(callback).(entry), acc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro reject(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
unless unquote(callback).(entry) do
|
||||
cont(unquote(f), entry, acc)
|
||||
else
|
||||
{:cont, acc}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro scan_2(callback, f \\ nil) do
|
||||
quote do
|
||||
fn
|
||||
entry, acc(h, :first, t) ->
|
||||
cont_with_acc(unquote(f), entry, h, {:ok, entry}, t)
|
||||
entry, acc(h, {:ok, acc}, t) ->
|
||||
value = unquote(callback).(entry, acc)
|
||||
cont_with_acc(unquote(f), value, h, {:ok, value}, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro scan_3(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc(h, acc, t)) ->
|
||||
value = unquote(callback).(entry, acc)
|
||||
cont_with_acc(unquote(f), value, h, value, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro take(f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc(h, n, t) = orig) ->
|
||||
if n >= 1 do
|
||||
cont_with_acc(unquote(f), entry, h, n-1, t)
|
||||
else
|
||||
{:halt, orig}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro take_every(nth, f \\ nil) do
|
||||
quote do
|
||||
fn
|
||||
entry, acc(h, n, t) when n === :first
|
||||
when n === unquote(nth) ->
|
||||
cont_with_acc(unquote(f), entry, h, 1, t)
|
||||
entry, acc(h, n, t) ->
|
||||
{:cont, acc(h, n+1, t)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro take_while(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc) ->
|
||||
if unquote(callback).(entry) do
|
||||
cont(unquote(f), entry, acc)
|
||||
else
|
||||
{:halt, acc}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro uniq(callback, f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc(h, prev, t) = acc) ->
|
||||
value = unquote(callback).(entry)
|
||||
if :lists.member(value, prev) do
|
||||
{:cont, acc}
|
||||
else
|
||||
cont_with_acc(unquote(f), entry, h, [value|prev], t)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro with_index(f \\ nil) do
|
||||
quote do
|
||||
fn(entry, acc(h, counter, t)) ->
|
||||
cont_with_acc(unquote(f), {entry, counter}, h, counter + 1, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
import Kernel, except: [to_string: 1]
|
||||
|
||||
defprotocol String.Chars do
|
||||
@moduledoc ~S"""
|
||||
The String.Chars protocol is responsible for
|
||||
converting a structure to a Binary (only if applicable).
|
||||
The only function required to be implemented is
|
||||
`to_string` which does the conversion.
|
||||
|
||||
The `to_string` function automatically imported
|
||||
by Kernel invokes this protocol. String
|
||||
interpolation also invokes to_string in its
|
||||
arguments. For example, `"foo#{bar}"` is the same
|
||||
as `"foo" <> to_string(bar)`.
|
||||
"""
|
||||
|
||||
def to_string(thing)
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Atom do
|
||||
def to_string(nil) do
|
||||
""
|
||||
end
|
||||
|
||||
def to_string(atom) do
|
||||
Atom.to_string(atom)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: BitString do
|
||||
def to_string(thing) when is_binary(thing) do
|
||||
thing
|
||||
end
|
||||
|
||||
def to_string(thing) do
|
||||
raise Protocol.UndefinedError,
|
||||
protocol: @protocol,
|
||||
value: thing,
|
||||
description: "cannot convert a bitstring to a string"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: List do
|
||||
def to_string(char_list), do: List.to_string(char_list)
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Integer do
|
||||
def to_string(thing) do
|
||||
Integer.to_string(thing)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Float do
|
||||
def to_string(thing) do
|
||||
IO.iodata_to_binary(:io_lib_format.fwrite_g(thing))
|
||||
end
|
||||
end
|
||||
@@ -1,340 +0,0 @@
|
||||
defmodule StringIO do
|
||||
@moduledoc """
|
||||
This module provides an IO device that wraps a string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("foo")
|
||||
iex> IO.read(pid, 2)
|
||||
"fo"
|
||||
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
@doc """
|
||||
Creates an IO device.
|
||||
|
||||
If the `:capture_prompt` option is set to `true`,
|
||||
prompts (specified as arguments to `IO.get*` functions)
|
||||
are captured.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("foo")
|
||||
iex> IO.gets(pid, ">")
|
||||
"foo"
|
||||
iex> StringIO.contents(pid)
|
||||
{"", ""}
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("foo", capture_prompt: true)
|
||||
iex> IO.gets(pid, ">")
|
||||
"foo"
|
||||
iex> StringIO.contents(pid)
|
||||
{"", ">"}
|
||||
|
||||
"""
|
||||
@spec open(binary, Keyword.t) :: {:ok, pid}
|
||||
def open(string, options \\ []) when is_binary(string) do
|
||||
GenServer.start_link(__MODULE__, {string, options}, [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns current buffers.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("in")
|
||||
iex> IO.write(pid, "out")
|
||||
iex> StringIO.contents(pid)
|
||||
{"in", "out"}
|
||||
|
||||
"""
|
||||
@spec contents(pid) :: {binary, binary}
|
||||
def contents(pid) when is_pid(pid) do
|
||||
GenServer.call(pid, :contents)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the IO device and returns remaining buffers.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = StringIO.open("in")
|
||||
iex> IO.write(pid, "out")
|
||||
iex> StringIO.close(pid)
|
||||
{:ok, {"in", "out"}}
|
||||
|
||||
"""
|
||||
@spec close(pid) :: {:ok, {binary, binary}}
|
||||
def close(pid) when is_pid(pid) do
|
||||
GenServer.call(pid, :close)
|
||||
end
|
||||
|
||||
## callbacks
|
||||
|
||||
def init({string, options}) do
|
||||
capture_prompt = options[:capture_prompt] || false
|
||||
{:ok, %{input: string, output: "", capture_prompt: capture_prompt}}
|
||||
end
|
||||
|
||||
def handle_info({:io_request, from, reply_as, req}, s) do
|
||||
s = io_request(from, reply_as, req, s)
|
||||
{:noreply, s}
|
||||
end
|
||||
|
||||
def handle_info(msg, s) do
|
||||
super(msg, s)
|
||||
end
|
||||
|
||||
def handle_call(:contents, _from, %{input: input, output: output} = s) do
|
||||
{:reply, {input, output}, s}
|
||||
end
|
||||
|
||||
def handle_call(:close, _from, %{input: input, output: output} = s) do
|
||||
{:stop, :normal, {:ok, {input, output}}, s}
|
||||
end
|
||||
|
||||
def handle_call(request, from, s) do
|
||||
super(request, from, s)
|
||||
end
|
||||
|
||||
defp io_request(from, reply_as, req, s) do
|
||||
{reply, s} = io_request(req, s)
|
||||
io_reply(from, reply_as, to_reply(reply))
|
||||
s
|
||||
end
|
||||
|
||||
defp io_request({:put_chars, chars}, %{output: output} = s) do
|
||||
{:ok, %{s | output: << output :: binary, IO.chardata_to_string(chars) :: binary >>}}
|
||||
end
|
||||
|
||||
defp io_request({:put_chars, m, f, as}, %{output: output} = s) do
|
||||
chars = apply(m, f, as)
|
||||
{:ok, %{s | output: << output :: binary, IO.chardata_to_string(chars) :: binary >>}}
|
||||
end
|
||||
|
||||
defp io_request({:put_chars, _encoding, chars}, s) do
|
||||
io_request({:put_chars, chars}, s)
|
||||
end
|
||||
|
||||
defp io_request({:put_chars, _encoding, mod, func, args}, s) do
|
||||
io_request({:put_chars, mod, func, args}, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_chars, prompt, n}, s) when n >= 0 do
|
||||
io_request({:get_chars, :latin1, prompt, n}, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_chars, encoding, prompt, n}, s) when n >= 0 do
|
||||
get_chars(encoding, prompt, n, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_line, prompt}, s) do
|
||||
io_request({:get_line, :latin1, prompt}, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_line, encoding, prompt}, s) do
|
||||
get_line(encoding, prompt, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_until, prompt, mod, fun, args}, s) do
|
||||
io_request({:get_until, :latin1, prompt, mod, fun, args}, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_until, encoding, prompt, mod, fun, args}, s) do
|
||||
get_until(encoding, prompt, mod, fun, args, s)
|
||||
end
|
||||
|
||||
defp io_request({:get_password, encoding}, s) do
|
||||
get_line(encoding, "", s)
|
||||
end
|
||||
|
||||
defp io_request({:setopts, _opts}, s) do
|
||||
{{:error, :enotsup}, s}
|
||||
end
|
||||
|
||||
defp io_request(:getopts, s) do
|
||||
{{:ok, [binary: true, encoding: :unicode]}, s}
|
||||
end
|
||||
|
||||
defp io_request({:get_geometry, :columns}, s) do
|
||||
{{:error, :enotsup}, s}
|
||||
end
|
||||
|
||||
defp io_request({:get_geometry, :rows}, s) do
|
||||
{{:error, :enotsup}, s}
|
||||
end
|
||||
|
||||
defp io_request({:requests, reqs}, s) do
|
||||
io_requests(reqs, {:ok, s})
|
||||
end
|
||||
|
||||
defp io_request(_, s) do
|
||||
{{:error, :request}, s}
|
||||
end
|
||||
|
||||
## get_chars
|
||||
|
||||
defp get_chars(encoding, prompt, n,
|
||||
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
|
||||
case do_get_chars(input, encoding, n) do
|
||||
{:error, _} = error ->
|
||||
{error, s}
|
||||
{result, input} ->
|
||||
if capture_prompt do
|
||||
output = << output :: binary, IO.chardata_to_string(prompt) :: binary >>
|
||||
end
|
||||
|
||||
{result, %{s | input: input, output: output}}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_chars("", _encoding, _n) do
|
||||
{:eof, ""}
|
||||
end
|
||||
|
||||
defp do_get_chars(input, :latin1, n) when byte_size(input) < n do
|
||||
{input, ""}
|
||||
end
|
||||
|
||||
defp do_get_chars(input, :latin1, n) do
|
||||
<<chars :: binary-size(n), rest :: binary>> = input
|
||||
{chars, rest}
|
||||
end
|
||||
|
||||
defp do_get_chars(input, encoding, n) do
|
||||
try do
|
||||
case :file_io_server.count_and_find(input, n, encoding) do
|
||||
{buf_count, split_pos} when buf_count < n or split_pos == :none ->
|
||||
{input, ""}
|
||||
{_buf_count, split_pos} ->
|
||||
<<chars :: binary-size(split_pos), rest :: binary>> = input
|
||||
{chars, rest}
|
||||
end
|
||||
catch
|
||||
:exit, :invalid_unicode ->
|
||||
{:error, :invalid_unicode}
|
||||
end
|
||||
end
|
||||
|
||||
## get_line
|
||||
|
||||
defp get_line(encoding, prompt,
|
||||
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
|
||||
case :unicode.characters_to_list(input, encoding) do
|
||||
{:error, _, _} ->
|
||||
{{:error, :collect_line}, s}
|
||||
{:incomplete, _, _} ->
|
||||
{{:error, :collect_line}, s}
|
||||
chars ->
|
||||
{result, input} = do_get_line(chars, encoding)
|
||||
|
||||
if capture_prompt do
|
||||
output = << output :: binary, IO.chardata_to_string(prompt) :: binary >>
|
||||
end
|
||||
|
||||
{result, %{s | input: input, output: output}}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_line('', _encoding) do
|
||||
{:eof, ""}
|
||||
end
|
||||
|
||||
defp do_get_line(chars, encoding) do
|
||||
{line, rest} = collect_line(chars)
|
||||
{:unicode.characters_to_binary(line, encoding),
|
||||
:unicode.characters_to_binary(rest, encoding)}
|
||||
end
|
||||
|
||||
## get_until
|
||||
|
||||
defp get_until(encoding, prompt, mod, fun, args,
|
||||
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
|
||||
case :unicode.characters_to_list(input, encoding) do
|
||||
{:error, _, _} ->
|
||||
{:error, s}
|
||||
{:incomplete, _, _} ->
|
||||
{:error, s}
|
||||
chars ->
|
||||
{result, input, count} = do_get_until(chars, encoding, mod, fun, args)
|
||||
|
||||
if capture_prompt do
|
||||
output = << output :: binary, :binary.copy(IO.chardata_to_string(prompt), count) :: binary >>
|
||||
end
|
||||
|
||||
input =
|
||||
case input do
|
||||
:eof -> ""
|
||||
_ -> :unicode.characters_to_binary(input, encoding)
|
||||
end
|
||||
|
||||
{result, %{s | input: input, output: output}}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_until(chars, encoding, mod, fun, args, continuation \\ [], count \\ 0)
|
||||
|
||||
defp do_get_until('', encoding, mod, fun, args, continuation, count) do
|
||||
case apply(mod, fun, [continuation, :eof | args]) do
|
||||
{:done, result, rest} ->
|
||||
{result, rest, count + 1}
|
||||
{:more, next_continuation} ->
|
||||
do_get_until('', encoding, mod, fun, args, next_continuation, count + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_until(chars, encoding, mod, fun, args, continuation, count) do
|
||||
{line, rest} = collect_line(chars)
|
||||
|
||||
case apply(mod, fun, [continuation, line | args]) do
|
||||
{:done, result, rest1} ->
|
||||
unless rest1 == :eof do
|
||||
rest = rest1 ++ rest
|
||||
end
|
||||
{result, rest, count + 1}
|
||||
{:more, next_continuation} ->
|
||||
do_get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
|
||||
end
|
||||
end
|
||||
|
||||
## io_requests
|
||||
|
||||
defp io_requests([r|rs], {:ok, s}) do
|
||||
io_requests(rs, io_request(r, s))
|
||||
end
|
||||
|
||||
defp io_requests(_, result) do
|
||||
result
|
||||
end
|
||||
|
||||
## helpers
|
||||
|
||||
defp collect_line(chars) do
|
||||
collect_line(chars, [])
|
||||
end
|
||||
|
||||
defp collect_line([], stack) do
|
||||
{:lists.reverse(stack), []}
|
||||
end
|
||||
|
||||
defp collect_line([?\r, ?\n | rest], stack) do
|
||||
{:lists.reverse([?\n|stack]), rest}
|
||||
end
|
||||
|
||||
defp collect_line([?\n | rest], stack) do
|
||||
{:lists.reverse([?\n|stack]), rest}
|
||||
end
|
||||
|
||||
defp collect_line([h|t], stack) do
|
||||
collect_line(t, [h|stack])
|
||||
end
|
||||
|
||||
defp io_reply(from, reply_as, reply) do
|
||||
send from, {:io_reply, reply_as, reply}
|
||||
end
|
||||
|
||||
defp to_reply(list) when is_list(list), do: IO.chardata_to_string(list)
|
||||
defp to_reply(other), do: other
|
||||
end
|
||||
@@ -1,405 +0,0 @@
|
||||
defmodule Supervisor do
|
||||
@moduledoc """
|
||||
A behaviour module for implementing supervision functionality.
|
||||
|
||||
A supervisor is a process which supervises other processes called
|
||||
child processes. Supervisors are used to build an hierarchical process
|
||||
structure called a supervision tree, a nice way to structure fault-tolerant
|
||||
applications.
|
||||
|
||||
A supervisor implemented using this module will have a standard set
|
||||
of interface functions and include functionality for tracing and error
|
||||
reporting. It will also fit into an supervision tree.
|
||||
|
||||
## Example
|
||||
|
||||
In order to define a supervisor, we need to first define a child process
|
||||
that is going to be supervised. In order to do so, we will define a GenServer
|
||||
that represents a stack:
|
||||
|
||||
defmodule Stack do
|
||||
use GenServer
|
||||
|
||||
def start_link(state) do
|
||||
GenServer.start_link(__MODULE__, state, [name: :sup_stack])
|
||||
end
|
||||
|
||||
def handle_call(:pop, _from, [h|t]) do
|
||||
{:reply, h, t}
|
||||
end
|
||||
|
||||
def handle_cast({:push, h}, t) do
|
||||
{:noreply, [h|t]}
|
||||
end
|
||||
end
|
||||
|
||||
We can now define our supervisor and start it as follows:
|
||||
|
||||
# Import helpers for defining supervisors
|
||||
import Supervisor.Spec
|
||||
|
||||
# We are going to supervise the Stack server which will
|
||||
# be started with a single argument [:hello]
|
||||
children = [
|
||||
worker(Stack, [[:hello]])
|
||||
]
|
||||
|
||||
# Start the supervisor with our one child
|
||||
{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)
|
||||
|
||||
Notice that when starting the GenServer, we have registered it
|
||||
with name `:sup_stack`, which allows us to call it directly and
|
||||
get what is on the stack:
|
||||
|
||||
GenServer.call(:sup_stack, :pop)
|
||||
#=> :hello
|
||||
|
||||
GenServer.cast(:sup_stack, {:push, :world})
|
||||
#=> :ok
|
||||
|
||||
GenServer.call(:sup_stack, :pop)
|
||||
#=> :world
|
||||
|
||||
However, there is a bug in our stack server. If we call `:pop` and
|
||||
the stack is empty, it is going to crash because no clause matches.
|
||||
Let's try it:
|
||||
|
||||
GenServer.call(:sup_stack, :pop)
|
||||
=ERROR REPORT====
|
||||
|
||||
Luckily, since the server is being supervised by a supervisor, the
|
||||
supervisor will automatically start a new one, with the default stack
|
||||
of `[:hello]` like before:
|
||||
|
||||
GenServer.call(:sup_stack, :pop) == :hello
|
||||
|
||||
Supervisors support different strategies; in the example above, we
|
||||
have chosen `:one_for_one`. Furthermore, each supervisor can have many
|
||||
workers and supervisors as children, each of them with their specific
|
||||
configuration, shutdown values, and restart strategies.
|
||||
|
||||
Continue reading this moduledoc to learn more about supervision strategies
|
||||
and then follow to the `Supervisor.Spec` module documentation to learn
|
||||
about the specification for workers and supervisors.
|
||||
|
||||
## Module-based supervisors
|
||||
|
||||
In the example above, a supervisor was dynamically created by passing
|
||||
the supervision structure to `start_link/2`. However, supervisors
|
||||
can also be created by explicitly defining a supervision module:
|
||||
|
||||
defmodule MyApp.Supervisor do
|
||||
use Supervisor
|
||||
|
||||
def start_link do
|
||||
Supervisor.start_link(__MODULE__, [])
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
children = [
|
||||
worker(Stack, [[:hello]])
|
||||
]
|
||||
|
||||
supervise(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
|
||||
You may want to use a module-based supervisor if:
|
||||
|
||||
* You need to do some particular action on supervisor
|
||||
initialization, like setting up a ETS table.
|
||||
|
||||
* You want to perform partial hot-code swapping of the
|
||||
tree. For example, if you add or remove a children,
|
||||
the module-based supervision will add and remove the
|
||||
new children directly, while the dynamic supervision
|
||||
requires the whole tree to be restarted in order to
|
||||
perform such swaps.
|
||||
|
||||
## Strategies
|
||||
|
||||
* `:one_for_one` - if a child process terminates, only that
|
||||
process is restarted.
|
||||
|
||||
* `:one_for_all` - if a child process terminates, all other child
|
||||
processes are terminated and then all child processes (including
|
||||
the terminated one) are restarted.
|
||||
|
||||
* `:rest_for_one` - if a child process terminates, the "rest" of
|
||||
the child processes, i.e. the child processes after the terminated
|
||||
one in start order, are terminated. Then the terminated child
|
||||
process and the rest of the child processes are restarted.
|
||||
|
||||
* `:simple_one_for_one` - similar to `:one_for_one` but suits better
|
||||
when dynamically attaching children. This strategy requires the
|
||||
supervisor specification to contain only one child. Many functions
|
||||
in this module behave slightly differently when this strategy is
|
||||
used.
|
||||
|
||||
## Name Registration
|
||||
|
||||
A supervisor is bound to the same name registration rules as a `GenServer`.
|
||||
Read more about it in the `GenServer` docs.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
quote location: :keep do
|
||||
@behaviour :supervisor
|
||||
import Supervisor.Spec
|
||||
end
|
||||
end
|
||||
|
||||
@typedoc "Return values of `start_link` functions"
|
||||
@type on_start :: {:ok, pid} | :ignore |
|
||||
{:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
|
||||
@typedoc "Return values of `start_child` functions"
|
||||
@type on_start_child :: {:ok, child} | {:ok, child, info :: term} |
|
||||
{:error, {:already_started, child} | :already_present | term}
|
||||
|
||||
@type child :: pid | :undefined
|
||||
|
||||
@typedoc "The Supervisor name"
|
||||
@type name :: atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@typedoc "Options used by the `start*` functions"
|
||||
@type options :: [name: name,
|
||||
strategy: Supervisor.Spec.strategy,
|
||||
max_restarts: non_neg_integer,
|
||||
max_seconds: non_neg_integer]
|
||||
|
||||
@typedoc "The supervisor reference"
|
||||
@type supervisor :: pid | name | {atom, node}
|
||||
|
||||
@doc """
|
||||
Starts a supervisor with the given children.
|
||||
|
||||
A strategy is required to be given as an option. Furthermore,
|
||||
the `:max_restarts` and `:max_seconds` value can be configured
|
||||
as described in `Supervisor.Spec.supervise/2` docs.
|
||||
|
||||
The options can also be used to register a supervisor name.
|
||||
the supported values are described under the `Name Registration`
|
||||
section in the `GenServer` module docs.
|
||||
|
||||
If the supervisor and its child processes are successfully created
|
||||
(i.e. if the start function of all child processes returns `{:ok, child}`,
|
||||
`{:ok, child, info}`, or `:ignore`) the function returns
|
||||
`{:ok, pid}`, where `pid` is the pid of the supervisor. If there
|
||||
already exists a process with the specified name, the function returns
|
||||
`{:error, {:already_started, pid}}`, where pid is the pid of that
|
||||
process.
|
||||
|
||||
If any of the child process start functions fail or return an error tuple or
|
||||
an erroneous value, the supervisor will first terminate all already
|
||||
started child processes with reason `:shutdown` and then terminate
|
||||
itself and return `{:error, {:shutdown, reason}}`.
|
||||
|
||||
Note that the `Supervisor` is linked to the parent process
|
||||
and will exit not only on crashes but also if the parent process
|
||||
exits with `:normal` reason.
|
||||
"""
|
||||
@spec start_link([tuple], options) :: on_start
|
||||
def start_link(children, options) when is_list(children) do
|
||||
spec = Supervisor.Spec.supervise(children, options)
|
||||
start_link(Supervisor.Default, spec, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a supervisor module with the given `arg`.
|
||||
|
||||
To start the supervisor, the `init/1` callback will be invoked
|
||||
in the given module. The `init/1` callback must return a
|
||||
supervision specification which can be created with the help
|
||||
of `Supervisor.Spec` module.
|
||||
|
||||
If the `init/1` callback returns `:ignore`, this function returns
|
||||
`:ignore` as well and the supervisor terminates with reason `:normal`.
|
||||
If it fails or returns an incorrect value, this function returns
|
||||
`{:error, term}` where `term` is a term with information about the
|
||||
error, and the supervisor terminates with reason `term`.
|
||||
|
||||
The `:name` option can also be given in order to register a supervisor
|
||||
name, the supported values are described under the `Name Registration`
|
||||
section in the `GenServer` module docs.
|
||||
|
||||
Other failure conditions are specified in `start_link/2` docs.
|
||||
"""
|
||||
@spec start_link(module, term, options) :: on_start
|
||||
def start_link(module, arg, options \\ []) when is_list(options) do
|
||||
case Keyword.get(options, :name) do
|
||||
nil ->
|
||||
:supervisor.start_link(module, arg)
|
||||
atom when is_atom(atom) ->
|
||||
:supervisor.start_link({:local, atom}, module, arg)
|
||||
other when is_tuple(other) ->
|
||||
:supervisor.start_link(other, module, arg)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dynamically adds and starts a child specification to the supervisor.
|
||||
|
||||
`child_spec` should be a valid child specification (unless the supervisor
|
||||
is a `:simple_one_for_one` supervisor, see below). The child process will
|
||||
be started as defined in the child specification.
|
||||
|
||||
In the case of `:simple_one_for_one`, the child specification defined in
|
||||
the supervisor will be used and instead of a `child_spec`, an arbitrary list
|
||||
of terms is expected. The child process will then be started by appending
|
||||
the given list to the existing function arguments in the child specification.
|
||||
|
||||
If there already exists a child specification with the specified id,
|
||||
`child_spec` is discarded and the function returns an error with `:already_started`
|
||||
or `:already_present` if the corresponding child process is running or not.
|
||||
|
||||
If the child process start function returns `{:ok, child}` or `{:ok, child, info}`,
|
||||
the child specification and pid is added to the supervisor and the function returns
|
||||
the same value.
|
||||
|
||||
If the child process start function returns `:ignore, the child specification is
|
||||
added to the supervisor, the pid is set to undefined and the function returns
|
||||
`{:ok, :undefined}`.
|
||||
|
||||
If the child process start function returns an error tuple or an erroneous value,
|
||||
or if it fails, the child specification is discarded and the function returns
|
||||
`{:error, error}` where `error` is a term containing information about the error
|
||||
and child specification.
|
||||
"""
|
||||
@spec start_child(supervisor, Supervisor.Spec.spec | [term]) :: on_start_child
|
||||
def start_child(supervisor, child_spec_or_args) do
|
||||
call(supervisor, {:start_child, child_spec_or_args})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Terminates the given pid or child id.
|
||||
|
||||
If the supervisor is not a `simple_one_for_one`, the child id is expected
|
||||
and the process, if there is one, is terminated; the child specification is
|
||||
kept unless the child is temporary.
|
||||
|
||||
In case of a `simple_one_for_one` supervisor, a pid is expected. If the child
|
||||
specification identifier is given instead of a `pid`, the function will
|
||||
return `{:error, :simple_one_for_one}`.
|
||||
|
||||
A non-temporary child process may later be restarted by the supervisor. The child
|
||||
process can also be restarted explicitly by calling `restart_child/2`. Use
|
||||
`delete_child/2` to remove the child specification.
|
||||
|
||||
If successful, the function returns `:ok`. If there is no child specification or
|
||||
pid, the function returns `{:error, :not_found}`.
|
||||
"""
|
||||
@spec terminate_child(supervisor, pid | Supervisor.Spec.child_id) :: :ok | {:error, error}
|
||||
when error: :not_found | :simple_one_for_one
|
||||
def terminate_child(supervisor, pid_or_child_id) do
|
||||
call(supervisor, {:terminate_child, pid_or_child_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the child specification identified by `child_id`.
|
||||
|
||||
The corresponding child process must not be running, use `terminate_child/2`
|
||||
to terminate it.
|
||||
|
||||
If successful, the function returns `:ok`. This function may error with an
|
||||
appropriate error tuple if the `child_id` is not found, or if the current
|
||||
process is running or being restarted.
|
||||
|
||||
This operation is not supported by `simple_one_for_one` supervisors.
|
||||
"""
|
||||
@spec delete_child(supervisor, Supervisor.Spec.child_id) :: :ok | {:error, error}
|
||||
when error: :not_found | :simple_one_for_one | :running | :restarting
|
||||
def delete_child(supervisor, child_id) do
|
||||
call(supervisor, {:delete_child, child_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Restarts a child process identified by `child_id`.
|
||||
|
||||
The child specification must exist and the corresponding child process must not
|
||||
be running.
|
||||
|
||||
Note that for temporary children, the child specification is automatically deleted
|
||||
when the child terminates, and thus it is not possible to restart such children.
|
||||
|
||||
If the child process start function returns `{:ok, child}` or
|
||||
`{:ok, child, info}`, the pid is added to the supervisor and the function returns
|
||||
the same value.
|
||||
|
||||
If the child process start function returns `:ignore`, the pid remains set to
|
||||
`:undefined` and the function returns `{:ok, :undefined}`.
|
||||
|
||||
This function may error with an appropriate error tuple if the `child_id` is not
|
||||
found, or if the current process is running or being restarted.
|
||||
|
||||
If the child process start function returns an error tuple or an erroneous value,
|
||||
or if it fails, the function returns `{:error, error}`.
|
||||
|
||||
This operation is not supported by `simple_one_for_one` supervisors.
|
||||
"""
|
||||
@spec restart_child(supervisor, Supervisor.Spec.child_id) ::
|
||||
{:ok, child} | {:ok, child, term} | {:error, error}
|
||||
when error: :not_found | :simple_one_for_one | :running | :restarting | term
|
||||
def restart_child(supervisor, child_id) do
|
||||
call(supervisor, {:restart_child, child_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with information about all children.
|
||||
|
||||
Note that calling this function when supervising a large number of children
|
||||
under low memory conditions can cause an out of memory exception.
|
||||
|
||||
This function returns a list of tuples containing:
|
||||
|
||||
* `id` - as defined in the child specification or `:undefined` in the case
|
||||
of a `simple_one_for_one` supervisor
|
||||
|
||||
* `child` - the pid of the corresponding child process, the atom
|
||||
`:restarting` if the process is about to be restarted, or `:undefined` if
|
||||
there is no such process
|
||||
|
||||
* `type` - `:worker` or `:supervisor` as defined in the child specification
|
||||
|
||||
* `modules` – as defined in the child specification
|
||||
"""
|
||||
@spec which_children(supervisor) ::
|
||||
[{Supervisor.Spec.child_id | :undefined,
|
||||
child | :restarting,
|
||||
Supervisor.Spec.worker,
|
||||
Supervisor.Spec.modules}]
|
||||
def which_children(supervisor) do
|
||||
call(supervisor, :which_children)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a map containing count values for the supervisor.
|
||||
|
||||
The map contains the following keys:
|
||||
|
||||
* `:specs` - the total count of children, dead or alive
|
||||
|
||||
* `:active` - the count of all actively running child processes managed by
|
||||
this supervisor
|
||||
|
||||
* `:supervisors` - the count of all supervisors whether or not the child
|
||||
process is still alive
|
||||
|
||||
* `:workers` - the count of all workers, whether or not the child process
|
||||
is still alive
|
||||
|
||||
"""
|
||||
@spec count_children(supervisor) ::
|
||||
%{specs: non_neg_integer, active: non_neg_integer,
|
||||
supervisors: non_neg_integer, workers: non_neg_integer}
|
||||
def count_children(supervisor) do
|
||||
call(supervisor, :count_children) |> :maps.from_list
|
||||
end
|
||||
|
||||
@compile {:inline, call: 2}
|
||||
|
||||
defp call(supervisor, req) do
|
||||
GenServer.call(supervisor, req, :infinity)
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user