Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c345dab9c1 | ||
|
|
2d46eda619 | ||
|
|
beb9b1b3ef | ||
|
|
cb512a7f17 | ||
|
|
e4fdeb6b79 | ||
|
|
6b22171018 | ||
|
|
f0112289e6 | ||
|
|
72e18a0a0c | ||
|
|
e2be3f652e | ||
|
|
490bd32322 | ||
|
|
1bfcba133a | ||
|
|
c3d024c16c | ||
|
|
1530a761d5 | ||
|
|
5197b6ece6 |
-50
@@ -1,50 +0,0 @@
|
||||
env:
|
||||
CIRRUS_CLONE_DEPTH: 50
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
test_template: &DEFAULT_TEST_SETTINGS
|
||||
# don't cancel the task execution if it's master or a release branch
|
||||
auto_cancellation: $CIRRUS_BRANCH != 'master' && $CIRRUS_BRANCH !=~ 'v\d+\.\d+.*'
|
||||
|
||||
test_freebsd_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
name: FreeBSD 13.0
|
||||
alias: FreeBSD Stable
|
||||
|
||||
freebsd_instance:
|
||||
image_family: freebsd-13-0
|
||||
cpu: 8
|
||||
memory: 7424Mi
|
||||
|
||||
env:
|
||||
CHECK_REPRODUCIBLE: true
|
||||
LC_ALL: en_US.UTF-8
|
||||
PATH: $PATH:/usr/local/lib/erlang24/bin
|
||||
|
||||
install_script:
|
||||
- pkg install -y erlang-runtime24 git gmake
|
||||
- rm -rf .git
|
||||
- gmake compile
|
||||
|
||||
build_info_script: bin/elixir --version
|
||||
|
||||
test_formatted_script:
|
||||
- gmake test_formatted &&
|
||||
echo "All Elixir source code files are properly formatted."
|
||||
|
||||
dialyzer_script: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
|
||||
test_erlang_script: gmake test_erlang
|
||||
|
||||
test_elixir_script: gmake test_elixir
|
||||
|
||||
check_reproducible_script: |
|
||||
if [ -n "$CHECK_REPRODUCIBLE" ]; then
|
||||
gmake check_reproducible
|
||||
else
|
||||
echo "The reproducibility of the build is only checked in the last stable Erlang/OTP version."
|
||||
fi
|
||||
@@ -1,19 +0,0 @@
|
||||
[
|
||||
inputs: [
|
||||
"lib/*/{lib,unicode,test}/**/*.{ex,exs}",
|
||||
"lib/*/*.exs",
|
||||
"lib/ex_unit/examples/*.exs",
|
||||
".formatter.exs"
|
||||
],
|
||||
locals_without_parens: [
|
||||
# Formatter tests
|
||||
assert_format: 2,
|
||||
assert_format: 3,
|
||||
assert_same: 1,
|
||||
assert_same: 2,
|
||||
|
||||
# Errors tests
|
||||
assert_eval_raise: 3
|
||||
],
|
||||
normalize_bitstring_modifiers: false
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
|
||||
*.ex diff=elixir
|
||||
*.exs diff=elixir
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
blank_issues_enabled: true
|
||||
|
||||
contact_links:
|
||||
- name: Discuss proposals
|
||||
url: https://groups.google.com/g/elixir-lang-core
|
||||
about: Send proposals for new ideas to our mailing list
|
||||
|
||||
- name: Ask questions and support
|
||||
url: https://elixirforum.com/
|
||||
about: Ask questions, provide support and more on Elixir Forum
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
name: Report an issue
|
||||
description:
|
||||
Tell us about something that is not working the way we (probably) intend
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thank you for contributing to Elixir! :heart:
|
||||
|
||||
|
||||
Please, do not use this form for guidance, questions or support.
|
||||
Try instead in [Elixir Forum](https://elixirforum.com),
|
||||
the [IRC Chat](https://web.libera.chat/#elixir),
|
||||
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
|
||||
[Slack](https://elixir-slackin.herokuapp.com),
|
||||
[Discord](https://discord.gg/elixir) or in other online communities.
|
||||
|
||||
- type: textarea
|
||||
id: elixir-and-otp-version
|
||||
attributes:
|
||||
label: Elixir and Erlang/OTP versions
|
||||
description: Paste the output of `elixir --version` here.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
description: The operating system that this issue is happening on.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: current-behavior
|
||||
attributes:
|
||||
label: Current behavior
|
||||
description: >
|
||||
Include code samples, errors, and stacktraces if appropriate.
|
||||
|
||||
|
||||
If reporting a bug, please include the reproducing steps.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A short description on how you expect the code to behave.
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -1,87 +0,0 @@
|
||||
name: builds.hex.pm
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*.*
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
ELIXIR_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
concurrency: builds_txt
|
||||
|
||||
jobs:
|
||||
release_pre_built:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- otp: 25
|
||||
otp_version: '25.3'
|
||||
build_docs: build_docs
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin
|
||||
- uses: ./.github/workflows/release_pre_built
|
||||
with:
|
||||
otp_version: ${{ matrix.otp_version }}
|
||||
otp: ${{ matrix.otp }}
|
||||
build_docs: ${{ matrix.build_docs }}
|
||||
- name: Utils.sh
|
||||
run: |
|
||||
cat << 'EOF' > utils.sh
|
||||
function purge_key() {
|
||||
curl \
|
||||
-X POST \
|
||||
-H "Fastly-Key: ${FASTLY_KEY}" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Length: 0" \
|
||||
"https://api.fastly.com/service/$1/purge/$2"
|
||||
}
|
||||
function purge() {
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
sleep 2
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
sleep 2
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
}
|
||||
EOF
|
||||
chmod +x utils.sh
|
||||
- name: Upload Docs to S3
|
||||
if: ${{ matrix.build_docs }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
|
||||
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||
run: |
|
||||
source utils.sh
|
||||
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
|
||||
for f in doc/*; do
|
||||
if [ -d "$f" ]; then
|
||||
app=`echo $f | sed s/"doc\/"//`
|
||||
tarball="${app}-${version}.tar.gz"
|
||||
surrogate_key="docs/${app}-${version}"
|
||||
tar -czf "${tarball}" -C "doc/${app}" .
|
||||
aws s3 cp "${tarball}" "s3://${{ env.AWS_S3_BUCKET }}/docs/${tarball}" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
purge "${surrogate_key}"
|
||||
fi
|
||||
done
|
||||
@@ -1,129 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: Linux, ${{ matrix.otp_release }}, Ubuntu 20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_release: OTP-25.0
|
||||
otp_latest: true
|
||||
- otp_release: OTP-24.3
|
||||
- otp_release: OTP-24.0
|
||||
- otp_release: OTP-23.3
|
||||
- otp_release: OTP-23.0
|
||||
- otp_release: master
|
||||
development: true
|
||||
- otp_release: maint
|
||||
development: true
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Erlang/OTP
|
||||
run: |
|
||||
cd $RUNNER_TEMP
|
||||
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-20.04/${{ matrix.otp_release }}.tar.gz
|
||||
mkdir -p otp
|
||||
tar zxf otp.tar.gz -C otp --strip-components=1
|
||||
otp/Install -minimal $(pwd)/otp
|
||||
echo "$(pwd)/otp/bin" >> $GITHUB_PATH
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
rm -rf .git
|
||||
make compile
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
- name: Run Dialyzer
|
||||
run: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
- name: Elixir test suite
|
||||
run: make test_elixir
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
- name: Check reproducible builds
|
||||
run: taskset 1 make check_reproducible
|
||||
if: ${{ matrix.otp_latest }}
|
||||
- name: Build docs
|
||||
if: ${{ matrix.otp_latest }}
|
||||
run: |
|
||||
git config --global advice.detachedHead false
|
||||
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
for branch in main v${EX_DOC_LATEST_STABLE_VERSION}; do
|
||||
echo "Building docs with ExDoc ${branch}"
|
||||
cd ..
|
||||
git clone https://github.com/elixir-lang/ex_doc.git --branch ${branch} --depth 1
|
||||
cd ex_doc
|
||||
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
|
||||
cd ../elixir/
|
||||
make docs
|
||||
rm -rf ../ex_doc/
|
||||
done
|
||||
|
||||
test_windows:
|
||||
name: Windows, OTP-${{ matrix.otp_release }}, Windows Server 2019
|
||||
strategy:
|
||||
matrix:
|
||||
otp_release: ['23.3']
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Cache Erlang/OTP package
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\erlang
|
||||
key: OTP-${{ matrix.otp_release }}-windows-2019
|
||||
- name: Install Erlang/OTP
|
||||
run: choco install -y erlang --version ${{ matrix.otp_release }}
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
remove-item '.git' -recurse -force
|
||||
make compile
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
- name: Erlang test suite
|
||||
run: make --keep-going test_erlang
|
||||
- name: Elixir test suite
|
||||
run: |
|
||||
del c:/Windows/System32/drivers/etc/hosts
|
||||
make --keep-going test_elixir
|
||||
|
||||
check_posix_compliant:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Shellcheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
- name: Check POSIX-compliant
|
||||
run: |
|
||||
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
|
||||
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
|
||||
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
|
||||
@@ -1,72 +0,0 @@
|
||||
# #!/usr/bin/env elixir
|
||||
[tag] = System.argv()
|
||||
|
||||
Mix.install([
|
||||
{:req, "~> 0.2.1"},
|
||||
{:jason, "~> 1.0"}
|
||||
])
|
||||
|
||||
%{status: 200, body: release} =
|
||||
Req.get!("https://api.github.com/repos/elixir-lang/elixir/releases/tags/#{tag}")
|
||||
|
||||
if release["draft"] do
|
||||
raise "cannot notify a draft release"
|
||||
end
|
||||
|
||||
## Notify on elixir-lang-ann
|
||||
|
||||
names_and_checksums =
|
||||
for asset <- release["assets"],
|
||||
name = asset["name"],
|
||||
name =~ ~r/.sha\d+sum$/,
|
||||
do: {name, Req.get!(asset["browser_download_url"]).body}
|
||||
|
||||
line_items =
|
||||
for {name, checksum_and_name} <- Enum.sort(names_and_checksums) do
|
||||
[checksum | _] = String.split(checksum_and_name, " ")
|
||||
root = Path.rootname(name)
|
||||
"." <> type = Path.extname(name)
|
||||
" * #{root} - #{type} - #{checksum}\n"
|
||||
end
|
||||
|
||||
mail = %{
|
||||
"From" => "jose.valim@dashbit.co",
|
||||
"To" => "elixir-lang-ann@googlegroups.com",
|
||||
"Subject" => "Elixir #{tag} released",
|
||||
"HtmlBody" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{line_items}",
|
||||
"MessageStream" => "outbound"
|
||||
}
|
||||
|
||||
if System.get_env("DRYRUN") do
|
||||
IO.puts("MAIL")
|
||||
IO.inspect(mail)
|
||||
else
|
||||
headers = %{
|
||||
"X-Postmark-Server-Token" => System.fetch_env!("ELIXIR_LANG_ANN_TOKEN")
|
||||
}
|
||||
|
||||
resp = Req.post!("https://api.postmarkapp.com/email", {:json, mail}, headers: headers)
|
||||
IO.puts("#{resp.status} elixir-lang-ann\n#{inspect(resp.body)}")
|
||||
end
|
||||
|
||||
## Notify on Elixir Forum
|
||||
|
||||
post = %{
|
||||
"title" => "Elixir #{tag} released",
|
||||
"raw" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{release["body"]}",
|
||||
# Elixir News
|
||||
"category" => 28
|
||||
}
|
||||
|
||||
if System.get_env("DRYRUN") do
|
||||
IO.puts("POST")
|
||||
IO.inspect(post)
|
||||
else
|
||||
headers = %{
|
||||
"api-key" => System.fetch_env!("ELIXIR_FORUM_TOKEN"),
|
||||
"api-username" => "Elixir"
|
||||
}
|
||||
|
||||
resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
|
||||
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
|
||||
end
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Notify
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-20.04
|
||||
name: Notify
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: '25.0'
|
||||
elixir-version: '1.14.0'
|
||||
- name: Run Elixir script
|
||||
env:
|
||||
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
|
||||
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
|
||||
run: |
|
||||
elixir .github/workflows/notify.exs ${{ github.ref_name }}
|
||||
@@ -1,96 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
ELIXIR_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
create_draft_release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Create draft release
|
||||
run: |
|
||||
gh release create \
|
||||
--repo ${{ github.repository }} \
|
||||
--title ${{ github.ref_name }} \
|
||||
--notes '' \
|
||||
--draft \
|
||||
${{ github.ref_name }}
|
||||
release_pre_built:
|
||||
needs: create_draft_release
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- otp: 23
|
||||
otp_version: 23.3
|
||||
- otp: 24
|
||||
otp_version: 24.3
|
||||
- otp: 25
|
||||
otp_version: 25.0
|
||||
build_docs: build_docs
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
version-type: strict
|
||||
- name: Build Elixir Release
|
||||
run: |
|
||||
make Precompiled.zip
|
||||
mv Precompiled.zip elixir-otp-${{ matrix.otp }}.zip
|
||||
shasum -a 1 elixir-otp-${{ matrix.otp }}.zip > elixir-otp-${{ matrix.otp }}.zip.sha1sum
|
||||
shasum -a 256 elixir-otp-${{ matrix.otp }}.zip > elixir-otp-${{ matrix.otp }}.zip.sha256sum
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
- name: Upload Pre-built
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload --clobber "${{ github.ref_name }}" \
|
||||
elixir-otp-${{ matrix.otp }}.zip \
|
||||
elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum
|
||||
- name: Get latest stable ExDoc version
|
||||
if: ${{ matrix.build_docs }}
|
||||
run: |
|
||||
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
echo "EX_DOC_LATEST_STABLE_VERSION=${EX_DOC_LATEST_STABLE_VERSION}" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
if: ${{ matrix.build_docs }}
|
||||
with:
|
||||
repository: elixir-lang/ex_doc
|
||||
ref: v${{ env.EX_DOC_LATEST_STABLE_VERSION }}
|
||||
path: ex_doc
|
||||
- name: Build ex_doc
|
||||
if: ${{ matrix.build_docs }}
|
||||
run: |
|
||||
mv ex_doc ../ex_doc
|
||||
cd ../ex_doc
|
||||
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
|
||||
cd ../elixir
|
||||
- name: Build Docs
|
||||
if: ${{ matrix.build_docs }}
|
||||
run: |
|
||||
make Docs.zip
|
||||
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
- name: Upload Docs
|
||||
if: ${{ matrix.build_docs }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload --clobber "${{ github.ref_name }}" \
|
||||
Docs.zip \
|
||||
Docs.zip.sha{1,256}sum
|
||||
@@ -1,51 +0,0 @@
|
||||
name: "Release pre built"
|
||||
description: "Builds elixir release, ExDoc and generates docs"
|
||||
inputs:
|
||||
otp:
|
||||
description: "The major OTP version"
|
||||
otp_version:
|
||||
description: "The exact OTP version (major.minor[.patch])"
|
||||
build_docs:
|
||||
description: "If docs have to be built or not"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: ${{ inputs.otp_version }}
|
||||
version-type: strict
|
||||
- name: Build Elixir Release
|
||||
shell: bash
|
||||
run: |
|
||||
make Precompiled.zip
|
||||
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
|
||||
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
|
||||
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
- name: Get latest stable ExDoc version
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
echo "EX_DOC_LATEST_STABLE_VERSION=${EX_DOC_LATEST_STABLE_VERSION}" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
if: ${{ inputs.build_docs }}
|
||||
with:
|
||||
repository: elixir-lang/ex_doc
|
||||
ref: v${{ env.EX_DOC_LATEST_STABLE_VERSION }}
|
||||
path: ex_doc
|
||||
- name: Build ex_doc
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
mv ex_doc ../ex_doc
|
||||
cd ../ex_doc
|
||||
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
|
||||
cd ../elixir
|
||||
- name: Build Docs
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
make Docs.zip
|
||||
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
+9
-11
@@ -1,13 +1,11 @@
|
||||
.formatter.exs
|
||||
/_build/
|
||||
/cover/
|
||||
/deps/
|
||||
/doc/
|
||||
/lib/*/ebin/
|
||||
/lib/*/_build/
|
||||
/lib/*/tmp/
|
||||
/lib/elixir/src/*_parser.erl
|
||||
/lib/elixir/test/ebin/
|
||||
/man/elixir.1
|
||||
/man/iex.1
|
||||
/Docs-v*.zip
|
||||
/Precompiled-v*.zip
|
||||
/.eunit
|
||||
.elixir.plt
|
||||
/.fetch
|
||||
erl_crash.dump
|
||||
*.ez
|
||||
n8n_openai_adapter-*.tar
|
||||
/tmp/
|
||||
/result
|
||||
|
||||
-600
@@ -1,600 +0,0 @@
|
||||
# Changelog for Elixir v1.14
|
||||
|
||||
Elixir v1.14 brings many improvements to the debugging experience in Elixir
|
||||
and data-type inspection. It also includes a new abstraction for easy
|
||||
partitioning of processes called `PartitionSupervisor`, as well as improved
|
||||
compilation times and error messages.
|
||||
|
||||
Elixir v1.14 is the last version to support Erlang/OTP 23. Consider updating
|
||||
to Erlang/OTP 24 or Erlang/OTP 25.
|
||||
|
||||
## `dbg`
|
||||
|
||||
`Kernel.dbg/2` is a new macro that's somewhat similar to `IO.inspect/2`, but
|
||||
specifically tailored for **debugging**.
|
||||
|
||||
When called, it prints the value of whatever you pass to it, plus the debugged
|
||||
code itself as well as its location. This code:
|
||||
|
||||
```elixir
|
||||
# In my_file.exs
|
||||
feature = %{name: :dbg, inspiration: "Rust"}
|
||||
dbg(feature)
|
||||
dbg(Map.put(feature, :in_version, "1.14.0"))
|
||||
```
|
||||
|
||||
Prints this:
|
||||
|
||||
```shell
|
||||
$ elixir my_file.exs
|
||||
[my_file.exs:2: (file)]
|
||||
feature #=> %{inspiration: "Rust", name: :dbg}
|
||||
|
||||
[my_file.exs:3: (file)]
|
||||
Map.put(feature, :in_version, "1.14.0") #=> %{in_version: "1.14.0", inspiration: "Rust", name: :dbg}
|
||||
```
|
||||
|
||||
`dbg/2` can do more. It's a macro, so it *understands Elixir code*. You can see
|
||||
that when you pass a series of `|>` pipes to it. `dbg/2` will print the value
|
||||
for every step of the pipeline. This code:
|
||||
|
||||
```elixir
|
||||
# In dbg_pipes.exs
|
||||
__ENV__.file
|
||||
|> String.split("/", trim: true)
|
||||
|> List.last()
|
||||
|> File.exists?()
|
||||
|> dbg()
|
||||
```
|
||||
|
||||
Prints this:
|
||||
|
||||
```shell
|
||||
$ elixir dbg_pipes.exs
|
||||
[dbg_pipes.exs:5: (file)]
|
||||
__ENV__.file #=> "/home/myuser/dbg_pipes.exs"
|
||||
|> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"]
|
||||
|> List.last() #=> "dbg_pipes.exs"
|
||||
|> File.exists?() #=> true
|
||||
```
|
||||
|
||||
### IEx and Prying
|
||||
|
||||
`dbg/2` supports configurable backends. IEx automatically replaces the default
|
||||
backend by one that halts the code execution with `IEx.Pry`, giving developers
|
||||
the option to access local variables, imports, and more. This also works with
|
||||
pipelines: if you pass a series of `|>` pipe calls to `dbg` (or pipe into it at the
|
||||
end, like `|> dbg()`), you'll be able to step through every line in the pipeline.
|
||||
|
||||
You can keep the default behaviour by passing the `--no-pry` option to IEx.
|
||||
|
||||
## PartitionSupervisor
|
||||
|
||||
`PartitionSupervisor` is a new module that implements a new supervisor type. The
|
||||
partition supervisor is designed to help with situations where you have a single
|
||||
supervised process that becomes a bottleneck. If that process's state can be
|
||||
easily partitioned, then you can use `PartitionSupervisor` to supervise multiple
|
||||
isolated copies of that process running concurrently, each assigned its own
|
||||
partition.
|
||||
|
||||
For example, imagine you have an `ErrorReporter` process that you use to report
|
||||
errors to a monitoring service.
|
||||
|
||||
```elixir
|
||||
# Application supervisor:
|
||||
children = [
|
||||
# ...,
|
||||
ErrorReporter
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_one)
|
||||
```
|
||||
|
||||
As the concurrency of your application goes up, the `ErrorReporter` process
|
||||
might receive requests from many other processes and eventually become a
|
||||
bottleneck. In a case like this, it could help to spin up multiple copies of the
|
||||
`ErrorReporter` process under a `PartitionSupervisor`.
|
||||
|
||||
```elixir
|
||||
# Application supervisor
|
||||
children = [
|
||||
{PartitionSupervisor, child_spec: ErrorReporter, name: Reporters}
|
||||
]
|
||||
```
|
||||
|
||||
The `PartitionSupervisor` will spin up a number of processes equal to
|
||||
`System.schedulers_online()` by default (most often one per core). Now, when
|
||||
routing requests to `ErrorReporter` processes we can use a `:via` tuple and
|
||||
route the requests through the partition supervisor.
|
||||
|
||||
```elixir
|
||||
partitioning_key = self()
|
||||
ErrorReporter.report({:via, PartitionSupervisor, {Reporters, partitioning_key}}, error)
|
||||
```
|
||||
|
||||
Using `self()` as the partitioning key here means that the same process will
|
||||
always report errors to the same `ErrorReporter` process, ensuring a form of
|
||||
back-pressure. You can use any term as the partitioning key.
|
||||
|
||||
### A Common Example
|
||||
|
||||
A common and practical example of a good use case for `PartitionSupervisor` is
|
||||
partitioning something like a `DynamicSupervisor`. When starting many processes
|
||||
under it, a dynamic supervisor can be a bottleneck, especially if said processes
|
||||
take a long time to initialize. Instead of starting a single `DynamicSupervisor`,
|
||||
you can start multiple:
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{PartitionSupervisor, child_spec: DynamicSupervisor, name: MyApp.DynamicSupervisors}
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_one)
|
||||
```
|
||||
|
||||
Now you start processes on the dynamic supervisor for the right partition.
|
||||
For instance, you can partition by PID, like in the previous example:
|
||||
|
||||
```elixir
|
||||
DynamicSupervisor.start_child(
|
||||
{:via, PartitionSupervisor, {MyApp.DynamicSupervisors, self()}},
|
||||
my_child_specification
|
||||
)
|
||||
```
|
||||
|
||||
## Improved errors on binaries and evaluation
|
||||
|
||||
Erlang/OTP 25 improved errors on binary construction and evaluation. These improvements
|
||||
apply to Elixir as well. Before v1.14, errors when constructing binaries would
|
||||
often be hard-to-debug generic "argument errors". With Erlang/OTP 25 and Elixir v1.14,
|
||||
more detail is provided for easier debugging. This work is part of [EEP
|
||||
54](https://www.erlang.org/eeps/eep-0054).
|
||||
|
||||
Before:
|
||||
|
||||
```elixir
|
||||
int = 1
|
||||
bin = "foo"
|
||||
int <> bin
|
||||
#=> ** (ArgumentError) argument error
|
||||
```
|
||||
|
||||
Now:
|
||||
|
||||
```elixir
|
||||
int = 1
|
||||
bin = "foo"
|
||||
int <> bin
|
||||
#=> ** (ArgumentError) construction of binary failed:
|
||||
#=> segment 1 of type 'binary':
|
||||
#=> expected a binary but got: 1
|
||||
```
|
||||
|
||||
## Slicing with steps
|
||||
|
||||
Elixir v1.12 introduced **stepped ranges**, which are ranges where you can
|
||||
specify the "step":
|
||||
|
||||
```elixir
|
||||
Enum.to_list(1..10//3)
|
||||
#=> [1, 4, 7, 10]
|
||||
```
|
||||
|
||||
Stepped ranges are particularly useful for numerical operations involving
|
||||
vectors and matrices (see [Nx](https://github.com/elixir-nx/nx), for example).
|
||||
However, the Elixir standard library was not making use of stepped ranges in its
|
||||
APIs. Elixir v1.14 starts to take advantage of steps with support for stepped
|
||||
ranges in a couple of functions. One of them is `Enum.slice/2`:
|
||||
|
||||
```elixir
|
||||
letters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
|
||||
Enum.slice(letters, 0..5//2)
|
||||
#=> ["a", "c", "e"]
|
||||
```
|
||||
|
||||
`binary_slice/2` (and `binary_slice/3` for completeness) has been added to the
|
||||
`Kernel` module, that works with bytes and also support stepped ranges:
|
||||
|
||||
```elixir
|
||||
binary_slice("Elixir", 1..5//2)
|
||||
#=> "lxr"
|
||||
```
|
||||
|
||||
## Expression-based inspection and `Inspect` improvements
|
||||
|
||||
In Elixir, it's conventional to implement the `Inspect` protocol for opaque
|
||||
structs so that they're inspected with a special notation, resembling this:
|
||||
|
||||
```elixir
|
||||
MapSet.new([:apple, :banana])
|
||||
#MapSet<[:apple, :banana]>
|
||||
```
|
||||
|
||||
This is generally done when the struct content or part of it is private and the
|
||||
`%name{...}` representation would reveal fields that are not part of the public
|
||||
API.
|
||||
|
||||
The downside of the `#name<...>` convention is that *the inspected output is not
|
||||
valid Elixir code*. For example, you cannot copy the inspected output and paste
|
||||
it into an IEx session.
|
||||
|
||||
Elixir v1.14 changes the convention for some of the standard-library structs.
|
||||
The `Inspect` implementation for those structs now returns a string with a valid
|
||||
Elixir expression that recreates the struct when evaluated. In the `MapSet`
|
||||
example above, this is what we have now:
|
||||
|
||||
```elixir
|
||||
fruits = MapSet.new([:apple, :banana])
|
||||
MapSet.put(fruits, :pear)
|
||||
#=> MapSet.new([:apple, :banana, :pear])
|
||||
```
|
||||
|
||||
The `MapSet.new/1` expression evaluates to exactly the struct that we're
|
||||
inspecting. This allows us to hide the internals of `MapSet`, while keeping
|
||||
it as valid Elixir code. This expression-based inspection has been
|
||||
implemented for `Version.Requirement`, `MapSet`, and `Date.Range`.
|
||||
|
||||
Finally, we have improved the `Inspect` protocol for structs so that
|
||||
fields are inspected in the order they are declared in `defstruct`.
|
||||
The option `:optional` has also been added when deriving the `Inspect`
|
||||
protocol, giving developers more control over the struct representation.
|
||||
See the updated documentation for `Inspect` for a general rundown on
|
||||
the approaches and options available.
|
||||
|
||||
## v1.14.5 (2023-05-22)
|
||||
|
||||
This release contains fixes for Erlang/OTP 26.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [CLI] Fix a bug where stdout would block when there was no attached terminal on Windows when running on Erlang/OTP 26
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Properly set SSL configuration for Mix downloads when running on Erlang/OTP 26
|
||||
|
||||
## v1.14.4 (2023-04-03)
|
||||
|
||||
This release adds basic support for Erlang/OTP 26. When migrating
|
||||
to Erlang/OTP 26, keep it mind it changes how maps are stored
|
||||
internally and they will be printed and traversed in a different
|
||||
order (note maps never provided a guarantee of their order).
|
||||
To aid migration, this release adds `:sort_maps` to `inspect`
|
||||
custom options, in case you want to sort them before inspection:
|
||||
|
||||
inspect(map, custom_options: [sort_maps: true])
|
||||
|
||||
### Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Inspect] Add `:sort_maps` to `Inspect.Opts.custom_options`
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Support shell history in Erlang/OTP 26+
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] Optimize application tracer
|
||||
|
||||
### Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Properly handle blocks with comments in all cases in `Code.quoted_to_string_with_comments/2`
|
||||
* [Kernel] Fix `debug_info/4` when returning core_v1
|
||||
* [Kernel] Store complete path on `quote keep: true` to avoid invalid stacktraces
|
||||
* [Kernel] Fix column count when tokenizing escaped interpolations
|
||||
* [Stream] Fix `Stream.zip/1` hanging on empty list
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix format] Don't call formatter on directories
|
||||
|
||||
## v1.14.3 (2023-01-14)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Speed up loading of runtime modules in the parallel compiler
|
||||
* [Range] Optimize range membership implementation
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Return values from running doctests and make their order consistent
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Fix handling of negative years in `Calendar.strftime/2`
|
||||
* [DateTime] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `DateTime.add/3`)
|
||||
* [Exception] Improve blaming of FunctionClauseError with `is_struct/2` guards
|
||||
* [Kernel] Fix invalid variable scoping in `defguard` expansion
|
||||
* [Kernel] Do not warn on captured underscored vars from `defmodule`
|
||||
* [Kernel] Do not crash for missing line info on type warnings
|
||||
* [NaiveDateTime] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `NaiveDateTime.add/3`)
|
||||
* [Macro] Fix `Macro.to_string/1` for large negative integers
|
||||
* [Macro] Properly type and escape expansion of `__ENV__` in macros
|
||||
* [Path] Make sure `Path.wildcard/2` expands `..` symlinks accordingly
|
||||
* [Range] Address corner cases in `Range.disjoint?/2` implementation
|
||||
* [Time] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `Time.add/3`)
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.DocTest] Remove unnecessary literal quotes from error message on reports
|
||||
|
||||
## v1.14.2 (2022-11-11)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Add `Code.eval_quoted_with_env/4` with support for the `:prune_binding` option
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Case] Allow test cases to not be registered on use
|
||||
* [ExUnit.DocTest] Include `:doctest` and `:doctest_line` as meta tags
|
||||
* [ExUnit.Formatter] Expose `ExUnit.Formatter.format_assertion_diff/4`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] `Mix.install/2` accepts atoms as paths
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code.Formatter] Fix `size*unit` shortcut in bitstring
|
||||
* [Kernel] Generate unique variables for macro expansion of `defguard`
|
||||
* [Protocol] Expand `:for` in protocols with the appropriate env
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Do not run duplicate cases on `ExUnit.run/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix test] Ensure proper error message when there is no test directory
|
||||
|
||||
## v1.14.1 (2022-10-10)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Perform partial expansion of literals in module attributes
|
||||
* [Kernel] Do not add compile-time dependencies for literals as defaults in `Application.compile_env/3` inside module attributes
|
||||
* [Macro] Add `Macro.expand_literals/2` and `Macro.expand_literals/3`
|
||||
* [System] Add `:close_stdin` to `System.shell/2`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix test] Accept `--all-warnings` option
|
||||
|
||||
## 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix misleading warning when `:uniq` is given in `for` comprehensions and the result is unused
|
||||
* [Kernel] Improve error message for when there is a conflicting struct and ignoring module conflict
|
||||
* [Kernel] Do not delete `@enforce_keys` attribute after `defstruct` declaration
|
||||
* [Kernel] Do not crash the checker on modules with missing `:debug_info` chunk
|
||||
* [Macro] Fix error in `Macro.to_string/2` when converting an AST with `:erlang.binary_to_atom/2`
|
||||
* [String] Fix `String.split/3` and `String.next_grapheme/1` returning invalid results on invalid UTF-8 encoding
|
||||
* [System] Do not close stdin by default in `System.shell/2`
|
||||
* [URI] Do not return `uri.port` as `:undefined` in certain cases in `URI.new/1`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.DocTest] Do not crash when both `:moduledoc` and functions are specified in `:only`
|
||||
|
||||
#### IEx
|
||||
|
||||
* [CLI] Fix invalid argument handling when `--no-pry` is given
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix format] Do not cache inputs from `.formatter.exs` so they are properly re-evaluted on every call
|
||||
|
||||
## v1.14.0 (2022-09-01)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Support multi-line comments to EEx via `<%!-- --%>`
|
||||
* [EEx] Add `EEx.tokenize/2`
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Access] Add `Access.slice/1`
|
||||
* [Application] Add `Application.compile_env/4` and `Application.compile_env!/3` to read the compile-time environment inside macros
|
||||
* [Calendar] Support ISO8601 basic format parsing with `DateTime.from_iso8601/2`
|
||||
* [Calendar] Add `day`/`hour`/`minute` on `add`/`diff` across different calendar modules
|
||||
* [Code] Add `:normalize_bitstring_modifiers` to `Code.format_string!/2`
|
||||
* [Code] Emit deprecation and type warnings for invalid options in on `Code.compile_string/2` and `Code.compile_quoted/2`
|
||||
* [Code] Warn if an outdated lexical tracker is given on eval
|
||||
* [Code] Add `Code.env_for_eval/1` and `Code.eval_quoted_with_env/3`
|
||||
* [Code] Improve stacktraces from eval operations on Erlang/OTP 25+
|
||||
* [Code.Fragment] Add support for `__MODULE__` in several functions
|
||||
* [Code.Fragment] Support surround and context suggestions across multiple lines
|
||||
* [Enum] Allow slicing with steps in `Enum.slice/2`
|
||||
* [File] Support `dereference_symlinks: true` in `File.cp/3` and `File.cp_r/3`
|
||||
* [Float] Do not show floats in scientific notation if below `1.0e16` and the fractional value is precisely zero
|
||||
* [Float] Add `Float.min_finite/0` and `Float.max_finite/0`
|
||||
* [Inspect] Improve error reporting when there is a faulty implementation of the `Inspect` protocol
|
||||
* [Inspect] Allow `:optional` when deriving the Inspect protocol for hiding fields that match their default value
|
||||
* [Inspect] Inspect struct fields in the order they are declared in `defstruct`
|
||||
* [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, and `Version.Requirement`
|
||||
* [IO] Support `Macro.Env` and keywords as stacktrace definitions in `IO.warn/2`
|
||||
* [IO] Add `IO.ANSI.syntax_colors/0` and related configuration to be shared across IEx and `dbg`
|
||||
* [Kernel] Add new `dbg/0-2` macro
|
||||
* [Kernel] Allow any guard expression as the size of a bitstring in a pattern match
|
||||
* [Kernel] Allow composite types with pins as the map key in a pattern match
|
||||
* [Kernel] Print escaped version of control chars when they show up as unexpected tokens
|
||||
* [Kernel] Warn on confusable non-ASCII identifiers
|
||||
* [Kernel] Add `..` as a nullary operator that returns `0..-1//1`
|
||||
* [Kernel] Implement Unicode Technical Standard #39 recommendations. In particular, we warn for confusable scripts and restrict identifiers to single-scripts or highly restrictive mixed-scripts
|
||||
* [Kernel] Automatically perform NFC conversion of identifiers
|
||||
* [Kernel] Add `binary_slice/2` and `binary_slice/3`
|
||||
* [Kernel] Lazily expand module attributes to avoid compile-time deps
|
||||
* [Kernel] Automatically cascade `generated: true` annotations on macro expansion
|
||||
* [Keyword] Add `Keyword.from_keys/2` and `Keyword.replace_lazy/3`
|
||||
* [List] Add `List.keysort/3` with support for a `sorter` function
|
||||
* [Macro] Add `Macro.classify_atom/1` and `Macro.inspect_atom/2`
|
||||
* [Macro] Add `Macro.path/2`
|
||||
* [Macro.Env] Add `Macro.Env.prune_compile_info/1`
|
||||
* [Map] Add `Map.from_keys/2` and `Map.replace_lazy/3`
|
||||
* [MapSet] Add `MapSet.filter/2`, `MapSet.reject/2`, and `MapSet.symmetric_difference/2`
|
||||
* [Node] Add `Node.spawn_monitor/2` and `Node.spawn_monitor/4`
|
||||
* [Module] Support new `@after_verify` attribute for executing code whenever a module is verified
|
||||
* [PartitionSupervisor] Add `PartitionSupervisor` that starts multiple isolated partitions of the same child for scalability
|
||||
* [Path] Add `Path.safe_relative/1` and `Path.safe_relative_to/2`
|
||||
* [Registry] Add `Registry.count_select/2`
|
||||
* [Stream] Add `Stream.duplicate/2` and `Stream.transform/5`
|
||||
* [String] Support empty lookup lists in `String.replace/3`, `String.split/3`, and `String.splitter/3`
|
||||
* [String] Allow slicing with steps in `String.slice/2`
|
||||
* [Task] Add `:zip_input_on_exit` option to `Task.async_stream/3`
|
||||
* [Task] Store `:mfa` in the `Task` struct for reflection purposes
|
||||
* [URI] Add `URI.append_query/2`
|
||||
* [Version] Add `Version.to_string/1`
|
||||
* [Version] Colorize `Version.Requirement` source in the `Inspect` protocol
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Add `ExUnit.Callbacks.start_link_supervised!/2`
|
||||
* [ExUnit] Add `ExUnit.run/1` to rerun test modules
|
||||
* [ExUnit] Colorize summary in yellow with message when all tests are excluded
|
||||
* [ExUnit] Display friendly error when test name is too long
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Evaluate `--dot-iex` line by line
|
||||
* [IEx] Add line-by-line evaluation of IEx breakpoints
|
||||
* [IEx.Autocomplete] Autocomplete bitstrings modifiers (after `::` inside `<<...>>`)
|
||||
* [IEx.Helpers] Allow an atom to be given to `pid/1`
|
||||
* [IEx.Helpers] Support sigils in `h/1`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Add `Logger.put_process_level/2`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Add `:config_path` and `:lockfile` options to `Mix.install/2`
|
||||
* [mix compile] Add `--no-optional-deps` to skip optional dependencies to test compilation works without optional dependencies
|
||||
* [mix compile] Include column information on error diagnostics when possible
|
||||
* [mix deps] `Mix.Dep.Converger` now tells which deps formed a cycle
|
||||
* [mix do] Support `--app` option to restrict recursive tasks in umbrella projects
|
||||
* [mix do] Allow using `+` as a task separator instead of comma
|
||||
* [mix format] Support filename in `mix format -` when reading from stdin
|
||||
* [mix format] Compile if `mix format` plugins are missing
|
||||
* [mix new] Do not allow projects to be created with application names that conflict with multi-arg Erlang VM switches
|
||||
* [mix profile] Return the return value of the profiled function
|
||||
* [mix release] Make BEAM compression opt-in
|
||||
* [mix release] Let `:runtime_config_path` accept `false` to skip the `config/runtime.exs`
|
||||
* [mix test] Improve error message when suite fails due to coverage
|
||||
* [mix test] Support `:test_elixirc_options` and default to not generating docs nor debug info chunk for tests
|
||||
* [mix xref] Support `--group` flag in `mix xref graph`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Handle widths with "0" in them in `Calendar.strftime/3`
|
||||
* [CLI] Improve errors on incorrect `--rpc-eval` usage
|
||||
* [CLI] Return proper exit code on Windows
|
||||
* [Code] Do not emit warnings when formatting code
|
||||
* [Enum] Allow slices to overflow on both starting and ending positions
|
||||
* [Kernel] Do not allow restricted characters in identifiers according to UTS39
|
||||
* [Kernel] Define `__exception__` field as `true` when expanding exceptions in typespecs
|
||||
* [Kernel] Warn if any of `True`, `False`, and `Nil` aliases are used
|
||||
* [Kernel] Warn on underived `@derive` attributes
|
||||
* [Kernel] Remove compile-time dependency from `defimpl :for`
|
||||
* [Kernel] Track all arities on imported functions
|
||||
* [Kernel] Fix equality in guards for dynamic ranges without steps
|
||||
* [Module] Fix loop while unifying type variables
|
||||
* [Protocol] Warn if a protocol has no definitions
|
||||
* [Regex] Show list options when inspecting a Regex manually defined with `Regex.compile/2`
|
||||
* [String] Allow slices to overflow on both starting and ending positions
|
||||
* [System] Raise non-generic exception on missing env in `System.fetch_env!/1` to mirror map operations
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Do not crash when diffing unknown bindings in guards
|
||||
* [ExUnit] Properly print diffs when comparing improper lists with strings at the tail position
|
||||
* [ExUnit] Add short hash to `tmp_dir` in ExUnit to avoid test name collision
|
||||
* [ExUnit] Do not store logs in the CLI formatter (this reduces memory usage for suites with `capture_log`)
|
||||
* [ExUnit] Run `ExUnit.after_suite/1` callback even when no tests run
|
||||
* [ExUnit] Fix scenario where `setup` with imported function from within `describe` failed to compile
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Disallow short-hand pipe after matches
|
||||
* [IEx] Fix `exports/1` in IEx for long function names
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] Fix `--warnings-as-errors` when used with `--all-warnings`
|
||||
* [mix compile.elixir] Ensure semantic recompilation cascades to path dependencies
|
||||
* [mix compile.elixir] Lock the compiler to avoid concurrent usage
|
||||
* [mix format] Do not add new lines if the formatted file is empty
|
||||
* [mix format] Properly compile dependencies on `mix format`
|
||||
* [mix release] Only set `RELEASE_MODE` after `env.{sh,bat}` are executed
|
||||
* [mix release] Allow application mode configuration to cascade to dependencies
|
||||
* [mix xref] Do not emit already consolidated warnings during `mix xref trace`
|
||||
* [Mix] Do not start apps with `runtime: false` on `Mix.install/2`
|
||||
|
||||
### 3. Soft deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [File] Passing a callback as third argument to `File.cp/3` and `File.cp_r/3` is deprecated.
|
||||
Instead pass the callback the `:on_conflict` key of a keyword list
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Using `<%# ... %>` for comments is deprecated. Please use `<% # ... %>` or the new multi-line comments with `<%!-- ... --%>`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Deprecate `Logger.enable/1` and `Logger.disable/1` in favor of `Logger.put_process_level/2`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix cmd] The `--app` option in `mix cmd CMD` is deprecated in favor of the more efficient `mix do --app app cmd CMD`
|
||||
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Application] Calling `Application.get_env/3` and friends in the module body is now discouraged, use `Application.compile_env/3` instead
|
||||
* [Bitwise] `use Bitwise` is deprecated, use `import Bitwise` instead
|
||||
* [Bitwise] `~~~` is deprecated in favor of `bnot` for clarity
|
||||
* [Kernel.ParallelCompiler] Returning a list or two-element tuple from `:each_cycle` is deprecated, return a `{:compile | :runtime, modules, warnings}` tuple instead
|
||||
* [Kernel] Deprecate the operator `<|>` to avoid ambiguity with upcoming extended numerical operators
|
||||
* [String] Deprecate passing a binary compiled pattern to `String.starts_with?/2`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Deprecate `$levelpad` on message formatting
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] `Mix.Tasks.Xref.calls/1` is deprecated in favor of compilation tracers
|
||||
|
||||
### 5. Backwards incompatible changes
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix local.rebar] Remove support for rebar2, which has not been updated in 5 years, and is no longer supported on recent Erlang/OTP versions
|
||||
|
||||
## v1.13
|
||||
|
||||
The CHANGELOG for v1.13 releases can be found [in the v1.13 branch](https://github.com/elixir-lang/elixir/blob/v1.13/CHANGELOG.md).
|
||||
@@ -1,66 +0,0 @@
|
||||
# Code of Conduct
|
||||
|
||||
Contact: elixir-lang-conduct@googlegroups.com
|
||||
|
||||
## Why have a Code of Conduct?
|
||||
|
||||
As contributors and maintainers of this project, we are committed to providing a friendly, safe and welcoming environment for all, regardless of age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic.
|
||||
|
||||
The goal of the Code of Conduct is to specify a baseline standard of behavior so that people with different social values and communication styles can talk about Elixir effectively, productively, and respectfully, even in face of disagreements. The Code of Conduct also provides a mechanism for resolving conflicts in the community when they arise.
|
||||
|
||||
## Our Values
|
||||
|
||||
These are the values Elixir developers should aspire to:
|
||||
|
||||
* Be friendly and welcoming
|
||||
* Be kind
|
||||
* Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.)
|
||||
* Interpret the arguments of others in good faith, do not seek to disagree.
|
||||
* When we do disagree, try to understand why.
|
||||
* Be thoughtful
|
||||
* Productive communication requires effort. Think about how your words will be interpreted.
|
||||
* Remember that sometimes it is best to refrain entirely from commenting.
|
||||
* Be respectful
|
||||
* In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively.
|
||||
* Be constructive
|
||||
* Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation.
|
||||
* Avoid unconstructive criticism: don't merely decry the current state of affairs; offer — or at least solicit — suggestions as to how things may be improved.
|
||||
* Avoid harsh words and stern tone: we are all aligned towards the well-being of the community and the progress of the ecosystem. Harsh words exclude, demotivate, and lead to unnecessary conflict.
|
||||
* Avoid snarking (pithy, unproductive, sniping comments).
|
||||
* Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults towards a project, person or group).
|
||||
* Be responsible
|
||||
* What you say and do matters. Take responsibility for your words and actions, including their consequences, whether intended or otherwise.
|
||||
|
||||
The following actions are explicitly forbidden:
|
||||
|
||||
* Insulting, demeaning, hateful, or threatening remarks.
|
||||
* Discrimination based on age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic.
|
||||
* Bullying or systematic harassment.
|
||||
* Unwelcome sexual advances.
|
||||
* Incitement to any of these.
|
||||
|
||||
## Where does the Code of Conduct apply?
|
||||
|
||||
If you participate in or contribute to the Elixir ecosystem in any way, you are encouraged to follow the Code of Conduct while doing so.
|
||||
|
||||
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
|
||||
|
||||
* The [official GitHub projects][1] and code reviews.
|
||||
* The official elixir-lang mailing lists.
|
||||
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
||||
|
||||
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
|
||||
|
||||
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
||||
|
||||
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This document was based on the Code of Conduct from the Go project (dated Sep/2021) and the Contributor Covenant (v1.4).
|
||||
|
||||
[1]: https://github.com/elixir-lang/
|
||||
[2]: https://web.libera.chat/#elixir
|
||||
[3]: https://libera.chat/
|
||||
@@ -1,176 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,336 +0,0 @@
|
||||
PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
CANONICAL := 1.14/
|
||||
CANONICAL ?= main/
|
||||
DOCS_FORMAT ?= html
|
||||
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
|
||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
GENERATE_APP := $(CURDIR)/lib/elixir/generate_app.escript
|
||||
VERSION := $(strip $(shell cat VERSION))
|
||||
Q := @
|
||||
LIBDIR := lib
|
||||
BINDIR := bin
|
||||
INSTALL = install
|
||||
INSTALL_DIR = $(INSTALL) -m755 -d
|
||||
INSTALL_DATA = $(INSTALL) -m644
|
||||
INSTALL_PROGRAM = $(INSTALL) -m755
|
||||
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
|
||||
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
|
||||
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
|
||||
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
|
||||
|
||||
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
|
||||
.NOTPARALLEL: compile
|
||||
|
||||
#==> Functions
|
||||
|
||||
define CHECK_ERLANG_RELEASE
|
||||
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 23)])' -s erlang halt | grep -q '^true'; \
|
||||
if [ $$? != 0 ]; then \
|
||||
echo "At least Erlang/OTP 23.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) cd lib/$(1) && ../../bin/elixir -e 'Mix.start(:permanent, [])' -r mix.exs -e 'Mix.Task.run("compile.app", ~w[--compile-path ebin])'
|
||||
|
||||
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): compile $(1)
|
||||
@ echo "==> $(1) (ex_unit)"
|
||||
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
|
||||
endef
|
||||
|
||||
define WRITE_SOURCE_DATE_EPOCH
|
||||
$(shell mkdir -p $(SOURCE_DATE_EPOCH_PATH) && bin/elixir -e \
|
||||
'IO.puts System.build_info()[:date] \
|
||||
|> DateTime.from_iso8601() \
|
||||
|> elem(1) \
|
||||
|> DateTime.to_unix()' > $(SOURCE_DATE_EPOCH_FILE))
|
||||
endef
|
||||
|
||||
define READ_SOURCE_DATE_EPOCH
|
||||
$(strip $(shell cat $(SOURCE_DATE_EPOCH_FILE)))
|
||||
endef
|
||||
|
||||
#==> Compilation tasks
|
||||
|
||||
APP := lib/elixir/ebin/elixir.app
|
||||
PARSER := lib/elixir/src/elixir_parser.erl
|
||||
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
|
||||
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
|
||||
|
||||
default: compile
|
||||
|
||||
compile: erlang $(APP) elixir
|
||||
|
||||
erlang: $(PARSER)
|
||||
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
|
||||
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
|
||||
|
||||
$(PARSER): lib/elixir/src/elixir_parser.yrl
|
||||
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
|
||||
|
||||
# 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 bootstrap -s erlang halt; \
|
||||
fi
|
||||
$(Q) "$(MAKE)" unicode
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
|
||||
$(Q) "$(MAKE)" app
|
||||
|
||||
app: $(APP)
|
||||
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
|
||||
$(Q) $(GENERATE_APP) $< $@ $(VERSION)
|
||||
|
||||
unicode: $(UNICODE)
|
||||
$(UNICODE): lib/elixir/unicode/*
|
||||
@ echo "==> unicode (compile)";
|
||||
$(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
|
||||
$(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
|
||||
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/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 \
|
||||
rm -rf $(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin; \
|
||||
$(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)/$(BINDIR)"
|
||||
$(Q) for file in "$(DESTDIR)$(PREFIX)"/$(LIBDIR)/elixir/bin/*; do \
|
||||
ln -sf "../$(LIBDIR)/elixir/bin/$${file##*/}" "$(DESTDIR)$(PREFIX)/$(BINDIR)/"; \
|
||||
done
|
||||
"$(MAKE)" install_man
|
||||
|
||||
check_reproducible: compile
|
||||
$(Q) echo "==> Checking for reproducible builds..."
|
||||
$(Q) rm -rf lib/*/tmp/ebin_reproducible/
|
||||
$(call WRITE_SOURCE_DATE_EPOCH)
|
||||
$(Q) mkdir -p lib/elixir/tmp/ebin_reproducible/ \
|
||||
lib/eex/tmp/ebin_reproducible/ \
|
||||
lib/ex_unit/tmp/ebin_reproducible/ \
|
||||
lib/iex/tmp/ebin_reproducible/ \
|
||||
lib/logger/tmp/ebin_reproducible/ \
|
||||
lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/elixir/ebin/* lib/elixir/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/eex/ebin/* lib/eex/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/ex_unit/ebin/* lib/ex_unit/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
|
||||
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
|
||||
$(Q) echo "Diffing..."
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) echo "Builds are reproducible"
|
||||
|
||||
clean:
|
||||
rm -rf ebin
|
||||
rm -rf lib/*/ebin
|
||||
rm -rf $(PARSER)
|
||||
$(Q) "$(MAKE)" clean_residual_files
|
||||
|
||||
clean_elixir:
|
||||
$(Q) rm -f lib/*/ebin/Elixir.*.beam
|
||||
|
||||
clean_residual_files:
|
||||
rm -rf lib/*/_build/
|
||||
rm -rf lib/*/tmp/
|
||||
rm -rf lib/elixir/test/ebin/
|
||||
rm -rf lib/mix/test/fixtures/deps_on_git_repo/
|
||||
rm -rf lib/mix/test/fixtures/git_rebar/
|
||||
rm -rf lib/mix/test/fixtures/git_repo/
|
||||
rm -rf lib/mix/test/fixtures/git_sparse_repo/
|
||||
rm -rf lib/mix/test/fixtures/archive/ebin/
|
||||
rm -f erl_crash.dump
|
||||
$(Q) "$(MAKE)" clean_man
|
||||
|
||||
#==> Documentation tasks
|
||||
|
||||
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
|
||||
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||
|
||||
docs_elixir: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (elixir)"
|
||||
$(Q) rm -rf doc/elixir
|
||||
$(call DOCS_COMPILE,Elixir,elixir,Kernel,--config "lib/elixir/scripts/elixir_docs.exs")
|
||||
$(call DOCS_CONFIG,elixir)
|
||||
|
||||
docs_eex: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (eex)"
|
||||
$(Q) rm -rf doc/eex
|
||||
$(call DOCS_COMPILE,EEx,eex,EEx,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,eex)
|
||||
|
||||
docs_mix: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (mix)"
|
||||
$(Q) rm -rf doc/mix
|
||||
$(call DOCS_COMPILE,Mix,mix,Mix,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,mix)
|
||||
|
||||
docs_iex: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (iex)"
|
||||
$(Q) rm -rf doc/iex
|
||||
$(call DOCS_COMPILE,IEx,iex,IEx,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,iex)
|
||||
|
||||
docs_ex_unit: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (ex_unit)"
|
||||
$(Q) rm -rf doc/ex_unit
|
||||
$(call DOCS_COMPILE,ExUnit,ex_unit,ExUnit,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,ex_unit)
|
||||
|
||||
docs_logger: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (logger)"
|
||||
$(Q) rm -rf doc/logger
|
||||
$(call DOCS_COMPILE,Logger,logger,Logger,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,logger)
|
||||
|
||||
../ex_doc/bin/ex_doc:
|
||||
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
|
||||
@ false
|
||||
|
||||
#==> Zip tasks
|
||||
|
||||
Docs.zip: docs
|
||||
rm -f Docs.zip
|
||||
zip -9 -r Docs.zip CHANGELOG.md doc NOTICE LICENSE README.md
|
||||
@ echo "Docs file created $(CURDIR)/Docs.zip"
|
||||
|
||||
Precompiled.zip: build_man compile
|
||||
rm -f Precompiled.zip
|
||||
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man NOTICE README.md VERSION
|
||||
@ echo "Precompiled file created $(CURDIR)/Precompiled.zip"
|
||||
|
||||
#==> Test tasks
|
||||
|
||||
# If you modify this task, please update .cirrus.yml accordingly
|
||||
test: test_formatted test_erlang test_elixir
|
||||
|
||||
test_windows: test test_taskkill
|
||||
|
||||
test_taskkill:
|
||||
taskkill //IM erl.exe //F //T //FI "MEMUSAGE gt 0"
|
||||
taskkill //IM epmd.exe //F //T //FI "MEMUSAGE gt 0"
|
||||
|
||||
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)))))
|
||||
|
||||
define FORMAT
|
||||
$(Q) if [ "$(OS)" = "Windows_NT" ]; then \
|
||||
cmd //C call ./bin/mix.bat format $(1); \
|
||||
else \
|
||||
bin/elixir bin/mix format $(1); \
|
||||
fi
|
||||
endef
|
||||
|
||||
format: compile
|
||||
$(call FORMAT)
|
||||
|
||||
test_formatted: compile
|
||||
$(call FORMAT,--check-formatted)
|
||||
|
||||
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_eex test_iex test_mix
|
||||
|
||||
test_stdlib: compile
|
||||
@ echo "==> elixir (ex_unit)"
|
||||
$(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_FILES)"; \
|
||||
else \
|
||||
cd lib/elixir && ../../bin/elixir -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
|
||||
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 crypto runtime_tools ftp tftp mnesia public_key asn1 sasl
|
||||
|
||||
clean_plt:
|
||||
$(Q) rm -f $(PLT)
|
||||
|
||||
build_plt: clean_plt $(PLT)
|
||||
|
||||
dialyze: compile $(PLT)
|
||||
@ echo "==> Dialyzing Elixir..."
|
||||
$(Q) dialyzer -pa lib/elixir/ebin --plt $(PLT) $(DIALYZER_OPTS) lib/*/ebin
|
||||
|
||||
#==> Man page tasks
|
||||
|
||||
build_man: man/iex.1 man/elixir.1
|
||||
|
||||
man/iex.1:
|
||||
$(Q) cp man/iex.1.in man/iex.1
|
||||
$(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
|
||||
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
|
||||
$(Q) rm -f man/iex.1.bak
|
||||
|
||||
man/elixir.1:
|
||||
$(Q) cp man/elixir.1.in man/elixir.1
|
||||
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
|
||||
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
|
||||
$(Q) rm -f man/elixir.1.bak
|
||||
|
||||
clean_man:
|
||||
rm -f man/elixir.1
|
||||
rm -f man/elixir.1.bak
|
||||
rm -f man/iex.1
|
||||
rm -f man/iex.1.bak
|
||||
|
||||
install_man: build_man
|
||||
$(Q) mkdir -p $(DESTDIR)$(MAN_PREFIX)/man1
|
||||
$(Q) $(INSTALL_DATA) man/elixir.1 $(DESTDIR)$(MAN_PREFIX)/man1
|
||||
$(Q) $(INSTALL_DATA) man/elixirc.1 $(DESTDIR)$(MAN_PREFIX)/man1
|
||||
$(Q) $(INSTALL_DATA) man/iex.1 $(DESTDIR)$(MAN_PREFIX)/man1
|
||||
$(Q) $(INSTALL_DATA) man/mix.1 $(DESTDIR)$(MAN_PREFIX)/man1
|
||||
"$(MAKE)" clean_man
|
||||
@@ -1,37 +0,0 @@
|
||||
LEGAL NOTICE INFORMATION
|
||||
------------------------
|
||||
|
||||
All the files in this distribution are copyright to the terms below.
|
||||
|
||||
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
|
||||
|
||||
Copyright Ericsson AB 1996-2015
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
|
||||
== All other files
|
||||
|
||||
Copyright 2012 Plataformatec
|
||||
Copyright 2021 The Elixir Team
|
||||
|
||||
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
|
||||
|
||||
https://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,241 +1,94 @@
|
||||
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png#gh-light-mode-only" width="200" alt="Elixir">
|
||||
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png#gh-dark-mode-only" width="200" alt="Elixir">
|
||||
# n8n-openai-adapter
|
||||
|
||||
[](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI) [](https://cirrus-ci.com/github/elixir-lang/elixir)
|
||||
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.
|
||||
|
||||
Elixir is a dynamic, functional language designed for building scalable
|
||||
and maintainable applications.
|
||||
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.
|
||||
|
||||
For more about Elixir, installation and documentation,
|
||||
[check Elixir's website](https://elixir-lang.org/).
|
||||
## How it works
|
||||
|
||||
## Policies
|
||||
|
||||
New releases are announced in the [announcement mailing list][8].
|
||||
You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com
|
||||
and replying to the confirmation email.
|
||||
|
||||
All security releases [will be tagged with `[security]`][10]. For more
|
||||
information, please read our [Security Policy][9].
|
||||
|
||||
All interactions in our official communication channels follow our
|
||||
[Code of Conduct][1].
|
||||
|
||||
## Bug reports
|
||||
|
||||
For reporting bugs, [visit our issue tracker][2] and follow the steps
|
||||
for reporting a new issue. **Please disclose security vulnerabilities
|
||||
privately at elixir-security@googlegroups.com**.
|
||||
|
||||
## Issues tracker management
|
||||
|
||||
All currently open bugs related to the Elixir repository are listed
|
||||
in the issues tracker. The Elixir team uses the issues tracker to focus
|
||||
on *actionable items*, including planned enhancements in the short- and
|
||||
medium-term. We also do our best to label entries for clarity and to ease
|
||||
collaboration.
|
||||
|
||||
Our *actionable item policy* has some important consequences, such as:
|
||||
|
||||
* Proposing new features as well as request for support, help, and
|
||||
guidance must be done in their own spaces, detailed next.
|
||||
|
||||
* Issues where we have identified to be outside of Elixir scope,
|
||||
such as a bug upstream, will be closed (and requested to be moved
|
||||
elsewhere if appropriate).
|
||||
|
||||
* We actively close unrelated and non-actionable issues to keep the
|
||||
issues tracker tidy. However, we may get things wrong from time to
|
||||
time, so we are glad to revisit issues and reopen if necessary.
|
||||
|
||||
Keep the tone positive and be kind! For more information, see the
|
||||
[Code of Conduct][1].
|
||||
|
||||
### Proposing new features
|
||||
|
||||
For proposing new features, please start a discussion in the
|
||||
[Elixir Core mailing list][3]. Keep in mind that it is your responsibility
|
||||
to argue and explain why a feature is useful and how it will impact the
|
||||
codebase and the community.
|
||||
|
||||
Once a proposal is accepted, it will be added to [the issue tracker][2].
|
||||
Features and bug fixes that have already been merged and will be included
|
||||
in the next release are then "closed" and added to the [changelog][7].
|
||||
|
||||
### Discussions, support, and help
|
||||
|
||||
For general discussions, support, and help, please use many of the community
|
||||
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
|
||||
such as forums, chat platforms, etc, where the wider community will be available
|
||||
to help you.
|
||||
|
||||
## Compiling from source
|
||||
|
||||
For the many different ways to install Elixir,
|
||||
[see our installation instructions on the website](https://elixir-lang.org/install.html).
|
||||
However, if you want to contribute to Elixir, you will need to compile from source.
|
||||
|
||||
First, [install Erlang](https://elixir-lang.org/install.html#installing-erlang).
|
||||
After that, clone this repository to your machine, compile and test it:
|
||||
|
||||
```sh
|
||||
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.
|
||||
|
||||
In case you want to use this Elixir version as your system version,
|
||||
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
|
||||
## Configuration (env vars)
|
||||
|
||||
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`.
|
||||
| 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. |
|
||||
|
||||
## Contributing
|
||||
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.
|
||||
|
||||
We welcome everyone to contribute to Elixir. 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:
|
||||
## Admin API (manage agents at runtime)
|
||||
|
||||
* `elixir` - Elixir's kernel and standard library
|
||||
Agents are persisted to `AGENTS_FILE` and can be added/removed without a
|
||||
redeploy, using the `ADMIN_API_KEY`:
|
||||
|
||||
* `eex` - EEx is the template engine that allows you to embed Elixir
|
||||
```bash
|
||||
# list
|
||||
curl -H "Authorization: Bearer $ADMIN_API_KEY" https://openai.bueso.eu/admin/agents
|
||||
|
||||
* `ex_unit` - ExUnit is a simple test framework that ships with 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
|
||||
|
||||
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
|
||||
|
||||
* `logger` - Logger is the built-in logger
|
||||
|
||||
* `mix` - Mix is 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_#{APPLICATION}`, for example,
|
||||
`make test_ex_unit`. If you just changed something in Elixir's standard
|
||||
library, you can run only that portion through `make test_stdlib`.
|
||||
|
||||
If you are changing just one file, you can choose to 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:
|
||||
|
||||
```sh
|
||||
bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
|
||||
bin/elixir lib/elixir/test/elixir/string_test.exs
|
||||
# remove
|
||||
curl -X DELETE -H "Authorization: Bearer $ADMIN_API_KEY" \
|
||||
https://openai.bueso.eu/admin/agents/media-agent
|
||||
```
|
||||
|
||||
You can also use the `LINE` env var to run a single test:
|
||||
The store is authoritative and persists across restarts; no env config needed.
|
||||
|
||||
```sh
|
||||
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
|
||||
````
|
||||
## Building & running
|
||||
|
||||
To recompile (including Erlang modules):
|
||||
|
||||
```sh
|
||||
make compile
|
||||
```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
|
||||
```
|
||||
|
||||
After your changes are done, please remember to run `make format` to guarantee
|
||||
all files are properly formatted and then run the full suite with
|
||||
`make test`.
|
||||
## Testing
|
||||
|
||||
If your contribution fails during the bootstrapping of the language,
|
||||
you can rebuild the language from scratch with:
|
||||
|
||||
```sh
|
||||
make clean_elixir compile
|
||||
```bash
|
||||
MIX_ENV=test mix test
|
||||
```
|
||||
|
||||
Similarly, if you can't get Elixir to compile or the tests to pass after
|
||||
updating an existing checkout, run `make clean compile`. You can check
|
||||
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
|
||||
More tasks can be found by reading the [Makefile](Makefile).
|
||||
## Nix
|
||||
|
||||
With tests running and passing, you are ready to contribute to Elixir and
|
||||
[send a pull request](https://help.github.com/articles/using-pull-requests/).
|
||||
We have saved some excellent pull requests we have received in the past in
|
||||
case you are looking for some examples:
|
||||
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.:
|
||||
|
||||
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
|
||||
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
|
||||
### Reviewing changes
|
||||
|
||||
Once a pull request is sent, the Elixir team will review your changes.
|
||||
We outline our process below to clarify the roles of everyone involved.
|
||||
|
||||
All pull requests must be approved by two committers before being merged into
|
||||
the repository. If any changes are necessary, the team will leave appropriate
|
||||
comments requesting changes to the code. Unfortunately, we cannot guarantee a
|
||||
pull request will be merged, even when modifications are requested, as the Elixir
|
||||
team will re-evaluate the contribution as it changes.
|
||||
|
||||
Committers may also push style changes directly to your branch. If you would
|
||||
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
|
||||
feature when submitting your pull request.
|
||||
|
||||
The Elixir team may optionally assign someone to review a pull request.
|
||||
If someone is assigned, they must explicitly approve the code before
|
||||
another team member can merge it.
|
||||
|
||||
When the review finishes, your pull request will be squashed and merged
|
||||
into the repository. If you have carefully organized your commits and
|
||||
believe they should be merged without squashing, please mention it in
|
||||
a comment.
|
||||
|
||||
## Building documentation
|
||||
|
||||
Building the documentation requires [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
to be installed and built alongside Elixir:
|
||||
|
||||
```sh
|
||||
# After cloning and compiling Elixir, in its parent directory:
|
||||
git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && ../elixir/bin/mix do deps.get + compile
|
||||
```nix
|
||||
inputs.n8n-openai-adapter.url = "git+https://gitea.bueso.eu/<owner>/n8n-openai-adapter";
|
||||
```
|
||||
|
||||
Now go back to Elixir's root directory and run:
|
||||
|
||||
```sh
|
||||
make docs # to generate HTML pages
|
||||
make docs DOCS_FORMAT=epub # to generate EPUB documents
|
||||
```
|
||||
|
||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
||||
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
|
||||
|
||||
## Development links
|
||||
|
||||
* [Elixir Documentation][6]
|
||||
* [Elixir Core Mailing list (development)][3]
|
||||
* [Announcement mailing list][8]
|
||||
* [Code of Conduct][1]
|
||||
* [Issue tracker][2]
|
||||
* [Changelog][7]
|
||||
* [Security Policy][9]
|
||||
* **[#elixir][4]** on [Libera.Chat][5] IRC
|
||||
|
||||
[1]: CODE_OF_CONDUCT.md
|
||||
[2]: https://github.com/elixir-lang/elixir/issues
|
||||
[3]: https://groups.google.com/group/elixir-lang-core
|
||||
[4]: https://web.libera.chat/#elixir
|
||||
[5]: https://libera.chat
|
||||
[6]: https://elixir-lang.org/docs.html
|
||||
[7]: CHANGELOG.md
|
||||
[8]: https://groups.google.com/group/elixir-lang-ann
|
||||
[9]: SECURITY.md
|
||||
[10]: https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date
|
||||
|
||||
## License
|
||||
|
||||
"Elixir" and the Elixir logo are registered trademarks of The Elixir Team.
|
||||
|
||||
Elixir source code is released under Apache License 2.0.
|
||||
|
||||
Check [NOTICE](NOTICE) and [LICENSE](LICENSE) files for more information.
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
# Release process
|
||||
|
||||
## Shipping a new version
|
||||
|
||||
1. Ensure you are running on the oldest supported Erlang version
|
||||
|
||||
2. Update version in /VERSION, bin/elixir and bin/elixir.bat
|
||||
|
||||
3. Ensure /CHANGELOG.md is updated, versioned and add the current date
|
||||
|
||||
4. Update "Compatibility and Deprecations" if a new OTP version is supported
|
||||
|
||||
5. Commit changes above with title "Release vVERSION", generate a new tag, and push it
|
||||
|
||||
6. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
|
||||
|
||||
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it
|
||||
|
||||
8. Add the release to `elixir.csv` with the minimum supported OTP version (all releases), update `erlang.csv` to the latest supported OTP version, and `_data/elixir-versions.yml` (except for RCs) files in `elixir-lang/elixir-lang.github.com`
|
||||
|
||||
## Creating a new vMAJOR.MINOR branch
|
||||
|
||||
### In the new branch
|
||||
|
||||
1. Set `CANONICAL=` in /Makefile
|
||||
|
||||
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
|
||||
|
||||
3. Commit "Branch out vMAJOR.MINOR"
|
||||
|
||||
### Back in main
|
||||
|
||||
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
|
||||
|
||||
2. Start new /CHANGELOG.md
|
||||
|
||||
3. Update tables in /SECURITY.md and in "Compatibility and Deprecations"
|
||||
|
||||
4. Commit "Start vMAJOR.MINOR+1"
|
||||
|
||||
## Changing supported Erlang/OTP versions
|
||||
|
||||
1. Update the table in Compatibility and Deprecations
|
||||
|
||||
2. Update `otp_release` checks in /Makefile and `/lib/elixir/src/elixir.erl`
|
||||
|
||||
3. Update CI workflows in `/.cirrus.yml`, `/.github/workflows/ci.yml`, and `/.github/workflows/releases.yml`
|
||||
|
||||
4. Remove `otp_release` version checks that are no longer needed
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
Elixir applies bug fixes only to the latest minor branch. Security patches are available for the last 5 minor branches:
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.14 | Bug fixes and security patches
|
||||
1.13 | Security patches only
|
||||
1.12 | Security patches only
|
||||
1.11 | Security patches only
|
||||
1.10 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email.
|
||||
|
||||
Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please disclose security vulnerabilities privately at elixir-security@googlegroups.com
|
||||
-249
@@ -1,249 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.14.5
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
Usage: $(basename "$0") [options] [.exs file] [data]
|
||||
|
||||
## General options
|
||||
|
||||
-e "COMMAND" Evaluates the given command (*)
|
||||
-h, --help Prints this message (standalone)
|
||||
-r "FILE" Requires the given files/patterns (*)
|
||||
-S SCRIPT Finds and executes the given script in \$PATH
|
||||
-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 (*)
|
||||
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
|
||||
|
||||
--app APP Starts the given app and its dependencies (*)
|
||||
--erl "SWITCHES" Switches to be passed down to Erlang (*)
|
||||
--eval "COMMAND" Evaluates the given command, same as -e (*)
|
||||
--logger-otp-reports BOOL Enables or disables OTP reporting
|
||||
--logger-sasl-reports BOOL Enables or disables SASL reporting
|
||||
--no-halt Does not halt the Erlang VM after execution
|
||||
--short-version Prints Elixir version (standalone)
|
||||
--werl Uses Erlang's Windows shell GUI (Windows only)
|
||||
|
||||
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.
|
||||
|
||||
## Distribution options
|
||||
|
||||
The following options are related to node distribution.
|
||||
|
||||
--cookie COOKIE Sets a cookie for this distributed node
|
||||
--hidden Makes a hidden node
|
||||
--name NAME Makes and assigns a name to the distributed node
|
||||
--rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*)
|
||||
--sname NAME Makes and assigns a short name to the distributed node
|
||||
|
||||
## Release options
|
||||
|
||||
The following options are generally used under releases.
|
||||
|
||||
--boot "FILE" Uses the given FILE.boot to start the system
|
||||
--boot-var VAR "VALUE" Makes \$VAR available as VALUE to FILE.boot (*)
|
||||
--erl-config "FILE" Loads configuration in FILE.config written in Erlang (*)
|
||||
--pipe-to "PIPEDIR" "LOGDIR" Starts the Erlang VM as a named PIPEDIR and LOGDIR
|
||||
--vm-args "FILE" Passes the contents in file as arguments to the VM
|
||||
|
||||
--pipe-to starts Elixir detached from console (Unix-like only).
|
||||
It will attempt to create PIPEDIR and LOGDIR if they don't exist.
|
||||
See run_erl to learn more. To reattach, run: to_erl PIPEDIR.
|
||||
|
||||
** Options marked with (*) can be given more than once.
|
||||
** Standalone options can't be combined with other options.
|
||||
USAGE
|
||||
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
|
||||
}
|
||||
|
||||
if [ $# -eq 1 ] && [ "$1" = "--short-version" ]; then
|
||||
echo "$ELIXIR_VERSION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stores static Erlang arguments and --erl (which is passed as is)
|
||||
ERL=""
|
||||
|
||||
# Stores erl arguments preserving spaces/quotes (mimics an array)
|
||||
erl_set () {
|
||||
eval "E${E}=\$1"
|
||||
E=$((E + 1))
|
||||
}
|
||||
|
||||
# Checks if a string starts with prefix. Usage: starts_with "$STRING" "$PREFIX"
|
||||
starts_with () {
|
||||
case $1 in
|
||||
"$2"*) true;;
|
||||
*) false;;
|
||||
esac
|
||||
}
|
||||
|
||||
ERL_EXEC="erl"
|
||||
MODE="elixir"
|
||||
I=1
|
||||
E=0
|
||||
LENGTH=$#
|
||||
set -- "$@" -extra
|
||||
|
||||
while [ $I -le $LENGTH ]; do
|
||||
# S counts to be shifted, C counts to be copied
|
||||
S=0
|
||||
C=0
|
||||
case "$1" in
|
||||
+iex)
|
||||
C=1
|
||||
MODE="iex"
|
||||
;;
|
||||
+elixirc)
|
||||
C=1
|
||||
MODE="elixirc"
|
||||
;;
|
||||
-v|--no-halt|--no-pry)
|
||||
C=1
|
||||
;;
|
||||
-e|-r|-pr|-pa|-pz|--app|--eval|--remsh|--dot-iex)
|
||||
C=2
|
||||
;;
|
||||
--rpc-eval)
|
||||
C=3
|
||||
;;
|
||||
--hidden)
|
||||
S=1
|
||||
ERL="$ERL -hidden"
|
||||
;;
|
||||
--logger-otp-reports)
|
||||
S=2
|
||||
if [ "$2" = 'true' ] || [ "$2" = 'false' ]; then
|
||||
ERL="$ERL -logger handle_otp_reports $2"
|
||||
fi
|
||||
;;
|
||||
--logger-sasl-reports)
|
||||
S=2
|
||||
if [ "$2" = 'true' ] || [ "$2" = 'false' ]; then
|
||||
ERL="$ERL -logger handle_sasl_reports $2"
|
||||
fi
|
||||
;;
|
||||
--erl)
|
||||
S=2
|
||||
ERL="$ERL $2"
|
||||
;;
|
||||
--cookie)
|
||||
S=2
|
||||
erl_set "-setcookie"
|
||||
erl_set "$2"
|
||||
;;
|
||||
--sname|--name)
|
||||
S=2
|
||||
erl_set "$(echo "$1" | cut -c 2-)"
|
||||
erl_set "$2"
|
||||
;;
|
||||
--erl-config)
|
||||
S=2
|
||||
erl_set "-config"
|
||||
erl_set "$2"
|
||||
;;
|
||||
--vm-args)
|
||||
S=2
|
||||
erl_set "-args_file"
|
||||
erl_set "$2"
|
||||
;;
|
||||
--boot)
|
||||
S=2
|
||||
erl_set "-boot"
|
||||
erl_set "$2"
|
||||
;;
|
||||
--boot-var)
|
||||
S=3
|
||||
erl_set "-boot_var"
|
||||
erl_set "$2"
|
||||
erl_set "$3"
|
||||
;;
|
||||
--pipe-to)
|
||||
S=3
|
||||
RUN_ERL_PIPE="$2"
|
||||
RUN_ERL_LOG="$3"
|
||||
if [ "$(starts_with "$RUN_ERL_PIPE" "-")" ]; then
|
||||
echo "--pipe-to : PIPEDIR cannot be a switch" >&2 && exit 1
|
||||
elif [ "$(starts_with "$RUN_ERL_LOG" "-")" ]; then
|
||||
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
|
||||
fi
|
||||
;;
|
||||
--werl)
|
||||
S=1
|
||||
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
|
||||
;;
|
||||
*)
|
||||
while [ $I -le $LENGTH ]; do
|
||||
I=$((I + 1))
|
||||
set -- "$@" "$1"
|
||||
shift
|
||||
done
|
||||
break
|
||||
;;
|
||||
esac
|
||||
|
||||
while [ $I -le $LENGTH ] && [ $C -gt 0 ]; do
|
||||
C=$((C - 1))
|
||||
I=$((I + 1))
|
||||
set -- "$@" "$1"
|
||||
shift
|
||||
done
|
||||
|
||||
I=$((I + S))
|
||||
shift $S
|
||||
done
|
||||
|
||||
I=$((E - 1))
|
||||
while [ $I -ge 0 ]; do
|
||||
eval "VAL=\$E$I"
|
||||
set -- "$VAL" "$@"
|
||||
I=$((I - 1))
|
||||
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
|
||||
|
||||
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
|
||||
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
|
||||
fi
|
||||
|
||||
# One MAY change ERTS_BIN= but you MUST NOT change
|
||||
# ERTS_BIN=$ERTS_BIN as it is handled by Elixir releases.
|
||||
ERTS_BIN=
|
||||
ERTS_BIN="$ERTS_BIN"
|
||||
|
||||
set -- "$ERTS_BIN$ERL_EXEC" -pa "$SCRIPT_PATH"/../lib/*/ebin $ELIXIR_ERL_OPTIONS $ERL "$@"
|
||||
|
||||
if [ -n "$RUN_ERL_PIPE" ]; then
|
||||
ESCAPED=""
|
||||
for PART in "$@"; do
|
||||
ESCAPED="$ESCAPED $(echo "$PART" | sed 's/[^a-zA-Z0-9_\-\/]/\\&/g')"
|
||||
done
|
||||
mkdir -p "$RUN_ERL_PIPE"
|
||||
mkdir -p "$RUN_ERL_LOG"
|
||||
ERL_EXEC="run_erl"
|
||||
set -- "$ERTS_BIN$ERL_EXEC" -daemon "$RUN_ERL_PIPE/" "$RUN_ERL_LOG/" "$ESCAPED"
|
||||
fi
|
||||
|
||||
if [ -n "$ELIXIR_CLI_DRY_RUN" ]; then
|
||||
echo "$@"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||
|
||||
set ELIXIR_VERSION=1.14.5
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""/h"" if ""%2""=="""" goto documentation
|
||||
if ""%1""==""/?"" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--short-version"" if ""%2""=="""" goto shortversion
|
||||
goto parseopts
|
||||
|
||||
:documentation
|
||||
echo Usage: %~nx0 [options] [.exs file] [data]
|
||||
echo.
|
||||
echo ## General options
|
||||
echo.
|
||||
echo -e "COMMAND" Evaluates the given command (*)
|
||||
echo -h, --help Prints this message (standalone)
|
||||
echo -r "FILE" Requires the given files/patterns (*)
|
||||
echo -S SCRIPT Finds and executes the given script in $PATH
|
||||
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 -v, --version Prints Erlang/OTP and Elixir versions (standalone)
|
||||
echo.
|
||||
echo --app APP Starts the given app and its dependencies (*)
|
||||
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
|
||||
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
|
||||
echo --logger-otp-reports BOOL Enables or disables OTP reporting
|
||||
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
|
||||
echo --no-halt Does not halt the Erlang VM after execution
|
||||
echo --short-version Prints Elixir version (standalone)
|
||||
echo --werl Uses Erlang's Windows shell GUI (Windows only)
|
||||
echo.
|
||||
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.
|
||||
echo.
|
||||
echo ## Distribution options
|
||||
echo.
|
||||
echo The following options are related to node distribution.
|
||||
echo.
|
||||
echo --cookie COOKIE Sets a cookie for this distributed node
|
||||
echo --hidden Makes a hidden node
|
||||
echo --name NAME Makes and assigns a name to the distributed node
|
||||
echo --rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*)
|
||||
echo --sname NAME Makes and assigns a short name to the distributed node
|
||||
echo.
|
||||
echo ## Release options
|
||||
echo.
|
||||
echo The following options are generally used under releases.
|
||||
echo.
|
||||
echo --boot "FILE" Uses the given FILE.boot to start the system
|
||||
echo --boot-var VAR "VALUE" Makes $VAR available as VALUE to FILE.boot (*)
|
||||
echo --erl-config "FILE" Loads configuration in FILE.config written in Erlang (*)
|
||||
echo --vm-args "FILE" Passes the contents in file as arguments to the VM
|
||||
echo.
|
||||
echo --pipe-to is not supported on Windows. If set, Elixir won't boot.
|
||||
echo.
|
||||
echo ** Options marked with (*) can be given more than once.
|
||||
echo ** Standalone options can't be combined with other options.
|
||||
goto end
|
||||
|
||||
:shortversion
|
||||
echo !ELIXIR_VERSION!
|
||||
goto end
|
||||
|
||||
:parseopts
|
||||
|
||||
rem Parameters for Elixir
|
||||
set parsElixir=
|
||||
|
||||
rem Parameters for Erlang
|
||||
set parsErlang=
|
||||
|
||||
rem Optional parameters before the "-extra" parameter
|
||||
set beforeExtra=
|
||||
|
||||
rem Option which determines whether the loop is over
|
||||
set endLoop=0
|
||||
|
||||
rem Designates which mode / Elixir component to run as
|
||||
set runMode="elixir"
|
||||
|
||||
rem Designates the path to the current script
|
||||
set SCRIPT_PATH=%~dp0
|
||||
|
||||
rem Designates the path to the ERTS system
|
||||
set ERTS_BIN=
|
||||
set ERTS_BIN=!ERTS_BIN!
|
||||
|
||||
rem Recursive loop called for each parameter that parses the cmd line parameters
|
||||
:startloop
|
||||
set "par=%~1"
|
||||
if "!par!"=="" (
|
||||
rem skip if no parameter
|
||||
goto expand_erl_libs
|
||||
)
|
||||
shift
|
||||
set par="!par:"=\"!"
|
||||
if !endLoop! == 1 (
|
||||
set parsElixir=!parsElixir! !par!
|
||||
goto startloop
|
||||
)
|
||||
rem ******* EXECUTION OPTIONS **********************
|
||||
if !par!=="--werl" (set useWerl=1 && goto startloop)
|
||||
if !par!=="+iex" (set parsElixir=!parsElixir! +iex && set runMode="iex" && goto startloop)
|
||||
if !par!=="+elixirc" (set parsElixir=!parsElixir! +elixirc && set runMode="elixirc" && goto startloop)
|
||||
rem ******* EVAL PARAMETERS ************************
|
||||
if ""==!par:-e=! (
|
||||
set "VAR=%~1"
|
||||
if not defined VAR (set VAR= )
|
||||
set parsElixir=!parsElixir! -e "!VAR:"=\"!"
|
||||
shift
|
||||
goto startloop
|
||||
)
|
||||
if ""==!par:--eval=! (
|
||||
set "VAR=%~1"
|
||||
if not defined VAR (set VAR= )
|
||||
set parsElixir=!parsElixir! --eval "!VAR:"=\"!"
|
||||
shift
|
||||
goto startloop
|
||||
)
|
||||
if ""==!par:--rpc-eval=! (
|
||||
set "VAR=%~2"
|
||||
if not defined VAR (set VAR= )
|
||||
set parsElixir=!parsElixir! --rpc-eval %1 "!VAR:"=\"!"
|
||||
shift
|
||||
shift
|
||||
goto startloop
|
||||
)
|
||||
rem ******* ELIXIR PARAMETERS **********************
|
||||
if ""==!par:-r=! (set "parsElixir=!parsElixir! -r %1" && shift && goto startloop)
|
||||
if ""==!par:-pr=! (set "parsElixir=!parsElixir! -pr %1" && shift && goto startloop)
|
||||
if ""==!par:-pa=! (set "parsElixir=!parsElixir! -pa %1" && shift && goto startloop)
|
||||
if ""==!par:-pz=! (set "parsElixir=!parsElixir! -pz %1" && shift && goto startloop)
|
||||
if ""==!par:-v=! (set "parsElixir=!parsElixir! -v" && goto startloop)
|
||||
if ""==!par:--version=! (set "parsElixir=!parsElixir! --version" && goto startloop)
|
||||
if ""==!par:--app=! (set "parsElixir=!parsElixir! --app %1" && shift && goto startloop)
|
||||
if ""==!par:--no-halt=! (set "parsElixir=!parsElixir! --no-halt" && goto startloop)
|
||||
if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %1" && shift && goto startloop)
|
||||
if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %1" && shift && goto startloop)
|
||||
if ""==!par:--no-pry=! (set "parsElixir=!parsElixir! --no-pry" && goto startloop)
|
||||
rem ******* ERLANG PARAMETERS **********************
|
||||
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot %1" && shift && goto startloop)
|
||||
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var %1 %2" && shift && shift && goto startloop)
|
||||
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie %1" && shift && goto startloop)
|
||||
if ""==!par:--hidden=! (set "parsErlang=!parsErlang! -hidden" && goto startloop)
|
||||
if ""==!par:--detached=! (set "parsErlang=!parsErlang! -detached" && echo warning: the --detached option is deprecated && goto startloop)
|
||||
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config %1" && shift && goto startloop)
|
||||
if ""==!par:--logger-otp-reports=! (set "parsErlang=!parsErlang! -logger handle_otp_reports %1" && shift && goto startloop)
|
||||
if ""==!par:--logger-sasl-reports=! (set "parsErlang=!parsErlang! -logger handle_sasl_reports %1" && shift && goto startloop)
|
||||
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name %1" && shift && goto startloop)
|
||||
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname %1" && shift && goto startloop)
|
||||
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file %1" && shift && goto startloop)
|
||||
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
|
||||
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
|
||||
set endLoop=1
|
||||
set parsElixir=!parsElixir! !par!
|
||||
goto startloop
|
||||
|
||||
:expand_erl_libs
|
||||
rem expand all ebin paths as Windows does not support the ..\*\ebin wildcard
|
||||
set ext_libs=
|
||||
for /d %%d in ("!SCRIPT_PATH!..\lib\*.") do (
|
||||
set ext_libs=!ext_libs! -pa "%%~fd\ebin"
|
||||
)
|
||||
|
||||
:run
|
||||
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
|
||||
if %errorlevel% == 0 (
|
||||
set beforeExtra=-elixir ansi_enabled true !beforeExtra!
|
||||
)
|
||||
if not !runMode! == "iex" (
|
||||
set beforeExtra=-noshell -s elixir start_cli !beforeExtra!
|
||||
)
|
||||
if defined ELIXIR_CLI_DRY_RUN (
|
||||
if defined useWerl (
|
||||
echo start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||
) else (
|
||||
echo "!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||
)
|
||||
) else (
|
||||
if defined useWerl (
|
||||
start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||
) else (
|
||||
"!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||
)
|
||||
)
|
||||
exit /B %ERRORLEVEL%
|
||||
:end
|
||||
endlocal
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
cat <<USAGE >&2
|
||||
Usage: $(basename "$0") [elixir switches] [compiler switches] [.ex files]
|
||||
|
||||
-h, --help Prints this message and exits
|
||||
-o The directory to output compiled files
|
||||
-v, --version Prints Elixir version and exits (standalone)
|
||||
|
||||
--ignore-module-conflict Does not emit warnings if a module was previously defined
|
||||
--no-debug-info Does not attach debug info to compiled modules
|
||||
--no-docs Does not attach documentation to compiled modules
|
||||
--profile time Profile the time to compile modules
|
||||
--verbose Prints compilation status
|
||||
--warnings-as-errors Treats warnings as errors and return non-zero exit status
|
||||
|
||||
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.
|
||||
USAGE
|
||||
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,37 +0,0 @@
|
||||
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||
setlocal
|
||||
set argc=0
|
||||
for %%A in (%*) do (
|
||||
if /I "%%A"=="--help" goto documentation
|
||||
if /I "%%A"=="-h" goto documentation
|
||||
if /I "%%A"=="/h" goto documentation
|
||||
if "%%A"=="/?" 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 -h, --help Prints this message and exits
|
||||
echo -o The directory to output compiled files
|
||||
echo -v, --version Prints Elixir version and exits (standalone)
|
||||
echo.
|
||||
echo --ignore-module-conflict Does not emit warnings if a module was previously defined
|
||||
echo --no-debug-info Does not attach debug info to compiled modules
|
||||
echo --no-docs Does not attach documentation to compiled modules
|
||||
echo --profile time Profile the time to compile modules
|
||||
echo --verbose Prints compilation status
|
||||
echo --warnings-as-errors Treats warnings as errors and returns non-zero exit status
|
||||
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
|
||||
goto end
|
||||
|
||||
:run
|
||||
call "%~dp0\elixir.bat" +elixirc %*
|
||||
|
||||
:end
|
||||
endlocal
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
cat <<USAGE >&2
|
||||
Usage: $(basename "$0") [options] [.exs file] [data]
|
||||
|
||||
The following options are exclusive to IEx:
|
||||
|
||||
--dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment.
|
||||
Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
|
||||
If FILE is empty, then no file will be loaded.
|
||||
--remsh NAME Connects to a node using a remote shell.
|
||||
--no-pry Doesn't start pry sessions when dbg/2 is called.
|
||||
|
||||
It accepts all other options listed by "elixir --help".
|
||||
USAGE
|
||||
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 --no-halt --erl "-noshell -user Elixir.IEx.CLI" +iex "$@"
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||
setlocal
|
||||
if /I ""%1""==""--help"" goto documentation
|
||||
if /I ""%1""==""-h"" goto documentation
|
||||
if /I ""%1""==""/h"" goto documentation
|
||||
if ""%1""==""/?"" goto documentation
|
||||
goto run
|
||||
|
||||
:documentation
|
||||
echo Usage: %~nx0 [options] [.exs file] [data]
|
||||
echo.
|
||||
echo The following options are exclusive to IEx:
|
||||
echo.
|
||||
echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment.
|
||||
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
|
||||
echo If FILE is empty, then no file will be loaded.
|
||||
echo --remsh NAME Connects to a node using a remote shell
|
||||
echo --werl Uses Erlang's Windows shell GUI (Windows only)
|
||||
echo --no-pry Doesn't start pry sessions when dbg/2 is called.
|
||||
echo.
|
||||
echo Set the IEX_WITH_WERL environment variable to always use werl.
|
||||
echo It accepts all other options listed by "elixir --help".
|
||||
goto end
|
||||
|
||||
:run
|
||||
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
|
||||
call "%~dp0\elixir.bat" --no-halt --erl "-noshell -user Elixir.IEx.CLI" +iex %__ELIXIR_IEX_FLAGS% %*
|
||||
:end
|
||||
endlocal
|
||||
@@ -1,2 +0,0 @@
|
||||
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||
call "%~dp0\elixir.bat" "%~dp0\mix" %*
|
||||
-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,347 +0,0 @@
|
||||
defmodule EEx.SyntaxError do
|
||||
defexception [:message, :file, :line, :column]
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
"#{exception.file}:#{exception.line}:#{exception.column}: #{exception.message}"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EEx do
|
||||
@moduledoc ~S"""
|
||||
EEx stands for Embedded Elixir.
|
||||
|
||||
Embedded Elixir allows you to embed Elixir code inside a string
|
||||
in a robust way.
|
||||
|
||||
iex> EEx.eval_string("foo <%= bar %>", bar: "baz")
|
||||
"foo baz"
|
||||
|
||||
This module provides three main APIs for you to use:
|
||||
|
||||
1. Evaluate a string (`eval_string/3`) or a file (`eval_file/3`)
|
||||
directly. This is the simplest API to use but also the
|
||||
slowest, since the code is evaluated at runtime and not precompiled.
|
||||
|
||||
2. Define a function from a string (`function_from_string/5`)
|
||||
or a file (`function_from_file/5`). 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/2`) or a file (`compile_file/2`)
|
||||
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.
|
||||
|
||||
The APIs above support several options, documented below. You may
|
||||
also pass an engine which customizes how the EEx code is compiled.
|
||||
|
||||
## Options
|
||||
|
||||
All functions in this module, unless otherwise noted, accept EEx-related
|
||||
options. They are:
|
||||
|
||||
* `: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.
|
||||
|
||||
* `:line` - the line to be used as the template start. Defaults to `1`.
|
||||
|
||||
* `:indentation` - (since v1.11.0) an integer added to the column after every
|
||||
new line. Defaults to `0`.
|
||||
|
||||
* `:engine` - the EEx engine to be used for compilation.
|
||||
|
||||
* `:trim` - if `true`, trims whitespace left and right of quotation as
|
||||
long as at least one newline is present. All subsequent newlines and
|
||||
spaces are removed but one newline is retained. Defaults to `false`.
|
||||
|
||||
* `:parser_options` - (since: 1.13.0) allow customizing the parsed code
|
||||
that is generated. See `Code.string_to_quoted/2` for available options.
|
||||
Note that the options `:file`, `:line` and `:column` are ignored if
|
||||
passed in. Defaults to `Code.get_compiler_option(:parser_options)`
|
||||
(which defaults to `[]` if not set).
|
||||
|
||||
## Tags
|
||||
|
||||
EEx supports multiple tags, declared below:
|
||||
|
||||
<% Elixir expression: executes code but discards output %>
|
||||
<%= Elixir expression: executes code and prints result %>
|
||||
<%% EEx quotation: returns the contents inside the tag as is %>
|
||||
<%!-- Comments: they are discarded from source --%>
|
||||
|
||||
EEx supports additional tags, that may be used by some engines,
|
||||
but they do not have a meaning by default:
|
||||
|
||||
<%| ... %>
|
||||
<%/ ... %>
|
||||
|
||||
## 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`.
|
||||
|
||||
### `EEx.SmartEngine`
|
||||
|
||||
The smart engine uses EEx default rules and adds the `@` construct
|
||||
for reading template assigns:
|
||||
|
||||
iex> EEx.eval_string("<%= @foo %>", assigns: [foo: 1])
|
||||
"1"
|
||||
|
||||
In other words, `<%= @foo %>` translates to:
|
||||
|
||||
<%= {:ok, v} = Access.fetch(assigns, :foo); v %>
|
||||
|
||||
The `assigns` extension is useful when the number of variables
|
||||
required by the template is not specified at compilation time.
|
||||
"""
|
||||
|
||||
@type line :: non_neg_integer
|
||||
@type column :: non_neg_integer
|
||||
@type marker :: '=' | '/' | '|' | ''
|
||||
@type metadata :: %{column: column, line: line}
|
||||
@type token ::
|
||||
{:comment, charlist, metadata}
|
||||
| {:text, charlist, metadata}
|
||||
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
|
||||
| {:eof, metadata}
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the given string.
|
||||
|
||||
The first argument is the kind of the generated function (`:def` or `:defp`).
|
||||
The `name` argument is the name that the generated function will have.
|
||||
`template` is the string containing the EEx template. `args` is a list of arguments
|
||||
that the generated function will accept. They will be available inside the EEx
|
||||
template.
|
||||
|
||||
The supported `options` are described [in the module docs](#module-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, template, 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(template, 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 first argument is the kind of the generated function (`:def` or `:defp`).
|
||||
The `name` argument is the name that the generated function will have.
|
||||
`file` is the path to the EEx template file. `args` is a list of arguments
|
||||
that the generated function will accept. They will be available inside the EEx
|
||||
template.
|
||||
|
||||
This function is useful in case you have templates but
|
||||
you want to precompile inside a module for speed.
|
||||
|
||||
The supported `options` are described [in the module docs](#module-options).
|
||||
|
||||
## 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([file: IO.chardata_to_string(file), line: 1], options)
|
||||
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 """
|
||||
Gets a string `source` and generates a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
|
||||
This is useful if you want to compile a EEx template into code and inject
|
||||
that code somewhere or evaluate it at runtime.
|
||||
|
||||
The generated quoted code will use variables defined in the template that
|
||||
will be taken from the context where the code is evaluated. If you
|
||||
have a template such as `<%= a + b %>`, then the returned quoted code
|
||||
will use the `a` and `b` variables in the context where it's evaluated. See
|
||||
examples below.
|
||||
|
||||
The supported `options` are described [in the module docs](#module-options).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> quoted = EEx.compile_string("<%= a + b %>")
|
||||
iex> {result, _bindings} = Code.eval_quoted(quoted, a: 1, b: 2)
|
||||
iex> result
|
||||
"3"
|
||||
|
||||
"""
|
||||
@spec compile_string(String.t(), keyword) :: Macro.t()
|
||||
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
|
||||
case tokenize(source, options) do
|
||||
{:ok, tokens} ->
|
||||
EEx.Compiler.compile(tokens, options)
|
||||
|
||||
{:error, message, %{column: column, line: line}} ->
|
||||
file = options[:file] || "nofile"
|
||||
raise EEx.SyntaxError, file: file, line: line, column: column, message: message
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a `filename` and generates a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
|
||||
This is useful if you want to compile a EEx template into code and inject
|
||||
that code somewhere or evaluate it at runtime.
|
||||
|
||||
The generated quoted code will use variables defined in the template that
|
||||
will be taken from the context where the code is evaluated. If you
|
||||
have a template such as `<%= a + b %>`, then the returned quoted code
|
||||
will use the `a` and `b` variables in the context where it's evaluated. See
|
||||
examples below.
|
||||
|
||||
The supported `options` are described [in the module docs](#module-options).
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.eex
|
||||
<%= a + b %>
|
||||
|
||||
# In code:
|
||||
quoted = EEx.compile_file("sample.eex")
|
||||
{result, _bindings} = Code.eval_quoted(quoted, a: 1, b: 2)
|
||||
result
|
||||
#=> "3"
|
||||
|
||||
"""
|
||||
@spec compile_file(Path.t(), keyword) :: Macro.t()
|
||||
def compile_file(filename, options \\ []) when is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
options = Keyword.merge([file: filename, line: 1], options)
|
||||
compile_string(File.read!(filename), options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a string `source` and evaluate the values using the `bindings`.
|
||||
|
||||
The supported `options` are described [in the module docs](#module-options).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> EEx.eval_string("foo <%= bar %>", bar: "baz")
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_string(String.t(), keyword, keyword) :: String.t()
|
||||
def eval_string(source, bindings \\ [], options \\ [])
|
||||
when is_binary(source) and is_list(bindings) and is_list(options) do
|
||||
compiled = compile_string(source, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a `filename` and evaluate the values using the `bindings`.
|
||||
|
||||
The supported `options` are described [in the module docs](#module-options).
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.eex
|
||||
foo <%= bar %>
|
||||
|
||||
# IEx
|
||||
EEx.eval_file("sample.eex", bar: "baz")
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
|
||||
def eval_file(filename, bindings \\ [], options \\ [])
|
||||
when is_list(bindings) and is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
options = Keyword.put_new(options, :file, filename)
|
||||
compiled = compile_file(filename, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Tokenize the given contents according to the given options.
|
||||
|
||||
## Options
|
||||
|
||||
* `:line` - An integer to start as line. Default is 1.
|
||||
* `:column` - An integer to start as column. Default is 1.
|
||||
* `:indentation` - An integer that indicates the indentation. Default is 0.
|
||||
* `:trim` - Tells the tokenizer to either trim the content or not. Default is false.
|
||||
* `:file` - Can be either a file or a string "nofile".
|
||||
|
||||
## Examples
|
||||
|
||||
iex> EEx.tokenize('foo', line: 1, column: 1)
|
||||
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
|
||||
|
||||
## Result
|
||||
|
||||
It returns `{:ok, [token]}` where a token is one of:
|
||||
|
||||
* `{:text, content, %{column: column, line: line}}`
|
||||
* `{:expr, marker, content, %{column: column, line: line}}`
|
||||
* `{:start_expr, marker, content, %{column: column, line: line}}`
|
||||
* `{:middle_expr, marker, content, %{column: column, line: line}}`
|
||||
* `{:end_expr, marker, content, %{column: column, line: line}}`
|
||||
* `{:eof, %{column: column, line: line}}`
|
||||
|
||||
Or `{:error, message, %{column: column, line: line}}` in case of errors.
|
||||
Note new tokens may be added in the future.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec tokenize(IO.chardata(), opts :: keyword) ::
|
||||
{:ok, [token()]} | {:error, String.t(), metadata()}
|
||||
def tokenize(contents, opts \\ []) do
|
||||
EEx.Compiler.tokenize(contents, opts)
|
||||
end
|
||||
|
||||
### Helpers
|
||||
|
||||
defp do_eval(compiled, bindings, options) do
|
||||
{result, _} = Code.eval_quoted(compiled, bindings, options)
|
||||
result
|
||||
end
|
||||
end
|
||||
@@ -1,481 +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
|
||||
@h_spaces [?\s, ?\t]
|
||||
@all_spaces [?\s, ?\t, ?\n, ?\r]
|
||||
|
||||
@doc """
|
||||
Tokenize EEx contents.
|
||||
"""
|
||||
def tokenize(contents, opts) when is_binary(contents) do
|
||||
tokenize(String.to_charlist(contents), opts)
|
||||
end
|
||||
|
||||
def tokenize(contents, opts) when is_list(contents) do
|
||||
file = opts[:file] || "nofile"
|
||||
line = opts[:line] || 1
|
||||
trim = opts[:trim] || false
|
||||
indentation = opts[:indentation] || 0
|
||||
column = indentation + (opts[:column] || 1)
|
||||
|
||||
state = %{trim: trim, indentation: indentation, file: file}
|
||||
|
||||
{contents, line, column} =
|
||||
(trim && trim_init(contents, line, column, state)) || {contents, line, column}
|
||||
|
||||
tokenize(contents, line, column, state, [{line, column}], [])
|
||||
end
|
||||
|
||||
defp tokenize('<%%' ++ t, line, column, state, buffer, acc) do
|
||||
tokenize(t, line, column + 3, state, [?%, ?< | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize('<%!--' ++ t, line, column, state, buffer, acc) do
|
||||
case comment(t, line, column + 5, state, []) do
|
||||
{:error, line, column, message} ->
|
||||
{:error, message, %{line: line, column: column}}
|
||||
|
||||
{:ok, new_line, new_column, rest, comments} ->
|
||||
token = {:comment, Enum.reverse(comments), %{line: line, column: column}}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Deprecate this on Elixir v1.18
|
||||
defp tokenize('<%#' ++ t, line, column, state, buffer, acc) do
|
||||
case expr(t, line, column + 3, state, []) do
|
||||
{:error, line, column, message} ->
|
||||
{:error, message, %{line: line, column: column}}
|
||||
|
||||
{:ok, _, new_line, new_column, rest} ->
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, & &1)
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('<%' ++ t, line, column, state, buffer, acc) do
|
||||
{marker, t} = retrieve_marker(t)
|
||||
|
||||
case expr(t, line, column + 2 + length(marker), state, []) do
|
||||
{:error, line, column, message} ->
|
||||
{:error, message, %{line: line, column: column}}
|
||||
|
||||
{:ok, expr, new_line, new_column, rest} ->
|
||||
{key, expr} =
|
||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, _line, _column, warnings, tokens} ->
|
||||
Enum.each(Enum.reverse(warnings), fn {location, file, msg} ->
|
||||
:elixir_errors.erl_warn(location, file, msg)
|
||||
end)
|
||||
|
||||
token_key(tokens, expr)
|
||||
|
||||
{:error, _, _, _, _} ->
|
||||
{:expr, expr}
|
||||
end
|
||||
|
||||
marker =
|
||||
if key in [:middle_expr, :end_expr] and marker != '' do
|
||||
message =
|
||||
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
|
||||
"please remove \"#{marker}\""
|
||||
|
||||
:elixir_errors.erl_warn({line, column}, state.file, message)
|
||||
''
|
||||
else
|
||||
marker
|
||||
end
|
||||
|
||||
token = {key, marker, expr, %{line: line, column: column}}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('\n' ++ t, line, _column, state, buffer, acc) do
|
||||
tokenize(t, line + 1, state.indentation + 1, state, [?\n | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize([h | t], line, column, state, buffer, acc) do
|
||||
tokenize(t, line, column + 1, state, [h | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize([], line, column, _state, buffer, acc) do
|
||||
eof = {:eof, %{line: line, column: column}}
|
||||
{:ok, Enum.reverse([eof | tokenize_text(buffer, acc)])}
|
||||
end
|
||||
|
||||
defp trim_and_tokenize(rest, line, column, state, buffer, acc, fun) do
|
||||
{rest, line, column, buffer} = trim_if_needed(rest, line, column, state, buffer)
|
||||
|
||||
acc = tokenize_text(buffer, acc)
|
||||
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
|
||||
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
|
||||
{[marker], t}
|
||||
end
|
||||
|
||||
defp retrieve_marker(t) do
|
||||
{'', t}
|
||||
end
|
||||
|
||||
# Tokenize a multi-line comment until we find --%>
|
||||
|
||||
defp comment([?-, ?-, ?%, ?> | t], line, column, _state, buffer) do
|
||||
{:ok, line, column + 4, t, buffer}
|
||||
end
|
||||
|
||||
defp comment('\n' ++ t, line, _column, state, buffer) do
|
||||
comment(t, line + 1, state.indentation + 1, state, '\n' ++ buffer)
|
||||
end
|
||||
|
||||
defp comment([head | t], line, column, state, buffer) do
|
||||
comment(t, line, column + 1, state, [head | buffer])
|
||||
end
|
||||
|
||||
defp comment([], line, column, _state, _buffer) do
|
||||
{:error, line, column, "missing token '--%>'"}
|
||||
end
|
||||
|
||||
# Tokenize an expression until we find %>
|
||||
|
||||
defp expr([?%, ?> | t], line, column, _state, buffer) do
|
||||
{:ok, Enum.reverse(buffer), line, column + 2, t}
|
||||
end
|
||||
|
||||
defp expr('\n' ++ t, line, _column, state, buffer) do
|
||||
expr(t, line + 1, state.indentation + 1, state, [?\n | buffer])
|
||||
end
|
||||
|
||||
defp expr([h | t], line, column, state, buffer) do
|
||||
expr(t, line, column + 1, state, [h | buffer])
|
||||
end
|
||||
|
||||
defp expr([], line, column, _state, _buffer) do
|
||||
{:error, line, column, "missing token '%>'"}
|
||||
end
|
||||
|
||||
# Receives tokens and check if it is a start, middle or an end token.
|
||||
defp token_key(tokens, expr) do
|
||||
case {tokens, tokens |> Enum.reverse() |> drop_eol()} do
|
||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
|
||||
{_, [{:do, _} | _]} ->
|
||||
{:start_expr, maybe_append_space(expr)}
|
||||
|
||||
{_, [{:block_identifier, _, _} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr)}
|
||||
|
||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
|
||||
{_, [{:stab_op, _, _} | reverse_tokens]} ->
|
||||
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
|
||||
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
|
||||
|
||||
if end_index > fn_index do
|
||||
{:start_expr, expr}
|
||||
else
|
||||
{:middle_expr, expr}
|
||||
end
|
||||
|
||||
{tokens, _} ->
|
||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||
[{:end, _} | _] -> {:end_expr, expr}
|
||||
_ -> {:expr, expr}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
|
||||
defp drop_eol(rest), do: rest
|
||||
|
||||
defp maybe_append_space([?\s]), do: [?\s]
|
||||
defp maybe_append_space([h]), do: [h, ?\s]
|
||||
defp maybe_append_space([h | t]), do: [h | maybe_append_space(t)]
|
||||
|
||||
defp closing_bracket?({closing, _}) when closing in ~w"( [ {"a, do: true
|
||||
defp closing_bracket?(_), do: false
|
||||
|
||||
# Tokenize the buffered text by appending
|
||||
# it to the given accumulator.
|
||||
|
||||
defp tokenize_text([{_line, _column}], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp tokenize_text(buffer, acc) do
|
||||
[{line, column} | buffer] = Enum.reverse(buffer)
|
||||
[{:text, buffer, %{line: line, column: column}} | acc]
|
||||
end
|
||||
|
||||
## Trim
|
||||
|
||||
defp trim_if_needed(rest, line, column, state, buffer) do
|
||||
if state.trim do
|
||||
buffer = trim_left(buffer, 0)
|
||||
{rest, line, column} = trim_right(rest, line, column, 0, state)
|
||||
{rest, line, column, buffer}
|
||||
else
|
||||
{rest, line, column, buffer}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_init([h | t], line, column, state) when h in @h_spaces,
|
||||
do: trim_init(t, line, column + 1, state)
|
||||
|
||||
defp trim_init([?\r, ?\n | t], line, _column, state),
|
||||
do: trim_init(t, line + 1, state.indentation + 1, state)
|
||||
|
||||
defp trim_init([?\n | t], line, _column, state),
|
||||
do: trim_init(t, line + 1, state.indentation + 1, state)
|
||||
|
||||
defp trim_init([?<, ?% | _] = rest, line, column, _state),
|
||||
do: {rest, line, column}
|
||||
|
||||
defp trim_init(_, _, _, _), do: false
|
||||
|
||||
defp trim_left(buffer, count) do
|
||||
case trim_whitespace(buffer, 0) do
|
||||
{[?\n, ?\r | rest], _} -> trim_left(rest, count + 1)
|
||||
{[?\n | rest], _} -> trim_left(rest, count + 1)
|
||||
_ when count > 0 -> [?\n | buffer]
|
||||
_ -> buffer
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_right(rest, line, column, last_column, state) do
|
||||
case trim_whitespace(rest, column) do
|
||||
{[?\r, ?\n | rest], column} ->
|
||||
trim_right(rest, line + 1, state.indentation + 1, column + 1, state)
|
||||
|
||||
{[?\n | rest], column} ->
|
||||
trim_right(rest, line + 1, state.indentation + 1, column, state)
|
||||
|
||||
{[], column} ->
|
||||
{[], line, column}
|
||||
|
||||
_ when last_column > 0 ->
|
||||
{[?\n | rest], line - 1, last_column}
|
||||
|
||||
_ ->
|
||||
{rest, line, column}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_whitespace([h | t], column) when h in @h_spaces, do: trim_whitespace(t, column + 1)
|
||||
defp trim_whitespace(list, column), do: {list, column}
|
||||
|
||||
@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.
|
||||
"""
|
||||
@spec compile([EEx.token()], keyword) :: Macro.t()
|
||||
def compile(tokens, opts) do
|
||||
file = opts[:file] || "nofile"
|
||||
line = opts[:line] || 1
|
||||
parser_options = opts[:parser_options] || Code.get_compiler_option(:parser_options)
|
||||
engine = opts[:engine] || @default_engine
|
||||
|
||||
state = %{
|
||||
engine: engine,
|
||||
file: file,
|
||||
line: line,
|
||||
quoted: [],
|
||||
start_line: nil,
|
||||
start_column: nil,
|
||||
parser_options: parser_options
|
||||
}
|
||||
|
||||
init = state.engine.init(opts)
|
||||
generate_buffer(tokens, init, [], state)
|
||||
end
|
||||
|
||||
# Ignore tokens related to comment.
|
||||
defp generate_buffer([{:comment, _chars, _meta} | rest], buffer, scope, state) do
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
# Generates the buffers by handling each expression from the tokenizer.
|
||||
# It returns Macro.t/0 or it raises.
|
||||
|
||||
defp generate_buffer([{:text, chars, meta} | rest], buffer, scope, state) do
|
||||
buffer =
|
||||
if function_exported?(state.engine, :handle_text, 3) do
|
||||
meta = [line: meta.line, column: meta.column]
|
||||
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
|
||||
else
|
||||
# TODO: Deprecate this branch on Elixir v1.18.
|
||||
# We should most likely move this check to init to emit the deprecation once.
|
||||
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
|
||||
end
|
||||
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:expr, mark, chars, meta} | rest], buffer, scope, state) do
|
||||
options =
|
||||
[file: state.file, line: meta.line, column: column(meta.column, mark)] ++
|
||||
state.parser_options
|
||||
|
||||
expr = Code.string_to_quoted!(chars, options)
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:start_expr, mark, chars, meta} | rest],
|
||||
buffer,
|
||||
scope,
|
||||
state
|
||||
) do
|
||||
if mark == '' do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
end
|
||||
|
||||
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
|
||||
|
||||
{contents, rest} =
|
||||
generate_buffer(
|
||||
rest,
|
||||
state.engine.handle_begin(buffer),
|
||||
[contents | scope],
|
||||
%{
|
||||
state
|
||||
| quoted: [],
|
||||
line: line,
|
||||
start_line: meta.line,
|
||||
start_column: column(meta.column, mark)
|
||||
}
|
||||
)
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:middle_expr, '', chars, meta} | rest],
|
||||
buffer,
|
||||
[current | scope],
|
||||
state
|
||||
) do
|
||||
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
|
||||
state = %{state | line: meta.line}
|
||||
generate_buffer(rest, state.engine.handle_begin(buffer), [wrapped | scope], state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:middle_expr, _, chars, meta} | _], _buffer, [], state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected middle of expression <%#{chars}%>",
|
||||
file: state.file,
|
||||
line: meta.line,
|
||||
column: meta.column
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:end_expr, '', chars, meta} | rest],
|
||||
buffer,
|
||||
[current | _],
|
||||
state
|
||||
) do
|
||||
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
|
||||
column = state.start_column
|
||||
options = [file: state.file, line: state.start_line, column: column] ++ state.parser_options
|
||||
tuples = Code.string_to_quoted!(wrapped, options)
|
||||
buffer = insert_quoted(tuples, state.quoted)
|
||||
{buffer, rest}
|
||||
end
|
||||
|
||||
defp generate_buffer([{:end_expr, _, chars, meta} | _], _buffer, [], state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected end of expression <%#{chars}%>",
|
||||
file: state.file,
|
||||
line: meta.line,
|
||||
column: meta.column
|
||||
end
|
||||
|
||||
defp generate_buffer([{:eof, _meta}], buffer, [], state) do
|
||||
state.engine.handle_body(buffer)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:eof, meta}], _buffer, _scope, state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected end of string, expected a closing '<% end %>'",
|
||||
file: state.file,
|
||||
line: meta.line,
|
||||
column: meta.column
|
||||
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_charlist(key) ++ ');'
|
||||
count = current ++ placeholder ++ new_lines ++ chars
|
||||
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
|
||||
|
||||
{count, new_state}
|
||||
end
|
||||
|
||||
# Look middle expressions that immediately follow a start_expr
|
||||
|
||||
defp look_ahead_middle([{:comment, _comment, _meta} | rest], start, contents),
|
||||
do: look_ahead_middle(rest, start, contents)
|
||||
|
||||
defp look_ahead_middle([{:text, text, _meta} | rest], start, contents) do
|
||||
if only_spaces?(text) do
|
||||
look_ahead_middle(rest, start, contents ++ text)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp look_ahead_middle([{:middle_expr, _, chars, meta} | rest], _start, contents) do
|
||||
{rest, meta.line, contents ++ chars}
|
||||
end
|
||||
|
||||
defp look_ahead_middle(_tokens, _start, _contents) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp only_spaces?(chars) do
|
||||
Enum.all?(chars, &(&1 in @all_spaces))
|
||||
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
|
||||
|
||||
defp column(column, mark) do
|
||||
# length('<%') == 2
|
||||
column + 2 + length(mark)
|
||||
end
|
||||
end
|
||||
@@ -1,221 +0,0 @@
|
||||
defmodule EEx.Engine do
|
||||
@moduledoc ~S"""
|
||||
Basic EEx engine that ships with Elixir.
|
||||
|
||||
An engine needs to implement all callbacks below.
|
||||
|
||||
This module also ships with a default engine implementation
|
||||
you can delegate to. See `EEx.SmartEngine` as an example.
|
||||
"""
|
||||
|
||||
@type state :: term
|
||||
|
||||
@doc """
|
||||
Called at the beginning of every template.
|
||||
|
||||
It must return the initial state.
|
||||
"""
|
||||
@callback init(opts :: keyword) :: state
|
||||
|
||||
@doc """
|
||||
Called at the end of every template.
|
||||
|
||||
It must return Elixir's quoted expressions for the template.
|
||||
"""
|
||||
@callback handle_body(state) :: Macro.t()
|
||||
|
||||
@doc """
|
||||
Called for the text/static parts of a template.
|
||||
|
||||
It must return the updated state.
|
||||
"""
|
||||
@callback handle_text(state, [line: pos_integer, column: pos_integer], text :: String.t()) ::
|
||||
state
|
||||
|
||||
@doc """
|
||||
Called for the dynamic/code parts of a template.
|
||||
|
||||
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:
|
||||
|
||||
* `""`
|
||||
* `"="`
|
||||
* `"/"`
|
||||
* `"|"`
|
||||
|
||||
Markers `"/"` and `"|"` are only for use in custom EEx engines
|
||||
and are not implemented by default. Using them without an
|
||||
appropriate implementation raises `EEx.SyntaxError`.
|
||||
|
||||
It must return the updated state.
|
||||
"""
|
||||
@callback handle_expr(state, marker :: String.t(), expr :: Macro.t()) :: state
|
||||
|
||||
@doc """
|
||||
Invoked at the beginning of every nesting.
|
||||
|
||||
It must return a new state that is used only inside the nesting.
|
||||
Once the nesting terminates, the current `state` is resumed.
|
||||
"""
|
||||
@callback handle_begin(state) :: state
|
||||
|
||||
@doc """
|
||||
Invokes at the end of a nesting.
|
||||
|
||||
It must return Elixir's quoted expressions for the nesting.
|
||||
"""
|
||||
@callback handle_end(state) :: Macro.t()
|
||||
|
||||
@doc false
|
||||
@deprecated "Use explicit delegation to EEx.Engine instead"
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
@behaviour EEx.Engine
|
||||
|
||||
def init(opts) do
|
||||
EEx.Engine.init(opts)
|
||||
end
|
||||
|
||||
def handle_body(state) do
|
||||
EEx.Engine.handle_body(state)
|
||||
end
|
||||
|
||||
def handle_begin(state) do
|
||||
EEx.Engine.handle_begin(state)
|
||||
end
|
||||
|
||||
def handle_end(state) do
|
||||
EEx.Engine.handle_end(state)
|
||||
end
|
||||
|
||||
def handle_text(state, text) do
|
||||
EEx.Engine.handle_text(state, [], text)
|
||||
end
|
||||
|
||||
def handle_expr(state, marker, expr) do
|
||||
EEx.Engine.handle_expr(state, marker, expr)
|
||||
end
|
||||
|
||||
defoverridable EEx.Engine
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles assigns in quoted expressions.
|
||||
|
||||
A warning will be printed on missing assigns.
|
||||
Future versions will raise.
|
||||
|
||||
This can be added to any custom engine by invoking
|
||||
`handle_assign/1` with `Macro.prewalk/2`:
|
||||
|
||||
def handle_expr(state, token, expr) do
|
||||
expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
|
||||
super(state, token, expr)
|
||||
end
|
||||
|
||||
"""
|
||||
@spec handle_assign(Macro.t()) :: Macro.t()
|
||||
def handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
|
||||
line = meta[:line] || 0
|
||||
quote(line: line, do: EEx.Engine.fetch_assign!(var!(assigns), unquote(name)))
|
||||
end
|
||||
|
||||
def handle_assign(arg) do
|
||||
arg
|
||||
end
|
||||
|
||||
@doc false
|
||||
# TODO: Raise on v2.0
|
||||
@spec fetch_assign!(Access.t(), Access.key()) :: term | nil
|
||||
def fetch_assign!(assigns, key) do
|
||||
case Access.fetch(assigns, key) do
|
||||
{:ok, val} ->
|
||||
val
|
||||
|
||||
:error ->
|
||||
keys = Enum.map(assigns, &elem(&1, 0))
|
||||
|
||||
IO.warn(
|
||||
"assign @#{key} not available in EEx template. " <>
|
||||
"Please ensure all assigns are given as options. " <>
|
||||
"Available assigns: #{inspect(keys)}"
|
||||
)
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:init/1`."
|
||||
def init(_opts) do
|
||||
%{
|
||||
binary: [],
|
||||
dynamic: [],
|
||||
vars_count: 0
|
||||
}
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_begin/1`."
|
||||
def handle_begin(state) do
|
||||
check_state!(state)
|
||||
%{state | binary: [], dynamic: []}
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_end/1`."
|
||||
def handle_end(quoted) do
|
||||
handle_body(quoted)
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_body/1`."
|
||||
def handle_body(state) do
|
||||
check_state!(state)
|
||||
%{binary: binary, dynamic: dynamic} = state
|
||||
binary = {:<<>>, [], Enum.reverse(binary)}
|
||||
dynamic = [binary | dynamic]
|
||||
{:__block__, [], Enum.reverse(dynamic)}
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_text/3`."
|
||||
def handle_text(state, _meta, text) do
|
||||
check_state!(state)
|
||||
%{binary: binary} = state
|
||||
%{state | binary: [text | binary]}
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_expr/3`."
|
||||
def handle_expr(state, "=", ast) do
|
||||
check_state!(state)
|
||||
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
|
||||
var = Macro.var(:"arg#{vars_count}", __MODULE__)
|
||||
|
||||
ast =
|
||||
quote do
|
||||
unquote(var) = String.Chars.to_string(unquote(ast))
|
||||
end
|
||||
|
||||
segment =
|
||||
quote do
|
||||
unquote(var) :: binary
|
||||
end
|
||||
|
||||
%{state | dynamic: [ast | dynamic], binary: [segment | binary], vars_count: vars_count + 1}
|
||||
end
|
||||
|
||||
def handle_expr(state, "", ast) do
|
||||
%{dynamic: dynamic} = state
|
||||
%{state | dynamic: [ast | dynamic]}
|
||||
end
|
||||
|
||||
def handle_expr(_state, marker, _ast) when marker in ["/", "|"] do
|
||||
raise EEx.SyntaxError,
|
||||
"unsupported EEx syntax <%#{marker} %> (the syntax is valid but not supported by the current EEx engine)"
|
||||
end
|
||||
|
||||
defp check_state!(%{binary: _, dynamic: _, vars_count: _}), do: :ok
|
||||
|
||||
defp check_state!(state) do
|
||||
raise "unexpected EEx.Engine state: #{inspect(state)}. " <>
|
||||
"This typically means a bug or an outdated EEx.Engine or tool"
|
||||
end
|
||||
end
|
||||
@@ -1,58 +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 because
|
||||
a template, after being compiled, can receive different
|
||||
assigns and would not require recompilation 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"
|
||||
|
||||
"""
|
||||
|
||||
@behaviour EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate init(opts), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate handle_body(state), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate handle_begin(state), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate handle_end(state), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate handle_text(state, meta, text), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
def handle_expr(state, marker, expr) do
|
||||
expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
|
||||
EEx.Engine.handle_expr(state, marker, expr)
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
defmodule EEx.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :eex,
|
||||
version: System.version(),
|
||||
build_per_environment: false
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -1,59 +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 "error with missing assigns" do
|
||||
stderr =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
assert_eval("", "<%= @foo %>", assigns: %{})
|
||||
end)
|
||||
|
||||
assert stderr =~ "assign @foo not available in EEx template"
|
||||
end
|
||||
|
||||
test "evaluates with loops" do
|
||||
assert_eval("1\n2\n3\n", "<%= for x <- [1, 2, 3] do %><%= x %>\n<% end %>")
|
||||
end
|
||||
|
||||
test "preserves line numbers in assignments" do
|
||||
result = EEx.compile_string("foo\n<%= @hello %>", engine: EEx.SmartEngine)
|
||||
|
||||
Macro.prewalk(result, fn
|
||||
{_left, meta, [_, :hello]} ->
|
||||
assert Keyword.get(meta, :line) == 2
|
||||
send(self(), :found)
|
||||
|
||||
node ->
|
||||
node
|
||||
end)
|
||||
|
||||
assert_received :found
|
||||
end
|
||||
|
||||
test "error with unused \"do\" block without \"<%=\" modifier" do
|
||||
stderr =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
|
||||
end)
|
||||
|
||||
assert stderr =~ "the contents of this expression won't be output"
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
|
||||
assert result == expected
|
||||
end
|
||||
end
|
||||
@@ -1,395 +0,0 @@
|
||||
Code.require_file("../test_helper.exs", __DIR__)
|
||||
|
||||
defmodule EEx.TokenizerTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
@opts [indentation: 0, trim: false]
|
||||
|
||||
test "simple chars lists" do
|
||||
assert EEx.tokenize('foo', @opts) ==
|
||||
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
|
||||
end
|
||||
|
||||
test "simple strings" do
|
||||
assert EEx.tokenize("foo", @opts) ==
|
||||
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
|
||||
end
|
||||
|
||||
test "strings with embedded code" do
|
||||
assert EEx.tokenize('foo <% bar %>', @opts) ==
|
||||
{:ok,
|
||||
[
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:expr, '', ' bar ', %{column: 5, line: 1}},
|
||||
{:eof, %{column: 14, line: 1}}
|
||||
]}
|
||||
end
|
||||
|
||||
test "strings with embedded equals code" do
|
||||
assert EEx.tokenize('foo <%= bar %>', @opts) ==
|
||||
{:ok,
|
||||
[
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:expr, '=', ' bar ', %{column: 5, line: 1}},
|
||||
{:eof, %{column: 15, line: 1}}
|
||||
]}
|
||||
end
|
||||
|
||||
test "strings with embedded slash code" do
|
||||
assert EEx.tokenize('foo <%/ bar %>', @opts) ==
|
||||
{:ok,
|
||||
[
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:expr, '/', ' bar ', %{column: 5, line: 1}},
|
||||
{:eof, %{column: 15, line: 1}}
|
||||
]}
|
||||
end
|
||||
|
||||
test "strings with embedded pipe code" do
|
||||
assert EEx.tokenize('foo <%| bar %>', @opts) ==
|
||||
{:ok,
|
||||
[
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:expr, '|', ' bar ', %{column: 5, line: 1}},
|
||||
{:eof, %{column: 15, line: 1}}
|
||||
]}
|
||||
end
|
||||
|
||||
test "strings with more than one line" do
|
||||
assert EEx.tokenize('foo\n<%= bar %>', @opts) ==
|
||||
{:ok,
|
||||
[
|
||||
{:text, 'foo\n', %{column: 1, line: 1}},
|
||||
{:expr, '=', ' bar ', %{column: 1, line: 2}},
|
||||
{:eof, %{column: 11, line: 2}}
|
||||
]}
|
||||
end
|
||||
|
||||
test "strings with more than one line and expression with more than one line" do
|
||||
string = '''
|
||||
foo <%= bar
|
||||
|
||||
baz %>
|
||||
<% foo %>
|
||||
'''
|
||||
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:expr, '=', ' bar\n\nbaz ', %{column: 5, line: 1}},
|
||||
{:text, '\n', %{column: 7, line: 3}},
|
||||
{:expr, '', ' foo ', %{column: 1, line: 4}},
|
||||
{:text, '\n', %{column: 10, line: 4}},
|
||||
{:eof, %{column: 1, line: 5}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(string, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "quotation" do
|
||||
assert EEx.tokenize('foo <%% true %>', @opts) ==
|
||||
{:ok,
|
||||
[
|
||||
{:text, 'foo <% true %>', %{column: 1, line: 1}},
|
||||
{:eof, %{column: 16, line: 1}}
|
||||
]}
|
||||
end
|
||||
|
||||
test "quotation with do-end" do
|
||||
assert EEx.tokenize('foo <%% true do %>bar<%% end %>', @opts) ==
|
||||
{:ok,
|
||||
[
|
||||
{:text, 'foo <% true do %>bar<% end %>', %{column: 1, line: 1}},
|
||||
{:eof, %{column: 32, line: 1}}
|
||||
]}
|
||||
end
|
||||
|
||||
test "quotation with interpolation" do
|
||||
exprs = [
|
||||
{:text, 'a <% b ', %{column: 1, line: 1}},
|
||||
{:expr, '=', ' c ', %{column: 9, line: 1}},
|
||||
{:text, ' ', %{column: 17, line: 1}},
|
||||
{:expr, '=', ' d ', %{column: 18, line: 1}},
|
||||
{:text, ' e %> f', %{column: 26, line: 1}},
|
||||
{:eof, %{column: 33, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('a <%% b <%= c %> <%= d %> e %> f', @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "improperly formatted quotation with interpolation" do
|
||||
exprs = [
|
||||
{:text, '<%% a <%= b %> c %>', %{column: 1, line: 1}},
|
||||
{:eof, %{column: 22, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('<%%% a <%%= b %> c %>', @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "EEx comments" do
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:eof, %{column: 16, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <%# true %>', @opts) == {:ok, exprs}
|
||||
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:eof, %{column: 8, line: 2}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <%#\ntrue %>', @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "EEx comments with do-end" do
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:text, 'bar', %{column: 19, line: 1}},
|
||||
{:eof, %{column: 32, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <%# true do %>bar<%# end %>', @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "EEx comments inside do-end" do
|
||||
exprs = [
|
||||
{:start_expr, '', ' if true do ', %{column: 1, line: 1}},
|
||||
{:text, 'bar', %{column: 31, line: 1}},
|
||||
{:end_expr, [], ' end ', %{column: 34, line: 1}},
|
||||
{:eof, %{column: 43, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('<% if true do %><%# comment %>bar<% end %>', @opts) == {:ok, exprs}
|
||||
|
||||
exprs = [
|
||||
{:start_expr, [], ' case true do ', %{column: 1, line: 1}},
|
||||
{:middle_expr, '', ' true -> ', %{column: 33, line: 1}},
|
||||
{:text, 'bar', %{column: 46, line: 1}},
|
||||
{:end_expr, [], ' end ', %{column: 49, line: 1}},
|
||||
{:eof, %{column: 58, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('<% case true do %><%# comment %><% true -> %>bar<% end %>', @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "EEx multi-line comments" do
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:comment, ' true ', %{column: 5, line: 1}},
|
||||
{:text, ' bar', %{column: 20, line: 1}},
|
||||
{:eof, %{column: 24, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <%!-- true --%> bar', @opts) == {:ok, exprs}
|
||||
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:comment, ' \ntrue\n ', %{column: 5, line: 1}},
|
||||
{:text, ' bar', %{column: 6, line: 3}},
|
||||
{:eof, %{column: 10, line: 3}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <%!-- \ntrue\n --%> bar', @opts) == {:ok, exprs}
|
||||
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:comment, ' <%= true %> ', %{column: 5, line: 1}},
|
||||
{:text, ' bar', %{column: 27, line: 1}},
|
||||
{:eof, %{column: 31, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <%!-- <%= true %> --%> bar', @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "Elixir comments" do
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:expr, [], ' true # this is a boolean ', %{column: 5, line: 1}},
|
||||
{:eof, %{column: 35, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <% true # this is a boolean %>', @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "Elixir comments with do-end" do
|
||||
exprs = [
|
||||
{:start_expr, [], ' if true do # startif ', %{column: 1, line: 1}},
|
||||
{:text, 'text', %{column: 27, line: 1}},
|
||||
{:end_expr, [], ' end # closeif ', %{column: 31, line: 1}},
|
||||
{:eof, %{column: 50, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('<% if true do # startif %>text<% end # closeif %>', @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded do end" do
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:start_expr, '', ' if true do ', %{column: 5, line: 1}},
|
||||
{:text, 'bar', %{column: 21, line: 1}},
|
||||
{:end_expr, '', ' end ', %{column: 24, line: 1}},
|
||||
{:eof, %{column: 33, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <% if true do %>bar<% end %>', @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded -> end" do
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:start_expr, '', ' cond do ', %{column: 5, line: 1}},
|
||||
{:middle_expr, '', ' false -> ', %{column: 18, line: 1}},
|
||||
{:text, 'bar', %{column: 32, line: 1}},
|
||||
{:middle_expr, '', ' true -> ', %{column: 35, line: 1}},
|
||||
{:text, 'baz', %{column: 48, line: 1}},
|
||||
{:end_expr, '', ' end ', %{column: 51, line: 1}},
|
||||
{:eof, %{column: 60, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with fn-end with newline" do
|
||||
exprs = [
|
||||
{:start_expr, '=', ' a fn ->\n', %{column: 1, line: 1}},
|
||||
{:text, 'foo', %{column: 3, line: 2}},
|
||||
{:end_expr, [], ' end ', %{column: 6, line: 2}},
|
||||
{:eof, %{column: 15, line: 2}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('<%= a fn ->\n%>foo<% end %>', @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with multiple fn-end" do
|
||||
exprs = [
|
||||
{:start_expr, '=', ' a fn -> ', %{column: 1, line: 1}},
|
||||
{:text, 'foo', %{column: 15, line: 1}},
|
||||
{:middle_expr, '', ' end, fn -> ', %{column: 18, line: 1}},
|
||||
{:text, 'bar', %{column: 34, line: 1}},
|
||||
{:end_expr, '', ' end ', %{column: 37, line: 1}},
|
||||
{:eof, %{column: 46, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with fn-end followed by do block" do
|
||||
exprs = [
|
||||
{:start_expr, '=', ' a fn -> ', %{column: 1, line: 1}},
|
||||
{:text, 'foo', %{column: 15, line: 1}},
|
||||
{:middle_expr, '', ' end do ', %{column: 18, line: 1}},
|
||||
{:text, 'bar', %{column: 30, line: 1}},
|
||||
{:end_expr, '', ' end ', %{column: 33, line: 1}},
|
||||
{:eof, %{column: 42, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded keywords blocks" do
|
||||
exprs = [
|
||||
{:text, 'foo ', %{column: 1, line: 1}},
|
||||
{:start_expr, '', ' if true do ', %{column: 5, line: 1}},
|
||||
{:text, 'bar', %{column: 21, line: 1}},
|
||||
{:middle_expr, '', ' else ', %{column: 24, line: 1}},
|
||||
{:text, 'baz', %{column: 34, line: 1}},
|
||||
{:end_expr, '', ' end ', %{column: 37, line: 1}},
|
||||
{:eof, %{column: 46, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode" do
|
||||
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> \n\n '
|
||||
|
||||
exprs = [
|
||||
{:start_expr, '=', ' if true do ', %{column: 2, line: 1}},
|
||||
{:text, '\n TRUE \n', %{column: 20, line: 1}},
|
||||
{:middle_expr, '', ' else ', %{column: 3, line: 3}},
|
||||
{:text, '\n FALSE \n', %{column: 13, line: 3}},
|
||||
{:end_expr, '', ' end ', %{column: 3, line: 5}},
|
||||
{:eof, %{column: 3, line: 7}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with comment" do
|
||||
exprs = [
|
||||
{:text, '\n123', %{column: 19, line: 1}},
|
||||
{:eof, %{column: 4, line: 2}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(' <%# comment %> \n123', [trim: true] ++ @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with multi-line comment" do
|
||||
exprs = [
|
||||
{:comment, ' comment ', %{column: 3, line: 1}},
|
||||
{:text, '\n123', %{column: 23, line: 1}},
|
||||
{:eof, %{column: 4, line: 2}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(' <%!-- comment --%> \n123', [trim: true] ++ @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with CRLF" do
|
||||
exprs = [
|
||||
{:text, '0\n', %{column: 1, line: 1}},
|
||||
{:expr, '=', ' 12 ', %{column: 3, line: 2}},
|
||||
{:text, '\n34', %{column: 15, line: 2}},
|
||||
{:eof, %{column: 3, line: 3}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize('0\r\n <%= 12 %> \r\n34', [trim: true] ++ @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode set to false" do
|
||||
exprs = [
|
||||
{:text, ' ', %{column: 1, line: 1}},
|
||||
{:expr, '=', ' 12 ', %{column: 2, line: 1}},
|
||||
{:text, ' \n', %{column: 11, line: 1}},
|
||||
{:eof, %{column: 1, line: 2}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(' <%= 12 %> \n', [trim: false] ++ @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode no false positives" do
|
||||
assert_not_trimmed = fn x ->
|
||||
assert EEx.tokenize(x, [trim: false] ++ @opts) == EEx.tokenize(x, @opts)
|
||||
end
|
||||
|
||||
assert_not_trimmed.('foo <%= "bar" %> ')
|
||||
assert_not_trimmed.('\n <%= "foo" %>bar')
|
||||
assert_not_trimmed.(' <%% hello %> ')
|
||||
assert_not_trimmed.(' <%= 01 %><%= 23 %>\n')
|
||||
end
|
||||
|
||||
test "returns error when there is start mark and no end mark" do
|
||||
assert EEx.tokenize('foo <% :bar', @opts) ==
|
||||
{:error, "missing token '%>'", %{column: 12, line: 1}}
|
||||
|
||||
assert EEx.tokenize('<%# true ', @opts) ==
|
||||
{:error, "missing token '%>'", %{column: 10, line: 1}}
|
||||
|
||||
assert EEx.tokenize('<%!-- foo ', @opts) ==
|
||||
{:error, "missing token '--%>'", %{column: 11, line: 1}}
|
||||
end
|
||||
|
||||
test "marks invalid expressions as regular expressions" do
|
||||
assert EEx.tokenize('<% 1 $ 2 %>', @opts) ==
|
||||
{:ok,
|
||||
[
|
||||
{:expr, [], ' 1 $ 2 ', %{column: 1, line: 1}},
|
||||
{:eof, %{column: 12, line: 1}}
|
||||
]}
|
||||
end
|
||||
end
|
||||
@@ -1,786 +0,0 @@
|
||||
Code.require_file("test_helper.exs", __DIR__)
|
||||
|
||||
require EEx
|
||||
|
||||
defmodule EExTest.Compiled do
|
||||
def before_compile do
|
||||
{__ENV__.line, hd(tl(get_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
|
||||
{__ENV__.line, hd(tl(get_stacktrace()))}
|
||||
end
|
||||
|
||||
@file "unknown"
|
||||
def unknown do
|
||||
{__ENV__.line, hd(tl(get_stacktrace()))}
|
||||
end
|
||||
|
||||
defp get_stacktrace do
|
||||
try do
|
||||
:erlang.error("failed")
|
||||
rescue
|
||||
_ -> __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
|
||||
|
||||
describe "evaluates" do
|
||||
test "simple string" do
|
||||
assert_eval("foo bar", "foo bar")
|
||||
end
|
||||
|
||||
test "Unicode" do
|
||||
template = """
|
||||
• <%= "•" %> •
|
||||
<%= "Jößé Vâlìm" %> Jößé Vâlìm
|
||||
"""
|
||||
|
||||
assert_eval(" • • •\n Jößé Vâlìm Jößé Vâlìm\n", template)
|
||||
end
|
||||
|
||||
test "no spaces" do
|
||||
string = """
|
||||
<%=cond do%>
|
||||
<%false ->%>
|
||||
this
|
||||
<%true ->%>
|
||||
that
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = "\n that\n\n"
|
||||
assert_eval(expected, string, [])
|
||||
end
|
||||
|
||||
test "trim mode" do
|
||||
string = "<%= 123 %> \n \n <%= 789 %>"
|
||||
expected = "123\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "<%= 123 %> \n456\n <%= 789 %>"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "<%= 123 %> \n\n456\n\n <%= 789 %>"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "<%= 123 %> \n \n456\n \n <%= 789 %>"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "\n <%= 123 %> \n <%= 456 %> \n <%= 789 %> \n"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "\r\n <%= 123 %> \r\n <%= 456 %> \r\n <%= 789 %> \r\n"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
test "trim mode with middle expression" do
|
||||
string = """
|
||||
<%= cond do %>
|
||||
<% false -> %>
|
||||
this
|
||||
<% true -> %>
|
||||
that
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
expected = "\n that\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
test "trim mode with multiple lines" do
|
||||
string = """
|
||||
<%= "First line" %>
|
||||
<%= "Second line" %>
|
||||
<%= "Third line" %>
|
||||
<%= "Fourth line" %>
|
||||
"""
|
||||
|
||||
expected = "First line\nSecond line\nThird line\nFourth line"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
test "trim mode with no spaces" do
|
||||
string = """
|
||||
<%=if true do%>
|
||||
this
|
||||
<%else%>
|
||||
that
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = "\n this\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = """
|
||||
<%=cond do%>
|
||||
<%false ->%>
|
||||
this
|
||||
<%true ->%>
|
||||
that
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = "\n that\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
test "embedded code" do
|
||||
assert_eval("foo bar", "foo <%= :bar %>")
|
||||
end
|
||||
|
||||
test "embedded code with binding" do
|
||||
assert EEx.eval_string("foo <%= bar %>", bar: 1) == "foo 1"
|
||||
end
|
||||
|
||||
test "embedded code with do end when true" do
|
||||
assert_eval("foo bar", "foo <%= if true do %>bar<% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with do end when false" do
|
||||
assert_eval("foo ", "foo <%= if false do %>bar<% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with do preceded by bracket" do
|
||||
assert_eval("foo bar", "foo <%= if {true}do %>bar<% end %>")
|
||||
assert_eval("foo bar", "foo <%= if (true)do %>bar<% end %>")
|
||||
assert_eval("foo bar", "foo <%= if [true]do %>bar<% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with do end and expression" do
|
||||
assert_eval("foo bar", "foo <%= if true do %><%= :bar %><% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with do end and multiple 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 "embedded code with middle expression" do
|
||||
assert_eval("foo bar", "foo <%= if true do %>bar<% else %>baz<% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with evaluated middle expression" do
|
||||
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with comments in do end" do
|
||||
assert_eval("foo bar", "foo <%= case true do %><%# comment %><% true -> %>bar<% end %>")
|
||||
|
||||
assert_eval(
|
||||
"foo\n\nbar\n",
|
||||
"foo\n<%= case true do %>\n<%# comment %>\n<% true -> %>\nbar\n<% end %>"
|
||||
)
|
||||
end
|
||||
|
||||
test "embedded code with multi-line comments in do end" do
|
||||
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
|
||||
|
||||
assert_eval(
|
||||
"foo\n\nbar\n",
|
||||
"foo\n<%= case true do %>\n<%!-- comment --%>\n<% true -> %>\nbar\n<% end %>"
|
||||
)
|
||||
end
|
||||
|
||||
test "embedded code with nested do end" do
|
||||
assert_eval("foo bar", "foo <%= if true do %><%= if true do %>bar<% end %><% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with nested do end with middle expression" do
|
||||
assert_eval(
|
||||
"foo baz",
|
||||
"foo <%= if true do %><%= if false do %>bar<% else %>baz<% end %><% end %>"
|
||||
)
|
||||
end
|
||||
|
||||
test "embedded code with end followed by bracket" do
|
||||
assert_eval(
|
||||
" 101 102 103 ",
|
||||
"<%= Enum.map([1, 2, 3], fn x -> %> <%= 100 + x %> <% end) %>"
|
||||
)
|
||||
|
||||
assert_eval(
|
||||
" 101 102 103 ",
|
||||
"<%= Enum.map([1, 2, 3], fn x ->\n%> <%= 100 + x %> <% end) %>"
|
||||
)
|
||||
|
||||
assert_eval(
|
||||
" 101 102 103 ",
|
||||
"<%= apply Enum, :map, [[1, 2, 3], fn x -> %> <%= 100 + x %> <% end] %>"
|
||||
)
|
||||
|
||||
assert_eval(
|
||||
" 101 102 103 ",
|
||||
"<%= #{__MODULE__}.tuple_map {[1, 2, 3], fn x -> %> <%= 100 + x %> <% end} %>"
|
||||
)
|
||||
|
||||
assert_eval(
|
||||
" 101 102 103 ",
|
||||
"<%= apply(Enum, :map, [[1, 2, 3], fn x -> %> <%= 100 + x %> <% end]) %>"
|
||||
)
|
||||
|
||||
assert_eval(
|
||||
" 101 102 103 ",
|
||||
"<%= Enum.map([1, 2, 3], (fn x -> %> <%= 100 + x %> <% end) ) %>"
|
||||
)
|
||||
end
|
||||
|
||||
test "embedded code with variable definition" do
|
||||
assert_eval("foo 1", "foo <% bar = 1 %><%= bar %>")
|
||||
end
|
||||
|
||||
test "embedded code with require" do
|
||||
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
|
||||
end
|
||||
|
||||
test "with end of token" do
|
||||
assert_eval("foo bar %>", "foo bar %>")
|
||||
end
|
||||
end
|
||||
|
||||
describe "raises syntax errors" do
|
||||
test "when the token is invalid" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1:12: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar")
|
||||
end
|
||||
end
|
||||
|
||||
test "when middle expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError,
|
||||
"nofile:1:18: unexpected middle of expression <% else %>",
|
||||
fn ->
|
||||
EEx.compile_string("<%= if true %>foo<% else %>bar<% end %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1:5: unexpected end of expression <% end %>", fn ->
|
||||
EEx.compile_string("foo <% end %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when start expression is found without an end expression" do
|
||||
msg = "nofile:2:18: unexpected end of string, expected a closing '<% end %>'"
|
||||
|
||||
assert_raise EEx.SyntaxError, msg, fn ->
|
||||
EEx.compile_string("foo\n<%= if true do %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when nested end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1:31: unexpected end of expression <% end %>", fn ->
|
||||
EEx.compile_string("foo <%= if true do %><% end %><% end %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when middle expression has a modifier" do
|
||||
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
EEx.compile_string("foo <%= if true do %>true<%= else %>false<% end %>")
|
||||
end) =~ ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= else %>\"]
|
||||
end
|
||||
|
||||
test "when end expression has a modifier" do
|
||||
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
EEx.compile_string("foo <%= if true do %>true<% else %>false<%= end %>")
|
||||
end) =~
|
||||
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
|
||||
end
|
||||
|
||||
test "when trying to use marker '/' without implementation" do
|
||||
msg =
|
||||
~r/unsupported EEx syntax <%\/ %> \(the syntax is valid but not supported by the current EEx engine\)/
|
||||
|
||||
assert_raise EEx.SyntaxError, msg, fn ->
|
||||
EEx.compile_string("<%/ true %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when trying to use marker '|' without implementation" do
|
||||
msg =
|
||||
~r/unsupported EEx syntax <%| %> \(the syntax is valid but not supported by the current EEx engine\)/
|
||||
|
||||
assert_raise EEx.SyntaxError, msg, fn ->
|
||||
EEx.compile_string("<%| true %>")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "error messages" do
|
||||
test "honor line numbers" do
|
||||
assert_raise EEx.SyntaxError, "nofile:99:12: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar", line: 99)
|
||||
end
|
||||
end
|
||||
|
||||
test "honor file names" do
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:1:12: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "environment" do
|
||||
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 "respects files" do
|
||||
assert_eval("sample.ex", "<%= __ENV__.file %>", [], file: "sample.ex")
|
||||
end
|
||||
end
|
||||
|
||||
describe "clauses" do
|
||||
test "inside 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 "inside multiple functions" do
|
||||
expected = """
|
||||
|
||||
A 1
|
||||
|
||||
B 2
|
||||
|
||||
A 3
|
||||
|
||||
"""
|
||||
|
||||
string = """
|
||||
<%= #{__MODULE__}.switching_map [1, 2, 3], fn x -> %>
|
||||
A <%= x %>
|
||||
<% end, fn x -> %>
|
||||
B <%= x %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval(expected, string)
|
||||
end
|
||||
|
||||
test "inside callback and do block" do
|
||||
expected = """
|
||||
|
||||
|
||||
A 1
|
||||
|
||||
B 2
|
||||
|
||||
A 3
|
||||
|
||||
"""
|
||||
|
||||
string = """
|
||||
<% require #{__MODULE__} %>
|
||||
<%= #{__MODULE__}.switching_macro [1, 2, 3], fn x -> %>
|
||||
A <%= x %>
|
||||
<% end do %>
|
||||
B <%= x %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval(expected, string)
|
||||
end
|
||||
|
||||
test "inside cond" do
|
||||
expected = """
|
||||
foo
|
||||
|
||||
true
|
||||
|
||||
"""
|
||||
|
||||
string = """
|
||||
foo
|
||||
<%= cond do %>
|
||||
<% false -> %> false
|
||||
<% fn -> 1 end -> %>
|
||||
<%= true %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval(expected, string)
|
||||
end
|
||||
|
||||
test "inside cond with do end" 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 "line and column meta" do
|
||||
parser_options = Code.get_compiler_option(:parser_options)
|
||||
Code.put_compiler_option(:parser_options, columns: true)
|
||||
|
||||
try do
|
||||
indentation = 12
|
||||
|
||||
ast =
|
||||
EEx.compile_string(
|
||||
"""
|
||||
<%= f() %> <% f() %>
|
||||
<%= f fn -> %>
|
||||
<%= f() %>
|
||||
<% end %>
|
||||
""",
|
||||
indentation: indentation
|
||||
)
|
||||
|
||||
{_, calls} =
|
||||
Macro.prewalk(ast, [], fn
|
||||
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
|
||||
other, acc -> {other, acc}
|
||||
end)
|
||||
|
||||
assert Enum.reverse(calls) == [
|
||||
[line: 1, column: indentation + 5],
|
||||
[line: 1, column: indentation + 15],
|
||||
[line: 2, column: indentation + 7],
|
||||
[line: 3, column: indentation + 9]
|
||||
]
|
||||
after
|
||||
Code.put_compiler_option(:parser_options, parser_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "buffers" do
|
||||
test "inside comprehensions" do
|
||||
string = """
|
||||
<%= for _name <- packages || [] do %>
|
||||
<% end %>
|
||||
<%= all || :done %>
|
||||
"""
|
||||
|
||||
assert_eval("\ndone\n", string, packages: nil, all: nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe "from file" do
|
||||
test "evaluates the source" do
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template.eex")
|
||||
result = EEx.eval_file(filename)
|
||||
assert_normalized_newline_equal("foo bar.\n", result)
|
||||
end
|
||||
|
||||
test "evaluates the source with bindings" do
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
||||
result = EEx.eval_file(filename, bar: 1)
|
||||
assert_normalized_newline_equal("foo 1\n", result)
|
||||
end
|
||||
|
||||
test "raises an Exception when file is missing" do
|
||||
msg = "could not read file \"non-existent.eex\": no such file or directory"
|
||||
|
||||
assert_raise File.Error, msg, 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 "supports t:Path.t() paths" do
|
||||
filename = to_charlist(Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex"))
|
||||
result = EEx.eval_file(filename, bar: 1)
|
||||
assert_normalized_newline_equal("foo 1\n", result)
|
||||
end
|
||||
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:1:12: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
||||
end
|
||||
|
||||
test "supports overriding file and line through options" do
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template_with_syntax_error.eex")
|
||||
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:11:1: missing token '%>'", fn ->
|
||||
EEx.eval_file(filename, _bindings = [], file: "my_file.eex", line: 10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "precompiled" do
|
||||
test "from string" do
|
||||
assert EExTest.Compiled.string_sample(1, 2) == "3"
|
||||
end
|
||||
|
||||
test "from file" do
|
||||
assert_normalized_newline_equal("foo 1\n", EExTest.Compiled.file_sample(1))
|
||||
assert_normalized_newline_equal("foo 1\n", EExTest.Compiled.public_file_sample(1))
|
||||
end
|
||||
|
||||
test "from file does not affect backtrace" do
|
||||
file = to_charlist(Path.relative_to_cwd(__ENV__.file))
|
||||
|
||||
assert EExTest.Compiled.before_compile() ==
|
||||
{7, {EExTest.Compiled, :before_compile, 0, [file: file, line: 7]}}
|
||||
|
||||
assert EExTest.Compiled.after_compile() ==
|
||||
{21, {EExTest.Compiled, :after_compile, 0, [file: file, line: 21]}}
|
||||
|
||||
assert EExTest.Compiled.unknown() ==
|
||||
{26, {EExTest.Compiled, :unknown, 0, [file: 'unknown', line: 26]}}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule TestEngine do
|
||||
@behaviour EEx.Engine
|
||||
|
||||
def init(_opts) do
|
||||
"INIT"
|
||||
end
|
||||
|
||||
def handle_body(body) do
|
||||
"BODY(#{body})"
|
||||
end
|
||||
|
||||
def handle_begin(_) do
|
||||
"BEGIN"
|
||||
end
|
||||
|
||||
def handle_end(buffer) do
|
||||
buffer <> ":END"
|
||||
end
|
||||
|
||||
def handle_text(buffer, meta, text) do
|
||||
buffer <> ":TEXT-#{meta[:line]}-#{meta[:column]}(#{String.trim(text)})"
|
||||
end
|
||||
|
||||
def handle_expr(buffer, "/", expr) do
|
||||
buffer <> ":DIV(#{Macro.to_string(expr)})"
|
||||
end
|
||||
|
||||
def handle_expr(buffer, "=", expr) do
|
||||
buffer <> ":EQUAL(#{Macro.to_string(expr)})"
|
||||
end
|
||||
|
||||
def handle_expr(buffer, mark, expr) do
|
||||
EEx.Engine.handle_expr(buffer, mark, expr)
|
||||
end
|
||||
end
|
||||
|
||||
describe "custom engines" do
|
||||
test "text" do
|
||||
assert_eval("BODY(INIT:TEXT-1-1(foo))", "foo", [], engine: TestEngine)
|
||||
end
|
||||
|
||||
test "custom marker" do
|
||||
assert_eval("BODY(INIT:TEXT-1-1(foo):DIV(:bar))", "foo <%/ :bar %>", [], engine: TestEngine)
|
||||
end
|
||||
|
||||
test "begin/end" do
|
||||
assert_eval(
|
||||
~s[BODY(INIT:TEXT-1-1(foo):EQUAL(if do\n "BEGIN:TEXT-1-17(this):END"\nelse\n "BEGIN:TEXT-1-31(that):END"\nend))],
|
||||
"foo <%= if do %>this<% else %>that<% end %>",
|
||||
[],
|
||||
engine: TestEngine
|
||||
)
|
||||
end
|
||||
|
||||
test "not implemented custom marker" do
|
||||
msg =
|
||||
~r/unsupported EEx syntax <%| %> \(the syntax is valid but not supported by the current EEx engine\)/
|
||||
|
||||
assert_raise EEx.SyntaxError, msg, fn ->
|
||||
assert_eval({:wrapped, "foo baz"}, "foo <%| :bar %>", [], engine: TestEngine)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "parser options" do
|
||||
test "customizes parsed code" do
|
||||
atoms_encoder = fn "not_jose", _ -> {:ok, :jose} end
|
||||
|
||||
assert_eval("valid", "<%= not_jose %>", [jose: "valid"],
|
||||
parser_options: [static_atoms_encoder: atoms_encoder]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ [], opts \\ []) do
|
||||
opts = Keyword.merge([file: __ENV__.file, engine: opts[:engine] || EEx.Engine], opts)
|
||||
result = EEx.eval_string(actual, binding, opts)
|
||||
assert result == expected
|
||||
end
|
||||
|
||||
defp assert_normalized_newline_equal(expected, actual) do
|
||||
assert String.replace(expected, "\r\n", "\n") == String.replace(actual, "\r\n", "\n")
|
||||
end
|
||||
|
||||
def tuple_map({list, callback}) do
|
||||
Enum.map(list, callback)
|
||||
end
|
||||
|
||||
def switching_map(list, a, b) do
|
||||
list
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn
|
||||
{element, index} when rem(index, 2) == 0 -> a.(element)
|
||||
{element, index} when rem(index, 2) == 1 -> b.(element)
|
||||
end)
|
||||
end
|
||||
|
||||
defmacro switching_macro(list, a, do: block) do
|
||||
quote do
|
||||
b = fn var!(x) ->
|
||||
unquote(block)
|
||||
end
|
||||
|
||||
unquote(__MODULE__).switching_map(unquote(list), unquote(a), b)
|
||||
end
|
||||
end
|
||||
end
|
||||
-1
@@ -1 +0,0 @@
|
||||
foo <%= if true do %>bar.<% end %>
|
||||
@@ -1 +0,0 @@
|
||||
foo <%= bar %>
|
||||
@@ -1 +0,0 @@
|
||||
foo <%= bar
|
||||
@@ -1,8 +0,0 @@
|
||||
{line_exclude, line_include} =
|
||||
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
|
||||
|
||||
ExUnit.start(
|
||||
trace: !!System.get_env("TRACE"),
|
||||
include: line_include,
|
||||
exclude: line_exclude
|
||||
)
|
||||
@@ -1,16 +0,0 @@
|
||||
{'src/*', [
|
||||
warn_unused_vars,
|
||||
warn_export_all,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_unused_function,
|
||||
warn_bif_clash,
|
||||
warn_unused_record,
|
||||
warn_deprecated_function,
|
||||
warn_obsolete_guard,
|
||||
warn_exported_vars,
|
||||
%% warn_missing_spec,
|
||||
%% warn_untyped_record,
|
||||
debug_info,
|
||||
{outdir, "ebin/"}
|
||||
]}.
|
||||
@@ -1,156 +0,0 @@
|
||||
defmodule Diff do
|
||||
@moduledoc """
|
||||
Utilities for comparing build artifacts.
|
||||
"""
|
||||
|
||||
@known_chunks ~w(
|
||||
abstract_code
|
||||
debug_info
|
||||
attributes
|
||||
compile_info
|
||||
exports
|
||||
labeled_exports
|
||||
imports
|
||||
indexed_imports
|
||||
locals
|
||||
labeled_locals
|
||||
atoms
|
||||
)a
|
||||
|
||||
@doc """
|
||||
Compares the build artifacts of two build directories.
|
||||
"""
|
||||
@spec compare_dirs(Path.t(), Path.t()) ::
|
||||
{
|
||||
only1_paths :: list(Path.t()),
|
||||
only2_paths :: list(Path.t()),
|
||||
diff :: list({Path.t(), diff :: String.t()})
|
||||
}
|
||||
def compare_dirs(dir1, dir2) do
|
||||
dir1 = Path.expand(dir1)
|
||||
dir2 = Path.expand(dir2)
|
||||
|
||||
assert_dir!(dir1)
|
||||
assert_dir!(dir2)
|
||||
|
||||
dir1_paths = relative_paths(dir1)
|
||||
dir2_paths = relative_paths(dir2)
|
||||
|
||||
only1_paths = dir1_paths -- dir2_paths
|
||||
only2_paths = dir2_paths -- dir1_paths
|
||||
common_paths = dir1_paths -- only1_paths
|
||||
common_files = Enum.reject(common_paths, &File.dir?/1)
|
||||
|
||||
diff =
|
||||
Enum.flat_map(common_files, fn path ->
|
||||
file1 = Path.join(dir1, path)
|
||||
file2 = Path.join(dir2, path)
|
||||
|
||||
case compare_files(file1, file2) do
|
||||
:eq -> []
|
||||
{:diff, diff} -> [{path, diff}]
|
||||
end
|
||||
end)
|
||||
|
||||
{only1_paths, only2_paths, diff}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compares the contents of two files.
|
||||
|
||||
If the files are BEAM files, it performs a more human-friendly
|
||||
"BEAM-diff".
|
||||
"""
|
||||
@spec compare_files(Path.t(), Path.t()) :: :eq | {:diff, diff :: String.t()}
|
||||
def compare_files(file1, file2) do
|
||||
content1 = File.read!(file1)
|
||||
content2 = File.read!(file2)
|
||||
|
||||
if content1 == content2 do
|
||||
:eq
|
||||
else
|
||||
diff =
|
||||
if String.ends_with?(file1, ".beam") do
|
||||
beam_diff(file1, content1, file2, content2)
|
||||
else
|
||||
file_diff(file1, file2)
|
||||
end
|
||||
|
||||
{:diff, diff}
|
||||
end
|
||||
end
|
||||
|
||||
defp beam_diff(file1, content1, file2, content2) do
|
||||
with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, @known_chunks),
|
||||
{:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, @known_chunks),
|
||||
true <- chunks1 != chunks2 do
|
||||
for {chunk1, chunk2} <- Enum.zip(chunks1, chunks2), chunk1 != chunk2 do
|
||||
tmp_file1 =
|
||||
chunk1
|
||||
|> inspect(pretty: true, limit: :infinity)
|
||||
|> write_tmp()
|
||||
|
||||
tmp_file2 =
|
||||
chunk2
|
||||
|> inspect(pretty: true, limit: :infinity)
|
||||
|> write_tmp()
|
||||
|
||||
file_diff(tmp_file1, tmp_file2)
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
file_diff(file1, file2)
|
||||
end
|
||||
end
|
||||
|
||||
defp file_diff(file1, file2) do
|
||||
{diff, _} = System.cmd("diff", [file1, file2])
|
||||
diff
|
||||
end
|
||||
|
||||
defp relative_paths(dir) do
|
||||
dir
|
||||
|> Path.join("**")
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(&Path.relative_to(&1, dir))
|
||||
end
|
||||
|
||||
defp assert_dir!(dir) do
|
||||
unless File.dir?(dir) do
|
||||
raise ArgumentError, "#{inspect(dir)} is not a directory"
|
||||
end
|
||||
end
|
||||
|
||||
defp write_tmp(content) do
|
||||
filename = generate_tmp_filename()
|
||||
File.mkdir_p!("tmp")
|
||||
File.write!(Path.join("tmp", filename), content)
|
||||
Path.join("tmp", filename)
|
||||
end
|
||||
|
||||
defp generate_tmp_filename do
|
||||
sec = :os.system_time(:second)
|
||||
rand = :rand.uniform(999_999_999)
|
||||
scheduler_id = :erlang.system_info(:scheduler_id)
|
||||
"tmp-#{sec}-#{rand}-#{scheduler_id}"
|
||||
end
|
||||
end
|
||||
|
||||
case System.argv() do
|
||||
[dir1, dir2] ->
|
||||
case Diff.compare_dirs(dir1, dir2) do
|
||||
{[], [], []} ->
|
||||
IO.puts("#{inspect(dir1)} and #{inspect(dir2)} are equal")
|
||||
|
||||
{only1, only2, diff} ->
|
||||
for path <- only1, do: IO.puts("Only in #{dir1}: #{path}")
|
||||
for path <- only2, do: IO.puts("Only in #{dir2}: #{path}")
|
||||
for {path, diff} <- diff, do: IO.puts("Diff #{path}:\n#{diff}")
|
||||
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
_ ->
|
||||
IO.puts("Please, provide two directories as arguments")
|
||||
System.halt(1)
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env escript
|
||||
%% -*- erlang -*-
|
||||
|
||||
main([Source, Target, Version]) ->
|
||||
{ok, [{application, Name, Props0}]} = file:consult(Source),
|
||||
Ebin = filename:dirname(Target),
|
||||
Files = filelib:wildcard(filename:join(Ebin, "*.beam")),
|
||||
Mods = [list_to_atom(filename:basename(F, ".beam")) || F <- Files],
|
||||
Props1 = lists:keyreplace(modules, 1, Props0, {modules, Mods}),
|
||||
Props = lists:keyreplace(vsn, 1, Props1, {vsn, Version}),
|
||||
AppDef = io_lib:format("~tp.~n", [{application, Name, Props}]),
|
||||
ok = file:write_file(Target, AppDef),
|
||||
io:format("Generated ~ts app~n", [Name]).
|
||||
@@ -1,946 +0,0 @@
|
||||
defmodule Access do
|
||||
@moduledoc """
|
||||
Key-based access to data structures.
|
||||
|
||||
The `Access` module defines a behaviour for dynamically accessing
|
||||
keys of any type in a data structure via the `data[key]` syntax.
|
||||
|
||||
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
||||
of the box. Keywords supports only atoms keys, keys for maps can
|
||||
be of any type. Both return `nil` if the key does not exist:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:a]
|
||||
1
|
||||
iex> keywords[:c]
|
||||
nil
|
||||
|
||||
iex> map = %{a: 1, b: 2}
|
||||
iex> map[:a]
|
||||
1
|
||||
|
||||
iex> star_ratings = %{1.0 => "★", 1.5 => "★☆", 2.0 => "★★"}
|
||||
iex> star_ratings[1.5]
|
||||
"★☆"
|
||||
|
||||
This syntax is very convenient as it can be nested arbitrarily:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:c][:unknown]
|
||||
nil
|
||||
|
||||
This works because accessing anything on a `nil` value, returns
|
||||
`nil` itself:
|
||||
|
||||
iex> nil[:a]
|
||||
nil
|
||||
|
||||
The access syntax can also be used with the `Kernel.put_in/2`,
|
||||
`Kernel.update_in/2` and `Kernel.get_and_update_in/2` macros
|
||||
to allow values to be set in nested data structures:
|
||||
|
||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
||||
iex> put_in(users["john"][:age], 28)
|
||||
%{"john" => %{age: 28}, "meg" => %{age: 23}}
|
||||
|
||||
> Attention! While the access syntax is allowed in maps via
|
||||
> `map[key]`, if your map is made of predefined atom keys,
|
||||
> you should prefer to access those atom keys with `map.key`
|
||||
> instead of `map[key]`, as `map.key` will raise if the key
|
||||
> is missing (which is not supposed to happen if the keys are
|
||||
> predefined). Similarly, since structs are maps and structs
|
||||
> have predefined keys, they only allow the `struct.key`
|
||||
> syntax and they do not allow the `struct[key]` access syntax.
|
||||
> See the `Map` module for more information.
|
||||
|
||||
## Nested data structures
|
||||
|
||||
Both key-based access syntaxes can be used with the nested update
|
||||
functions and macros in `Kernel`, such as `Kernel.get_in/2`,
|
||||
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
|
||||
`Kernel.get_and_update_in/3`.
|
||||
|
||||
For example, to update a map inside another map:
|
||||
|
||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
||||
iex> put_in(users["john"].age, 28)
|
||||
%{"john" => %{age: 28}, "meg" => %{age: 23}}
|
||||
|
||||
This module provides convenience functions for traversing other
|
||||
structures, like tuples and lists. These functions can be used
|
||||
in all the `Access`-related functions and macros in `Kernel`.
|
||||
|
||||
For instance, given a user map with the `:name` and `:languages`
|
||||
keys, here is how to deeply traverse the map and convert all
|
||||
language names to uppercase:
|
||||
|
||||
iex> languages = [
|
||||
...> %{name: "elixir", type: :functional},
|
||||
...> %{name: "c", type: :procedural}
|
||||
...> ]
|
||||
iex> user = %{name: "john", languages: languages}
|
||||
iex> update_in(user, [:languages, Access.all(), :name], &String.upcase/1)
|
||||
%{
|
||||
name: "john",
|
||||
languages: [
|
||||
%{name: "ELIXIR", type: :functional},
|
||||
%{name: "C", type: :procedural}
|
||||
]
|
||||
}
|
||||
|
||||
See the functions `key/1`, `key!/1`, `elem/1`, and `all/0` for
|
||||
some of the available accessors.
|
||||
"""
|
||||
|
||||
@type container :: keyword | struct | map
|
||||
@type nil_container :: nil
|
||||
@type t :: container | nil_container | any
|
||||
@type key :: any
|
||||
@type value :: any
|
||||
|
||||
@type get_fun(data) ::
|
||||
(:get, data, (term -> term) -> new_data :: container)
|
||||
|
||||
@type get_and_update_fun(data, current_value) ::
|
||||
(:get_and_update, data, (term -> term) ->
|
||||
{current_value, new_data :: container} | :pop)
|
||||
|
||||
@type access_fun(data, current_value) ::
|
||||
get_fun(data) | get_and_update_fun(data, current_value)
|
||||
|
||||
@doc """
|
||||
Invoked in order to access the value stored under `key` in the given term `term`.
|
||||
|
||||
This function should return `{:ok, value}` where `value` is the value under
|
||||
`key` if the key exists in the term, or `:error` if the key does not exist in
|
||||
the term.
|
||||
|
||||
Many of the functions defined in the `Access` module internally call this
|
||||
function. This function is also used when the square-brackets access syntax
|
||||
(`structure[key]`) is used: the `fetch/2` callback implemented by the module
|
||||
that defines the `structure` struct is invoked and if it returns `{:ok,
|
||||
value}` then `value` is returned, or if it returns `:error` then `nil` is
|
||||
returned.
|
||||
|
||||
See the `Map.fetch/2` and `Keyword.fetch/2` implementations for examples of
|
||||
how to implement this callback.
|
||||
"""
|
||||
@callback fetch(term :: t, key) :: {:ok, value} | :error
|
||||
|
||||
@doc """
|
||||
Invoked in order to access the value under `key` and update it at the same time.
|
||||
|
||||
The implementation of this callback should invoke `fun` with the value under
|
||||
`key` in the passed structure `data`, or with `nil` if `key` is not present in it.
|
||||
This function must return either `{current_value, new_value}` or `:pop`.
|
||||
|
||||
If the passed function returns `{current_value, new_value}`,
|
||||
the return value of this callback should be `{current_value, new_data}`, where:
|
||||
|
||||
* `current_value` is the retrieved value (which can be operated on before being returned)
|
||||
|
||||
* `new_value` is the new value to be stored under `key`
|
||||
|
||||
* `new_data` is `data` after updating the value of `key` with `new_value`.
|
||||
|
||||
If the passed function returns `:pop`, the return value of this callback
|
||||
must be `{value, new_data}` where `value` is the value under `key`
|
||||
(or `nil` if not present) and `new_data` is `data` without `key`.
|
||||
|
||||
See the implementations of `Map.get_and_update/3` or `Keyword.get_and_update/3`
|
||||
for more examples.
|
||||
"""
|
||||
@callback get_and_update(data, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, new_data :: data}
|
||||
when current_value: value, data: container
|
||||
|
||||
@doc """
|
||||
Invoked to "pop" the value under `key` out of the given data structure.
|
||||
|
||||
When `key` exists in the given structure `data`, the implementation should
|
||||
return a `{value, new_data}` tuple where `value` is the value that was under
|
||||
`key` and `new_data` is `term` without `key`.
|
||||
|
||||
When `key` is not present in the given structure, a tuple `{value, data}`
|
||||
should be returned, where `value` is implementation-defined.
|
||||
|
||||
See the implementations for `Map.pop/3` or `Keyword.pop/3` for more examples.
|
||||
"""
|
||||
@callback pop(data, key) :: {value, data} when data: container
|
||||
|
||||
defmacrop raise_undefined_behaviour(exception, module, top) do
|
||||
quote do
|
||||
exception =
|
||||
case __STACKTRACE__ do
|
||||
[unquote(top) | _] ->
|
||||
reason =
|
||||
"#{inspect(unquote(module))} does not implement the Access behaviour. " <>
|
||||
"If you are using get_in/put_in/update_in, you can specify the field " <>
|
||||
"to be accessed using Access.key!/1"
|
||||
|
||||
%{unquote(exception) | reason: reason}
|
||||
|
||||
_ ->
|
||||
unquote(exception)
|
||||
end
|
||||
|
||||
reraise exception, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the value for the given key in a container (a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
|
||||
Returns `{:ok, value}` where `value` is the value under `key` if there is such
|
||||
a key, or `:error` if `key` is not found.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Access.fetch(%{name: "meg", age: 26}, :name)
|
||||
{:ok, "meg"}
|
||||
|
||||
iex> Access.fetch([ordered: true, on_timeout: :exit], :timeout)
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec fetch(container, term) :: {:ok, term} | :error
|
||||
@spec fetch(nil_container, any) :: :error
|
||||
def fetch(container, key)
|
||||
|
||||
def fetch(%module{} = container, key) do
|
||||
module.fetch(container, key)
|
||||
rescue
|
||||
exception in UndefinedFunctionError ->
|
||||
raise_undefined_behaviour(exception, module, {^module, :fetch, [^container, ^key], _})
|
||||
end
|
||||
|
||||
def fetch(map, key) when is_map(map) do
|
||||
case map do
|
||||
%{^key => value} -> {:ok, value}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def fetch(list, key) when is_list(list) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, list) do
|
||||
{_, value} -> {:ok, value}
|
||||
false -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def fetch(list, key) when is_list(list) do
|
||||
raise ArgumentError,
|
||||
"the Access calls for keywords expect the key to be an atom, got: " <> inspect(key)
|
||||
end
|
||||
|
||||
def fetch(nil, _key) do
|
||||
:error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `fetch/2` but returns the value directly,
|
||||
or raises a `KeyError` exception if `key` is not found.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Access.fetch!(%{name: "meg", age: 26}, :name)
|
||||
"meg"
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec fetch!(container, term) :: term
|
||||
def fetch!(container, key) do
|
||||
case fetch(container, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> raise(KeyError, key: key, term: container)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value for the given key in a container (a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
|
||||
Returns the value under `key` if there is such a key, or `default` if `key` is
|
||||
not found.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Access.get(%{name: "john"}, :name, "default name")
|
||||
"john"
|
||||
iex> Access.get(%{name: "john"}, :age, 25)
|
||||
25
|
||||
|
||||
iex> Access.get([ordered: true], :timeout)
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec get(container, term, term) :: term
|
||||
@spec get(nil_container, any, default) :: default when default: var
|
||||
def get(container, key, default \\ nil)
|
||||
|
||||
# Reimplementing the same logic as Access.fetch/2 here is done for performance, since
|
||||
# this is called a lot and calling fetch/2 means introducing some overhead (like
|
||||
# building the "{:ok, _}" tuple and deconstructing it back right away).
|
||||
|
||||
def get(%module{} = container, key, default) do
|
||||
try do
|
||||
module.fetch(container, key)
|
||||
rescue
|
||||
exception in UndefinedFunctionError ->
|
||||
raise_undefined_behaviour(exception, module, {^module, :fetch, [^container, ^key], _})
|
||||
else
|
||||
{:ok, value} -> value
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
def get(map, key, default) when is_map(map) do
|
||||
case map do
|
||||
%{^key => value} -> value
|
||||
_ -> default
|
||||
end
|
||||
end
|
||||
|
||||
def get(list, key, default) when is_list(list) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, list) do
|
||||
{_, value} -> value
|
||||
false -> default
|
||||
end
|
||||
end
|
||||
|
||||
def get(list, key, _default) when is_list(list) do
|
||||
raise ArgumentError,
|
||||
"the Access calls for keywords expect the key to be an atom, got: " <> inspect(key)
|
||||
end
|
||||
|
||||
def get(nil, _key, default) do
|
||||
default
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets and updates the given key in a `container` (a map, a keyword list,
|
||||
a struct that implements the `Access` behaviour).
|
||||
|
||||
The `fun` argument receives the value of `key` (or `nil` if `key` is not
|
||||
present in `container`) and must return a two-element tuple `{current_value, new_value}`:
|
||||
the "get" value `current_value` (the retrieved value, which can be operated on before
|
||||
being returned) and the new value to be stored under `key` (`new_value`).
|
||||
`fun` may also return `:pop`, which means the current value
|
||||
should be removed from the container and returned.
|
||||
|
||||
The returned value is a two-element tuple with the "get" value returned by
|
||||
`fun` and a new container with the updated value under `key`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Access.get_and_update([a: 1], :a, fn current_value ->
|
||||
...> {current_value, current_value + 1}
|
||||
...> end)
|
||||
{1, [a: 2]}
|
||||
|
||||
"""
|
||||
@spec get_and_update(data, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, new_data :: data}
|
||||
when current_value: var, data: container
|
||||
def get_and_update(container, key, fun)
|
||||
|
||||
def get_and_update(%module{} = container, key, fun) do
|
||||
module.get_and_update(container, key, fun)
|
||||
rescue
|
||||
exception in UndefinedFunctionError ->
|
||||
raise_undefined_behaviour(
|
||||
exception,
|
||||
module,
|
||||
{^module, :get_and_update, [^container, ^key, ^fun], _}
|
||||
)
|
||||
end
|
||||
|
||||
def get_and_update(map, key, fun) when is_map(map) do
|
||||
Map.get_and_update(map, key, fun)
|
||||
end
|
||||
|
||||
def get_and_update(list, key, fun) when is_list(list) do
|
||||
Keyword.get_and_update(list, key, fun)
|
||||
end
|
||||
|
||||
def get_and_update(nil, key, _fun) do
|
||||
raise ArgumentError, "could not put/update key #{inspect(key)} on a nil value"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the entry with a given key from a container (a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
|
||||
Returns a tuple containing the value associated with the key and the
|
||||
updated container. `nil` is returned for the value if the key isn't
|
||||
in the container.
|
||||
|
||||
## Examples
|
||||
|
||||
With a map:
|
||||
|
||||
iex> Access.pop(%{name: "Elixir", creator: "Valim"}, :name)
|
||||
{"Elixir", %{creator: "Valim"}}
|
||||
|
||||
A keyword list:
|
||||
|
||||
iex> Access.pop([name: "Elixir", creator: "Valim"], :name)
|
||||
{"Elixir", [creator: "Valim"]}
|
||||
|
||||
An unknown key:
|
||||
|
||||
iex> Access.pop(%{name: "Elixir", creator: "Valim"}, :year)
|
||||
{nil, %{creator: "Valim", name: "Elixir"}}
|
||||
|
||||
"""
|
||||
@spec pop(data, key) :: {value, data} when data: container
|
||||
def pop(%module{} = container, key) do
|
||||
module.pop(container, key)
|
||||
rescue
|
||||
exception in UndefinedFunctionError ->
|
||||
raise_undefined_behaviour(exception, module, {^module, :pop, [^container, ^key], _})
|
||||
end
|
||||
|
||||
def pop(map, key) when is_map(map) do
|
||||
Map.pop(map, key)
|
||||
end
|
||||
|
||||
def pop(list, key) when is_list(list) do
|
||||
Keyword.pop(list, key)
|
||||
end
|
||||
|
||||
def pop(nil, key) do
|
||||
raise ArgumentError, "could not pop key #{inspect(key)} on a nil value"
|
||||
end
|
||||
|
||||
## Accessors
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
|
||||
The returned function uses the default value if the key does not exist.
|
||||
This can be used to specify defaults and safely traverse missing keys:
|
||||
|
||||
iex> get_in(%{}, [Access.key(:user, %{}), Access.key(:name, "meg")])
|
||||
"meg"
|
||||
|
||||
Such is also useful when using update functions, allowing us to introduce
|
||||
values as we traverse the data structure for updates:
|
||||
|
||||
iex> put_in(%{}, [Access.key(:user, %{}), Access.key(:name)], "Mary")
|
||||
%{user: %{name: "Mary"}}
|
||||
|
||||
## Examples
|
||||
|
||||
iex> map = %{user: %{name: "john"}}
|
||||
iex> get_in(map, [Access.key(:unknown, %{}), Access.key(:name, "john")])
|
||||
"john"
|
||||
iex> get_and_update_in(map, [Access.key(:user), Access.key(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", %{user: %{name: "JOHN"}}}
|
||||
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
|
||||
{"john", %{user: %{}}}
|
||||
|
||||
An error is raised if the accessed structure is not a map or a struct:
|
||||
|
||||
iex> get_in([], [Access.key(:foo)])
|
||||
** (BadMapError) expected a map, got: []
|
||||
|
||||
"""
|
||||
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
|
||||
def key(key, default \\ nil) do
|
||||
fn
|
||||
:get, data, next ->
|
||||
next.(Map.get(data, key, default))
|
||||
|
||||
:get_and_update, data, next ->
|
||||
value = Map.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Map.put(data, key, update)}
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
|
||||
Similar to `key/2`, but the returned function raises if the key does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> map = %{user: %{name: "john"}}
|
||||
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
iex> get_and_update_in(map, [Access.key!(:user), Access.key!(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", %{user: %{name: "JOHN"}}}
|
||||
iex> pop_in(map, [Access.key!(:user), Access.key!(:name)])
|
||||
{"john", %{user: %{}}}
|
||||
iex> get_in(map, [Access.key!(:user), Access.key!(:unknown)])
|
||||
** (KeyError) key :unknown not found in: %{name: \"john\"}
|
||||
|
||||
An error is raised if the accessed structure is not a map/struct:
|
||||
|
||||
iex> get_in([], [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
|
||||
|
||||
"""
|
||||
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
|
||||
def key!(key) do
|
||||
fn
|
||||
:get, %{} = data, next ->
|
||||
next.(Map.fetch!(data, key))
|
||||
|
||||
:get_and_update, %{} = data, next ->
|
||||
value = Map.fetch!(data, key)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Map.put(data, key, update)}
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Returns a function that accesses the element at the given index in a tuple.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
|
||||
The returned function raises if `index` is out of bounds.
|
||||
|
||||
Note that popping elements out of tuples is not possible and raises an
|
||||
error.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> map = %{user: {"john", 27}}
|
||||
iex> get_in(map, [:user, Access.elem(0)])
|
||||
"john"
|
||||
iex> get_and_update_in(map, [:user, Access.elem(0)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", %{user: {"JOHN", 27}}}
|
||||
iex> pop_in(map, [:user, Access.elem(0)])
|
||||
** (RuntimeError) cannot pop data from a tuple
|
||||
|
||||
An error is raised if the accessed structure is not a tuple:
|
||||
|
||||
iex> get_in(%{}, [Access.elem(0)])
|
||||
** (RuntimeError) Access.elem/1 expected a tuple, got: %{}
|
||||
|
||||
"""
|
||||
@spec elem(non_neg_integer) :: access_fun(data :: tuple, current_value :: term)
|
||||
def elem(index) when is_integer(index) and index >= 0 do
|
||||
pos = index + 1
|
||||
|
||||
fn
|
||||
:get, data, next when is_tuple(data) ->
|
||||
next.(:erlang.element(pos, data))
|
||||
|
||||
:get_and_update, data, next when is_tuple(data) ->
|
||||
value = :erlang.element(pos, data)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, :erlang.setelement(pos, data, update)}
|
||||
:pop -> raise "cannot pop data from a tuple"
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.elem/1 expected a tuple, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Returns a function that accesses all the elements in a list.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list = [%{name: "john"}, %{name: "mary"}]
|
||||
iex> get_in(list, [Access.all(), :name])
|
||||
["john", "mary"]
|
||||
iex> get_and_update_in(list, [Access.all(), :name], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{["john", "mary"], [%{name: "JOHN"}, %{name: "MARY"}]}
|
||||
iex> pop_in(list, [Access.all(), :name])
|
||||
{["john", "mary"], [%{}, %{}]}
|
||||
|
||||
Here is an example that traverses the list dropping even
|
||||
numbers and multiplying odd numbers by 2:
|
||||
|
||||
iex> require Integer
|
||||
iex> get_and_update_in([1, 2, 3, 4, 5], [Access.all()], fn num ->
|
||||
...> if Integer.is_even(num), do: :pop, else: {num, num * 2}
|
||||
...> end)
|
||||
{[1, 2, 3, 4, 5], [2, 6, 10]}
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.all()])
|
||||
** (RuntimeError) Access.all/0 expected a list, got: %{}
|
||||
|
||||
"""
|
||||
@spec all() :: access_fun(data :: list, current_value :: list)
|
||||
def all() do
|
||||
&all/3
|
||||
end
|
||||
|
||||
defp all(:get, data, next) when is_list(data) do
|
||||
Enum.map(data, next)
|
||||
end
|
||||
|
||||
defp all(:get_and_update, data, next) when is_list(data) do
|
||||
all(data, next, _gets = [], _updates = [])
|
||||
end
|
||||
|
||||
defp all(_op, data, _next) do
|
||||
raise "Access.all/0 expected a list, got: #{inspect(data)}"
|
||||
end
|
||||
|
||||
defp all([head | rest], next, gets, updates) do
|
||||
case next.(head) do
|
||||
{get, update} -> all(rest, next, [get | gets], [update | updates])
|
||||
:pop -> all(rest, next, [head | gets], updates)
|
||||
end
|
||||
end
|
||||
|
||||
defp all([], _next, gets, updates) do
|
||||
{:lists.reverse(gets), :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Returns a function that accesses the element at `index` (zero based) of a list.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list = [%{name: "john"}, %{name: "mary"}]
|
||||
iex> get_in(list, [Access.at(1), :name])
|
||||
"mary"
|
||||
iex> get_in(list, [Access.at(-1), :name])
|
||||
"mary"
|
||||
iex> get_and_update_in(list, [Access.at(0), :name], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [%{name: "JOHN"}, %{name: "mary"}]}
|
||||
iex> get_and_update_in(list, [Access.at(-1), :name], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"mary", [%{name: "john"}, %{name: "MARY"}]}
|
||||
|
||||
`at/1` can also be used to pop elements out of a list or
|
||||
a key inside of a list:
|
||||
|
||||
iex> list = [%{name: "john"}, %{name: "mary"}]
|
||||
iex> pop_in(list, [Access.at(0)])
|
||||
{%{name: "john"}, [%{name: "mary"}]}
|
||||
iex> pop_in(list, [Access.at(0), :name])
|
||||
{"john", [%{}, %{name: "mary"}]}
|
||||
|
||||
When the index is out of bounds, `nil` is returned and the update function is never called:
|
||||
|
||||
iex> list = [%{name: "john"}, %{name: "mary"}]
|
||||
iex> get_in(list, [Access.at(10), :name])
|
||||
nil
|
||||
iex> get_and_update_in(list, [Access.at(10), :name], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{nil, [%{name: "john"}, %{name: "mary"}]}
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.at(1)])
|
||||
** (RuntimeError) Access.at/1 expected a list, got: %{}
|
||||
|
||||
"""
|
||||
@spec at(integer) :: access_fun(data :: list, current_value :: term)
|
||||
def at(index) when is_integer(index) do
|
||||
fn op, data, next -> at(op, data, index, next) end
|
||||
end
|
||||
|
||||
defp at(:get, data, index, next) when is_list(data) do
|
||||
data |> Enum.at(index) |> next.()
|
||||
end
|
||||
|
||||
defp at(:get_and_update, data, index, next) when is_list(data) do
|
||||
get_and_update_at(data, index, next, [], fn -> nil end)
|
||||
end
|
||||
|
||||
defp at(_op, data, _index, _next) do
|
||||
raise "Access.at/1 expected a list, got: #{inspect(data)}"
|
||||
end
|
||||
|
||||
defp get_and_update_at([head | rest], 0, next, updates, _default_fun) do
|
||||
case next.(head) do
|
||||
{get, update} -> {get, :lists.reverse([update | updates], rest)}
|
||||
:pop -> {head, :lists.reverse(updates, rest)}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_at([_ | _] = list, index, next, updates, default_fun) when index < 0 do
|
||||
list_length = length(list)
|
||||
|
||||
if list_length + index >= 0 do
|
||||
get_and_update_at(list, list_length + index, next, updates, default_fun)
|
||||
else
|
||||
{default_fun.(), list}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_at([head | rest], index, next, updates, default_fun) when index > 0 do
|
||||
get_and_update_at(rest, index - 1, next, [head | updates], default_fun)
|
||||
end
|
||||
|
||||
defp get_and_update_at([], _index, _next, updates, default_fun) do
|
||||
{default_fun.(), :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Same as `at/1` except that it raises `Enum.OutOfBoundsError`
|
||||
if the given index is out of bounds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_in([:a, :b, :c], [Access.at!(2)])
|
||||
:c
|
||||
iex> get_in([:a, :b, :c], [Access.at!(3)])
|
||||
** (Enum.OutOfBoundsError) out of bounds error
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec at!(integer) :: access_fun(data :: list, current_value :: term)
|
||||
def at!(index) when is_integer(index) do
|
||||
fn op, data, next -> at!(op, data, index, next) end
|
||||
end
|
||||
|
||||
defp at!(:get, data, index, next) when is_list(data) do
|
||||
case Enum.fetch(data, index) do
|
||||
{:ok, value} -> next.(value)
|
||||
:error -> raise Enum.OutOfBoundsError
|
||||
end
|
||||
end
|
||||
|
||||
defp at!(:get_and_update, data, index, next) when is_list(data) do
|
||||
get_and_update_at(data, index, next, [], fn -> raise Enum.OutOfBoundsError end)
|
||||
end
|
||||
|
||||
defp at!(_op, data, _index, _next) do
|
||||
raise "Access.at!/1 expected a list, got: #{inspect(data)}"
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Returns a function that accesses all elements of a list that match the provided predicate.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> get_in(list, [Access.filter(&(&1.salary > 20)), :name])
|
||||
["francine"]
|
||||
iex> get_and_update_in(list, [Access.filter(&(&1.salary <= 20)), :name], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{["john"], [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}
|
||||
|
||||
`filter/1` can also be used to pop elements out of a list or
|
||||
a key inside of a list:
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> pop_in(list, [Access.filter(&(&1.salary >= 20))])
|
||||
{[%{name: "francine", salary: 30}], [%{name: "john", salary: 10}]}
|
||||
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
|
||||
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
|
||||
|
||||
When no match is found, an empty list is returned and the update function is never called
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
|
||||
[]
|
||||
iex> get_and_update_in(list, [Access.filter(&(&1.salary >= 50)), :name], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
||||
|
||||
An error is raised if the predicate is not a function or is of the incorrect arity:
|
||||
|
||||
iex> get_in([], [Access.filter(5)])
|
||||
** (FunctionClauseError) no function clause matching in Access.filter/1
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
|
||||
** (RuntimeError) Access.filter/1 expected a list, got: %{}
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
|
||||
def filter(func) when is_function(func) do
|
||||
fn op, data, next -> filter(op, data, func, next) end
|
||||
end
|
||||
|
||||
defp filter(:get, data, func, next) when is_list(data) do
|
||||
data |> Enum.filter(func) |> Enum.map(next)
|
||||
end
|
||||
|
||||
defp filter(:get_and_update, data, func, next) when is_list(data) do
|
||||
get_and_update_filter(data, func, next, [], [])
|
||||
end
|
||||
|
||||
defp filter(_op, data, _func, _next) do
|
||||
raise "Access.filter/1 expected a list, got: #{inspect(data)}"
|
||||
end
|
||||
|
||||
defp get_and_update_filter([head | rest], func, next, updates, gets) do
|
||||
if func.(head) do
|
||||
case next.(head) do
|
||||
{get, update} ->
|
||||
get_and_update_filter(rest, func, next, [update | updates], [get | gets])
|
||||
|
||||
:pop ->
|
||||
get_and_update_filter(rest, func, next, updates, [head | gets])
|
||||
end
|
||||
else
|
||||
get_and_update_filter(rest, func, next, [head | updates], gets)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_filter([], _func, _next, updates, gets) do
|
||||
{:lists.reverse(gets), :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Returns a function that accesses all items of a list that are within the provided range.
|
||||
|
||||
The range will be normalized following the same rules from `Enum.slice/2`.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
|
||||
iex> get_in(list, [Access.slice(1..2), :name])
|
||||
["francine", "vitor"]
|
||||
iex> get_and_update_in(list, [Access.slice(1..3//2), :name], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{["francine"], [%{name: "john", salary: 10}, %{name: "FRANCINE", salary: 30}, %{name: "vitor", salary: 25}]}
|
||||
|
||||
`slice/1` can also be used to pop elements out of a list or
|
||||
a key inside of a list:
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
|
||||
iex> pop_in(list, [Access.slice(-2..-1)])
|
||||
{[%{name: "francine", salary: 30}, %{name: "vitor", salary: 25}], [%{name: "john", salary: 10}]}
|
||||
iex> pop_in(list, [Access.slice(-2..-1), :name])
|
||||
{["francine", "vitor"], [%{name: "john", salary: 10}, %{salary: 30}, %{salary: 25}]}
|
||||
|
||||
When no match is found, an empty list is returned and the update function is never called
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
|
||||
iex> get_in(list, [Access.slice(5..10//2), :name])
|
||||
[]
|
||||
iex> get_and_update_in(list, [Access.slice(5..10//2), :name], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]}
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.slice(2..10//3)])
|
||||
** (ArgumentError) Access.slice/1 expected a list, got: %{}
|
||||
|
||||
An error is raised if the step of the range is negative:
|
||||
|
||||
iex> get_in([], [Access.slice(2..10//-1)])
|
||||
** (ArgumentError) Access.slice/1 does not accept ranges with negative steps, got: 2..10//-1
|
||||
|
||||
"""
|
||||
@doc since: "1.14"
|
||||
@spec slice(Range.t()) :: access_fun(data :: list, current_value :: list)
|
||||
def slice(%Range{} = range) do
|
||||
if range.step > 0 do
|
||||
fn op, data, next -> slice(op, data, range, next) end
|
||||
else
|
||||
raise ArgumentError,
|
||||
"Access.slice/1 does not accept ranges with negative steps, got: #{inspect(range)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp slice(:get, data, %Range{} = range, next) when is_list(data) do
|
||||
data
|
||||
|> Enum.slice(range)
|
||||
|> Enum.map(next)
|
||||
end
|
||||
|
||||
defp slice(:get_and_update, data, range, next) when is_list(data) do
|
||||
range = normalize_range(range, data)
|
||||
|
||||
if range.first > range.last do
|
||||
{[], data}
|
||||
else
|
||||
get_and_update_slice(data, range, next, [], [], 0)
|
||||
end
|
||||
end
|
||||
|
||||
defp slice(_op, data, _range, _next) do
|
||||
raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}"
|
||||
end
|
||||
|
||||
defp normalize_range(%Range{first: first, last: last, step: step}, list)
|
||||
when first < 0 or last < 0 do
|
||||
count = length(list)
|
||||
first = if first >= 0, do: first, else: Kernel.max(first + count, 0)
|
||||
last = if last >= 0, do: last, else: last + count
|
||||
Range.new(first, last, step)
|
||||
end
|
||||
|
||||
defp normalize_range(range, _list), do: range
|
||||
|
||||
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
|
||||
if index in range do
|
||||
case next.(head) do
|
||||
:pop ->
|
||||
get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
|
||||
|
||||
{get, update} ->
|
||||
get_and_update_slice(
|
||||
rest,
|
||||
range,
|
||||
next,
|
||||
[update | updates],
|
||||
[get | gets],
|
||||
index + 1
|
||||
)
|
||||
end
|
||||
else
|
||||
get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
|
||||
{:lists.reverse(gets), :lists.reverse(updates)}
|
||||
end
|
||||
end
|
||||
@@ -1,508 +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, the following agent implements a counter:
|
||||
|
||||
defmodule Counter do
|
||||
use Agent
|
||||
|
||||
def start_link(initial_value) do
|
||||
Agent.start_link(fn -> initial_value end, name: __MODULE__)
|
||||
end
|
||||
|
||||
def value do
|
||||
Agent.get(__MODULE__, & &1)
|
||||
end
|
||||
|
||||
def increment do
|
||||
Agent.update(__MODULE__, &(&1 + 1))
|
||||
end
|
||||
end
|
||||
|
||||
Usage would be:
|
||||
|
||||
Counter.start_link(0)
|
||||
#=> {:ok, #PID<0.123.0>}
|
||||
|
||||
Counter.value()
|
||||
#=> 0
|
||||
|
||||
Counter.increment()
|
||||
#=> :ok
|
||||
|
||||
Counter.increment()
|
||||
#=> :ok
|
||||
|
||||
Counter.value()
|
||||
#=> 2
|
||||
|
||||
Thanks to the agent server process, the counter can be safely incremented
|
||||
concurrently.
|
||||
|
||||
Agents provide a segregation between the client and server APIs (similar to
|
||||
`GenServer`s). In particular, the functions passed as arguments to the calls to
|
||||
`Agent` functions are invoked inside the agent (the server). This distinction
|
||||
is important because you may want to avoid expensive operations inside the
|
||||
agent, as they 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 function blocks the agent. The second function copies all the state
|
||||
to the client and then executes the operation in the client. One aspect to
|
||||
consider is whether the data is large enough to require processing in the server,
|
||||
at least initially, or small enough to be sent to the client cheaply. Another
|
||||
factor is whether the data needs to be processed atomically: getting the
|
||||
state and calling `do_something_expensive(state)` outside of the agent means
|
||||
that the agent's state can be updated in the meantime. This is specially
|
||||
important in case of updates as computing the new state in the client rather
|
||||
than in the server can lead to race conditions if multiple clients are trying
|
||||
to update the same state to different values.
|
||||
|
||||
## How to supervise
|
||||
|
||||
An `Agent` is most commonly started under a supervision tree.
|
||||
When we invoke `use Agent`, it automatically defines a `child_spec/1`
|
||||
function that allows us to start the agent directly under a supervisor.
|
||||
To start an agent under a supervisor with an initial counter of 0,
|
||||
one may do:
|
||||
|
||||
children = [
|
||||
{Counter, 0}
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_all)
|
||||
|
||||
While one could also simply pass the `Counter` as a child to the supervisor,
|
||||
such as:
|
||||
|
||||
children = [
|
||||
Counter # Same as {Counter, []}
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_all)
|
||||
|
||||
The definition above wouldn't work for this particular example,
|
||||
as it would attempt to start the counter with an initial value
|
||||
of an empty list. However, this may be a viable option in your
|
||||
own agents. A common approach is to use a keyword list, as that
|
||||
would allow setting the initial value and giving a name to the
|
||||
counter process, for example:
|
||||
|
||||
def start_link(opts) do
|
||||
{initial_value, opts} = Keyword.pop(opts, :initial_value, 0)
|
||||
Agent.start_link(fn -> initial_value end, opts)
|
||||
end
|
||||
|
||||
and then you can use `Counter`, `{Counter, name: :my_counter}` or
|
||||
even `{Counter, initial_value: 0, name: :my_counter}` as a child
|
||||
specification.
|
||||
|
||||
`use Agent` also accepts a list of options which configures the
|
||||
child specification and therefore how it runs under a supervisor.
|
||||
The generated `child_spec/1` can be customized with the following options:
|
||||
|
||||
* `:id` - the child specification identifier, defaults to the current module
|
||||
* `:restart` - when the child should be restarted, defaults to `:permanent`
|
||||
* `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down
|
||||
|
||||
For example:
|
||||
|
||||
use Agent, restart: :transient, shutdown: 10_000
|
||||
|
||||
See the "Child specification" section in the `Supervisor` module for more
|
||||
detailed information. The `@doc` annotation immediately preceding
|
||||
`use Agent` will be attached to the generated `child_spec/1` function.
|
||||
|
||||
## Name registration
|
||||
|
||||
An agent is bound to the same name registration rules as GenServers.
|
||||
Read more about it in the `GenServer` documentation.
|
||||
|
||||
## A word on distributed agents
|
||||
|
||||
It is important to consider the limitations of distributed agents. Agents
|
||||
provide two APIs, one that works with anonymous functions and another
|
||||
that expects an 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 arguments tuple to the update instruction. For example, imagine
|
||||
you have an agent named `:sample` and you want to convert its inner state
|
||||
from a keyword list 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 of arguments (`[%{}]`) 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 """
|
||||
Returns a specification to start an agent under a supervisor.
|
||||
|
||||
See the "Child specification" section in the `Supervisor` module for more detailed information.
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
def child_spec(arg) do
|
||||
%{
|
||||
id: Agent,
|
||||
start: {Agent, :start_link, [arg]}
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
See `Supervisor`.
|
||||
"""
|
||||
end
|
||||
|
||||
def child_spec(arg) do
|
||||
default = %{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [arg]}
|
||||
}
|
||||
|
||||
Supervisor.child_spec(default, unquote(Macro.escape(opts)))
|
||||
end
|
||||
|
||||
defoverridable child_spec: 1
|
||||
end
|
||||
end
|
||||
|
||||
@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 `fun` is invoked in the server
|
||||
process, and should return the initial agent state. Note that `start_link/2`
|
||||
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](`:sys`) 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 an agent with the
|
||||
specified name already exists, the function returns
|
||||
`{:error, {:already_started, pid}}` with the PID of that process.
|
||||
|
||||
If the given function callback fails, the function returns `{:error, reason}`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
42
|
||||
|
||||
iex> {:error, {exception, _stacktrace}} = Agent.start(fn -> raise "oops" end)
|
||||
iex> exception
|
||||
%RuntimeError{message: "oops"}
|
||||
|
||||
"""
|
||||
@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.
|
||||
|
||||
Same as `start_link/2` but a module, function, and arguments are expected
|
||||
instead of an anonymous function; `fun` in `module` will be called with the
|
||||
given arguments `args` to initialize the state.
|
||||
"""
|
||||
@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.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start(fn -> 42 end)
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
42
|
||||
|
||||
"""
|
||||
@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 without links with the given module, function, and arguments.
|
||||
|
||||
See `start_link/4` for more information.
|
||||
"""
|
||||
@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 anonymous 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 from this function.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
exits.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
42
|
||||
|
||||
"""
|
||||
@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 arguments are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of arguments.
|
||||
"""
|
||||
@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 via the given anonymous
|
||||
function.
|
||||
|
||||
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 (that is, the "get" value)
|
||||
and the second one being the new state of the agent.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
exits.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.get_and_update(pid, fn state -> {state, state + 1} end)
|
||||
42
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
43
|
||||
|
||||
"""
|
||||
@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 via the given function.
|
||||
|
||||
Same as `get_and_update/3` but a module, function, and arguments are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of arguments.
|
||||
"""
|
||||
@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 via the given anonymous function.
|
||||
|
||||
The function `fun` is sent to the `agent` which invokes the function
|
||||
passing the agent state. The return value of `fun` becomes the new
|
||||
state of the agent.
|
||||
|
||||
This function always returns `:ok`.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
exits.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.update(pid, fn state -> state + 1 end)
|
||||
:ok
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
43
|
||||
|
||||
"""
|
||||
@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 via the given function.
|
||||
|
||||
Same as `update/3` but a module, function, and arguments are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.update(pid, Kernel, :+, [12])
|
||||
:ok
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
54
|
||||
|
||||
"""
|
||||
@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 return value of `fun` becomes the new
|
||||
state of the agent.
|
||||
|
||||
Note that `cast` returns `:ok` immediately, regardless of whether `agent` (or
|
||||
the node it should live on) exists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.cast(pid, fn state -> state + 1 end)
|
||||
:ok
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
43
|
||||
|
||||
"""
|
||||
@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 arguments are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.cast(pid, Kernel, :+, [12])
|
||||
:ok
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
54
|
||||
|
||||
"""
|
||||
@spec cast(agent, module, atom, [term]) :: :ok
|
||||
def cast(agent, module, fun, args) do
|
||||
GenServer.cast(agent, {:cast, {module, fun, args}})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Synchronously stops the agent with the given `reason`.
|
||||
|
||||
It returns `:ok` if the agent terminates with the given
|
||||
reason. If the agent terminates with another reason, the call will
|
||||
exit.
|
||||
|
||||
This function keeps OTP semantics regarding error reporting.
|
||||
If the reason is any other than `:normal`, `:shutdown` or
|
||||
`{:shutdown, _}`, an error report will be logged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.stop(pid)
|
||||
:ok
|
||||
|
||||
"""
|
||||
@spec stop(agent, reason :: term, timeout) :: :ok
|
||||
def stop(agent, reason \\ :normal, timeout \\ :infinity) do
|
||||
GenServer.stop(agent, reason, timeout)
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
defmodule Agent.Server do
|
||||
@moduledoc false
|
||||
|
||||
use GenServer
|
||||
|
||||
def init(fun) do
|
||||
_ = initial_call(fun)
|
||||
{: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
|
||||
case run(fun, [state]) do
|
||||
{reply, state} -> {:reply, reply, state}
|
||||
other -> {:stop, {:bad_return_value, other}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:update, fun}, _from, state) do
|
||||
{:reply, :ok, run(fun, [state])}
|
||||
end
|
||||
|
||||
def handle_cast({:cast, fun}, state) do
|
||||
{:noreply, run(fun, [state])}
|
||||
end
|
||||
|
||||
def code_change(_old, state, fun) do
|
||||
{:ok, run(fun, [state])}
|
||||
end
|
||||
|
||||
defp initial_call(mfa) do
|
||||
_ = Process.put(:"$initial_call", get_initial_call(mfa))
|
||||
:ok
|
||||
end
|
||||
|
||||
defp get_initial_call(fun) when is_function(fun, 0) do
|
||||
{:module, module} = Function.info(fun, :module)
|
||||
{:name, name} = Function.info(fun, :name)
|
||||
{module, name, 0}
|
||||
end
|
||||
|
||||
defp get_initial_call({mod, fun, args}) do
|
||||
{mod, fun, length(args)}
|
||||
end
|
||||
|
||||
defp run({m, f, a}, extra), do: apply(m, f, extra ++ a)
|
||||
defp run(fun, extra), do: apply(fun, extra)
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,82 +0,0 @@
|
||||
defmodule Atom do
|
||||
@moduledoc """
|
||||
Atoms are constants whose values are their own name.
|
||||
|
||||
They are often useful to enumerate over distinct values, such as:
|
||||
|
||||
iex> :apple
|
||||
:apple
|
||||
iex> :orange
|
||||
:orange
|
||||
iex> :watermelon
|
||||
:watermelon
|
||||
|
||||
Atoms are equal if their names are equal.
|
||||
|
||||
iex> :apple == :apple
|
||||
true
|
||||
iex> :apple == :orange
|
||||
false
|
||||
|
||||
Often they are used to express the state of an operation, by using
|
||||
values such as `:ok` and `:error`.
|
||||
|
||||
The booleans `true` and `false` are also atoms:
|
||||
|
||||
iex> true == :true
|
||||
true
|
||||
iex> is_atom(false)
|
||||
true
|
||||
iex> is_boolean(:false)
|
||||
true
|
||||
|
||||
Elixir allows you to skip the leading `:` for the atoms `false`, `true`,
|
||||
and `nil`.
|
||||
|
||||
Atoms must be composed of Unicode characters such as letters, numbers,
|
||||
underscore, and `@`. If the keyword has a character that does not
|
||||
belong to the category above, such as spaces, you can wrap it in
|
||||
quotes:
|
||||
|
||||
iex> :"this is an atom with spaces"
|
||||
:"this is an atom with spaces"
|
||||
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Converts an atom to a string.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Atom.to_string(:foo)
|
||||
"foo"
|
||||
|
||||
"""
|
||||
@spec to_string(atom) :: String.t()
|
||||
def to_string(atom) do
|
||||
:erlang.atom_to_binary(atom)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an atom to a charlist.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Atom.to_charlist(:"An atom")
|
||||
'An atom'
|
||||
|
||||
"""
|
||||
@spec to_charlist(atom) :: charlist
|
||||
def to_charlist(atom) do
|
||||
:erlang.atom_to_list(atom)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Atom.to_charlist/1 instead"
|
||||
@spec to_char_list(atom) :: charlist
|
||||
def to_char_list(atom), do: Atom.to_charlist(atom)
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,123 +0,0 @@
|
||||
defmodule Behaviour do
|
||||
@moduledoc """
|
||||
Mechanism for handling behaviours.
|
||||
|
||||
This module is deprecated. Instead of `defcallback/1` and
|
||||
`defmacrocallback/1`, the `@callback` and `@macrocallback`
|
||||
module attributes can be used respectively. See the
|
||||
documentation for `Module` for more information on these
|
||||
attributes.
|
||||
|
||||
Instead of `MyModule.__behaviour__(:callbacks)`,
|
||||
`MyModule.behaviour_info(:callbacks)` can be used.
|
||||
"""
|
||||
|
||||
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
|
||||
|
||||
@doc """
|
||||
Defines a function callback according to the given type specification.
|
||||
"""
|
||||
@deprecated "Use the @callback module attribute instead"
|
||||
defmacro defcallback(spec) do
|
||||
do_defcallback(:def, split_spec(spec, quote(do: term)))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a macro callback according to the given type specification.
|
||||
"""
|
||||
@deprecated "Use the @macrocallback module attribute instead"
|
||||
defmacro defmacrocallback(spec) do
|
||||
do_defcallback(:defmacro, split_spec(spec, quote(do: Macro.t())))
|
||||
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(kind, {spec, return, guards}) do
|
||||
case Macro.decompose_call(spec) do
|
||||
{name, args} ->
|
||||
do_callback(kind, name, args, return, guards)
|
||||
|
||||
_ ->
|
||||
raise ArgumentError, "invalid syntax in #{kind}callback #{Macro.to_string(spec)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_callback(kind, name, args, return, guards) do
|
||||
fun = fn
|
||||
{:"::", _, [left, right]} ->
|
||||
ensure_not_default(left)
|
||||
ensure_not_default(right)
|
||||
left
|
||||
|
||||
other ->
|
||||
ensure_not_default(other)
|
||||
other
|
||||
end
|
||||
|
||||
:lists.foreach(fun, args)
|
||||
|
||||
spec =
|
||||
quote do
|
||||
unquote(name)(unquote_splicing(args)) :: unquote(return) when unquote(guards)
|
||||
end
|
||||
|
||||
case kind do
|
||||
:def -> quote(do: @callback(unquote(spec)))
|
||||
:defmacro -> quote(do: @macrocallback(unquote(spec)))
|
||||
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
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
warning =
|
||||
"the Behaviour module is deprecated. Instead of using this module, " <>
|
||||
"use the @callback and @macrocallback module attributes. See the " <>
|
||||
"documentation for Module for more information on these attributes"
|
||||
|
||||
IO.warn(warning)
|
||||
|
||||
@doc false
|
||||
def __behaviour__(:callbacks) do
|
||||
__MODULE__.behaviour_info(:callbacks)
|
||||
end
|
||||
|
||||
def __behaviour__(:docs) do
|
||||
{:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(__MODULE__)
|
||||
|
||||
for {{kind, name, arity}, line, _, doc, _} <- docs, kind in [:callback, :macrocallback] do
|
||||
case kind do
|
||||
:callback -> {{name, arity}, line, :def, __behaviour__doc_value(doc)}
|
||||
:macrocallback -> {{name, arity}, line, :defmacro, __behaviour__doc_value(doc)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp __behaviour__doc_value(%{"en" => doc}), do: doc
|
||||
defp __behaviour__doc_value(:hidden), do: false
|
||||
defp __behaviour__doc_value(_), do: nil
|
||||
|
||||
import unquote(__MODULE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,270 +0,0 @@
|
||||
defmodule Bitwise do
|
||||
@moduledoc """
|
||||
A set of functions that perform calculations on bits.
|
||||
|
||||
All bitwise functions work only on integers; otherwise an
|
||||
`ArithmeticError` is raised. The functions `band/2`,
|
||||
`bor/2`, `bsl/2`, and `bsr/2` also have operators,
|
||||
respectively: `&&&/2`, `|||/2`, `<<</2`, and `>>>/2`.
|
||||
|
||||
## Guards
|
||||
|
||||
All bitwise functions can be used in guards:
|
||||
|
||||
iex> odd? = fn
|
||||
...> int when Bitwise.band(int, 1) == 1 -> true
|
||||
...> _ -> false
|
||||
...> end
|
||||
iex> odd?.(1)
|
||||
true
|
||||
|
||||
All functions in this module are inlined by the compiler.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@deprecated "import Bitwise instead"
|
||||
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 """
|
||||
Calculates the bitwise NOT of the argument.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bnot(2)
|
||||
-3
|
||||
|
||||
iex> bnot(2) &&& 3
|
||||
1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec bnot(integer) :: integer
|
||||
def bnot(expr) do
|
||||
:erlang.bnot(expr)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unquote(:"~~~")(expr) do
|
||||
:erlang.bnot(expr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the bitwise AND of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> band(9, 3)
|
||||
1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec band(integer, integer) :: integer
|
||||
def band(left, right) do
|
||||
:erlang.band(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise AND operator.
|
||||
|
||||
Calculates the bitwise AND of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 9 &&& 3
|
||||
1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec integer &&& integer :: integer
|
||||
def left &&& right do
|
||||
:erlang.band(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the bitwise OR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bor(9, 3)
|
||||
11
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec bor(integer, integer) :: integer
|
||||
def bor(left, right) do
|
||||
:erlang.bor(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise OR operator.
|
||||
|
||||
Calculates the bitwise OR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 9 ||| 3
|
||||
11
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec integer ||| integer :: integer
|
||||
def left ||| right do
|
||||
:erlang.bor(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the bitwise XOR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bxor(9, 3)
|
||||
10
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec bxor(integer, integer) :: integer
|
||||
def bxor(left, right) do
|
||||
:erlang.bxor(left, right)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unquote(:"^^^")(left, right) do
|
||||
:erlang.bxor(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the result of an arithmetic left bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bsl(1, 2)
|
||||
4
|
||||
|
||||
iex> bsl(1, -2)
|
||||
0
|
||||
|
||||
iex> bsl(-1, 2)
|
||||
-4
|
||||
|
||||
iex> bsl(-1, -2)
|
||||
-1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec bsl(integer, integer) :: integer
|
||||
def bsl(left, right) do
|
||||
:erlang.bsl(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic left bitshift operator.
|
||||
|
||||
Calculates the result of an arithmetic left bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 1 <<< 2
|
||||
4
|
||||
|
||||
iex> 1 <<< -2
|
||||
0
|
||||
|
||||
iex> -1 <<< 2
|
||||
-4
|
||||
|
||||
iex> -1 <<< -2
|
||||
-1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec integer <<< integer :: integer
|
||||
def left <<< right do
|
||||
:erlang.bsl(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the result of an arithmetic right bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bsr(1, 2)
|
||||
0
|
||||
|
||||
iex> bsr(1, -2)
|
||||
4
|
||||
|
||||
iex> bsr(-1, 2)
|
||||
-1
|
||||
|
||||
iex> bsr(-1, -2)
|
||||
-4
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec bsr(integer, integer) :: integer
|
||||
def bsr(left, right) do
|
||||
:erlang.bsr(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic right bitshift operator.
|
||||
|
||||
Calculates the result of an arithmetic right bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 1 >>> 2
|
||||
0
|
||||
|
||||
iex> 1 >>> -2
|
||||
4
|
||||
|
||||
iex> -1 >>> 2
|
||||
-1
|
||||
|
||||
iex> -1 >>> -2
|
||||
-4
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec integer >>> integer :: integer
|
||||
def left >>> right do
|
||||
:erlang.bsr(left, right)
|
||||
end
|
||||
end
|
||||
@@ -1,893 +0,0 @@
|
||||
defmodule Calendar do
|
||||
@moduledoc """
|
||||
This module defines the responsibilities for working with
|
||||
calendars, dates, times and datetimes in Elixir.
|
||||
|
||||
Currently it defines types and the minimal implementation
|
||||
for a calendar behaviour in Elixir. The goal of the Calendar
|
||||
features in Elixir is to provide a base for interoperability
|
||||
instead of full-featured datetime API.
|
||||
|
||||
For the actual date, time and datetime structures, see `Date`,
|
||||
`Time`, `NaiveDateTime` and `DateTime`.
|
||||
|
||||
Note designations for year, month, day, and the like, are overspecified
|
||||
(i.e. an integer instead of `1..12` for months) because different
|
||||
calendars may have a different number of days per month, months per year and so on.
|
||||
"""
|
||||
|
||||
@type year :: integer
|
||||
@type month :: pos_integer
|
||||
@type day :: pos_integer
|
||||
@type week :: pos_integer
|
||||
@type day_of_week :: non_neg_integer
|
||||
@type era :: non_neg_integer
|
||||
|
||||
@typedoc """
|
||||
A tuple representing the `day` and the `era`.
|
||||
"""
|
||||
@type day_of_era :: {day :: non_neg_integer(), era}
|
||||
|
||||
@type hour :: non_neg_integer
|
||||
@type minute :: non_neg_integer
|
||||
@type second :: non_neg_integer
|
||||
|
||||
@typedoc """
|
||||
The internal time format is used when converting between calendars.
|
||||
|
||||
It represents time as a fraction of a day (starting from midnight).
|
||||
`parts_in_day` specifies how much of the day is already passed,
|
||||
while `parts_per_day` signifies how many parts there fit in a day.
|
||||
"""
|
||||
@type day_fraction :: {parts_in_day :: non_neg_integer, parts_per_day :: pos_integer}
|
||||
|
||||
@typedoc """
|
||||
The internal date format that is used when converting between calendars.
|
||||
|
||||
This is the number of days including the fractional part that has passed of
|
||||
the last day since 0000-01-01+00:00T00:00.000000 in ISO 8601 notation (also
|
||||
known as midnight 1 January BC 1 of the proleptic Gregorian calendar).
|
||||
"""
|
||||
@type iso_days :: {days :: integer, day_fraction}
|
||||
|
||||
@typedoc """
|
||||
Microseconds with stored precision.
|
||||
|
||||
The precision represents the number of digits that must be used when
|
||||
representing the microseconds to external format. If the precision is 0,
|
||||
it means microseconds must be skipped.
|
||||
"""
|
||||
@type microsecond :: {value :: non_neg_integer, precision :: non_neg_integer}
|
||||
|
||||
@typedoc "A calendar implementation"
|
||||
@type calendar :: module
|
||||
|
||||
@typedoc "The time zone ID according to the IANA tz database (for example, Europe/Zurich)"
|
||||
@type time_zone :: String.t()
|
||||
|
||||
@typedoc "The time zone abbreviation (for example, CET or CEST or BST, and such)"
|
||||
@type zone_abbr :: String.t()
|
||||
|
||||
@typedoc """
|
||||
The time zone UTC offset in seconds for standard time.
|
||||
|
||||
See also `t:std_offset/0`.
|
||||
"""
|
||||
@type utc_offset :: integer
|
||||
|
||||
@typedoc """
|
||||
The time zone standard offset in seconds (typically not zero in summer times).
|
||||
|
||||
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
|
||||
"""
|
||||
@type std_offset :: integer
|
||||
|
||||
@typedoc "Any map/struct that contains the date fields"
|
||||
@type date :: %{optional(any) => any, calendar: calendar, year: year, month: month, day: day}
|
||||
|
||||
@typedoc "Any map/struct that contains the time fields"
|
||||
@type time :: %{
|
||||
optional(any) => any,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}
|
||||
|
||||
@typedoc "Any map/struct that contains the naive_datetime fields"
|
||||
@type naive_datetime :: %{
|
||||
optional(any) => any,
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}
|
||||
|
||||
@typedoc "Any map/struct that contains the datetime fields"
|
||||
@type datetime :: %{
|
||||
optional(any) => any,
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
time_zone: time_zone,
|
||||
zone_abbr: zone_abbr,
|
||||
utc_offset: utc_offset,
|
||||
std_offset: std_offset
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
Specifies the time zone database for calendar operations.
|
||||
|
||||
Many functions in the `DateTime` module require a time zone database.
|
||||
By default, it uses the default time zone database returned by
|
||||
`Calendar.get_time_zone_database/0`, which defaults to
|
||||
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
|
||||
datetimes and returns `{:error, :utc_only_time_zone_database}`
|
||||
for any other time zone.
|
||||
|
||||
Other time zone databases (including ones provided by packages)
|
||||
can be configured as default either via configuration:
|
||||
|
||||
config :elixir, :time_zone_database, CustomTimeZoneDatabase
|
||||
|
||||
or by calling `Calendar.put_time_zone_database/1`.
|
||||
|
||||
See `Calendar.TimeZoneDatabase` for more information on custom
|
||||
time zone databases.
|
||||
"""
|
||||
@type time_zone_database :: module()
|
||||
|
||||
@doc """
|
||||
Returns how many days there are in the given year-month.
|
||||
"""
|
||||
@callback days_in_month(year, month) :: day
|
||||
|
||||
@doc """
|
||||
Returns how many months there are in the given year.
|
||||
"""
|
||||
@callback months_in_year(year) :: month
|
||||
|
||||
@doc """
|
||||
Returns `true` if the given year is a leap year.
|
||||
|
||||
A leap year is a year of a longer length than normal. The exact meaning
|
||||
is up to the calendar. A calendar must return `false` if it does not support
|
||||
the concept of leap years.
|
||||
"""
|
||||
@callback leap_year?(year) :: boolean
|
||||
|
||||
@doc """
|
||||
Calculates the day of the week from the given `year`, `month`, and `day`.
|
||||
|
||||
The `starting_on` represents the starting day of the week. All
|
||||
calendars must support at least the `:default` value. They may
|
||||
also support other values representing their days of the week.
|
||||
"""
|
||||
@callback day_of_week(year, month, day, starting_on :: :default | atom) ::
|
||||
{day_of_week(), first_day_of_week :: non_neg_integer(),
|
||||
last_day_of_week :: non_neg_integer()}
|
||||
|
||||
@doc """
|
||||
Calculates the day of the year from the given `year`, `month`, and `day`.
|
||||
"""
|
||||
@callback day_of_year(year, month, day) :: non_neg_integer()
|
||||
|
||||
@doc """
|
||||
Calculates the quarter of the year from the given `year`, `month`, and `day`.
|
||||
"""
|
||||
@callback quarter_of_year(year, month, day) :: non_neg_integer()
|
||||
|
||||
@doc """
|
||||
Calculates the year and era from the given `year`.
|
||||
"""
|
||||
@callback year_of_era(year, month, day) :: {year, era}
|
||||
|
||||
@doc """
|
||||
Calculates the day and era from the given `year`, `month`, and `day`.
|
||||
"""
|
||||
@callback day_of_era(year, month, day) :: day_of_era()
|
||||
|
||||
@doc """
|
||||
Converts the date into a string according to the calendar.
|
||||
"""
|
||||
@callback date_to_string(year, month, day) :: String.t()
|
||||
|
||||
@doc """
|
||||
Converts the datetime (without time zone) into a string according to the calendar.
|
||||
"""
|
||||
@callback naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) ::
|
||||
String.t()
|
||||
|
||||
@doc """
|
||||
Converts the datetime (with time zone) into a string according to the calendar.
|
||||
"""
|
||||
@callback datetime_to_string(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
time_zone,
|
||||
zone_abbr,
|
||||
utc_offset,
|
||||
std_offset
|
||||
) :: String.t()
|
||||
|
||||
@doc """
|
||||
Converts the time into a string according to the calendar.
|
||||
"""
|
||||
@callback time_to_string(hour, minute, second, microsecond) :: String.t()
|
||||
|
||||
@doc """
|
||||
Converts the given datetime (without time zone) into the `t:iso_days/0` format.
|
||||
"""
|
||||
@callback naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) ::
|
||||
iso_days
|
||||
|
||||
@doc """
|
||||
Converts `t:iso_days/0` to the Calendar's datetime format.
|
||||
"""
|
||||
@callback naive_datetime_from_iso_days(iso_days) ::
|
||||
{year, month, day, hour, minute, second, microsecond}
|
||||
|
||||
@doc """
|
||||
Converts the given time to the `t:day_fraction/0` format.
|
||||
"""
|
||||
@callback time_to_day_fraction(hour, minute, second, microsecond) :: day_fraction
|
||||
|
||||
@doc """
|
||||
Converts `t:day_fraction/0` to the Calendar's time format.
|
||||
"""
|
||||
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
|
||||
|
||||
@doc """
|
||||
Define the rollover moment for the given calendar.
|
||||
|
||||
This is the moment, in your calendar, when the current day ends
|
||||
and the next day starts.
|
||||
|
||||
The result of this function is used to check if two calendars rollover at
|
||||
the same time of day. If they do not, we can only convert datetimes and times
|
||||
between them. If they do, this means that we can also convert dates as well
|
||||
as naive datetimes between them.
|
||||
|
||||
This day fraction should be in its most simplified form possible, to make comparisons fast.
|
||||
|
||||
## Examples
|
||||
|
||||
* If, in your Calendar, a new day starts at midnight, return {0, 1}.
|
||||
* If, in your Calendar, a new day starts at sunrise, return {1, 4}.
|
||||
* If, in your Calendar, a new day starts at noon, return {1, 2}.
|
||||
* If, in your Calendar, a new day starts at sunset, return {3, 4}.
|
||||
|
||||
"""
|
||||
@callback day_rollover_relative_to_midnight_utc() :: day_fraction
|
||||
|
||||
@doc """
|
||||
Should return `true` if the given date describes a proper date in the calendar.
|
||||
"""
|
||||
@callback valid_date?(year, month, day) :: boolean
|
||||
|
||||
@doc """
|
||||
Should return `true` if the given time describes a proper time in the calendar.
|
||||
"""
|
||||
@callback valid_time?(hour, minute, second, microsecond) :: boolean
|
||||
|
||||
@doc """
|
||||
Parses the string representation for a time returned by `c:time_to_string/4`
|
||||
into a time-tuple.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@callback parse_time(String.t()) ::
|
||||
{:ok, {hour, minute, second, microsecond}}
|
||||
| {:error, atom}
|
||||
|
||||
@doc """
|
||||
Parses the string representation for a date returned by `c:date_to_string/3`
|
||||
into a date-tuple.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@callback parse_date(String.t()) ::
|
||||
{:ok, {year, month, day}}
|
||||
| {:error, atom}
|
||||
|
||||
@doc """
|
||||
Parses the string representation for a naive datetime returned by
|
||||
`c:naive_datetime_to_string/7` into a naive-datetime-tuple.
|
||||
|
||||
The given string may contain a timezone offset but it is ignored.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@callback parse_naive_datetime(String.t()) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}}
|
||||
| {:error, atom}
|
||||
|
||||
@doc """
|
||||
Parses the string representation for a datetime returned by
|
||||
`c:datetime_to_string/11` into a datetime-tuple.
|
||||
|
||||
The returned datetime must be in UTC. The original `utc_offset`
|
||||
it was written in must be returned in the result.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@callback parse_utc_datetime(String.t()) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset}
|
||||
| {:error, atom}
|
||||
|
||||
# General Helpers
|
||||
|
||||
@doc """
|
||||
Returns `true` if two calendars have the same moment of starting a new day,
|
||||
`false` otherwise.
|
||||
|
||||
If two calendars are not compatible, we can only convert datetimes and times
|
||||
between them. If they are compatible, this means that we can also convert
|
||||
dates as well as naive datetimes between them.
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec compatible_calendars?(Calendar.calendar(), Calendar.calendar()) :: boolean
|
||||
def compatible_calendars?(calendar, calendar), do: true
|
||||
|
||||
def compatible_calendars?(calendar1, calendar2) do
|
||||
calendar1.day_rollover_relative_to_midnight_utc() ==
|
||||
calendar2.day_rollover_relative_to_midnight_utc()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a microsecond tuple truncated to a given precision (`:microsecond`,
|
||||
`:millisecond` or `:second`).
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec truncate(Calendar.microsecond(), :microsecond | :millisecond | :second) ::
|
||||
Calendar.microsecond()
|
||||
def truncate(microsecond_tuple, :microsecond), do: microsecond_tuple
|
||||
|
||||
def truncate({microsecond, precision}, :millisecond) do
|
||||
output_precision = min(precision, 3)
|
||||
{div(microsecond, 1000) * 1000, output_precision}
|
||||
end
|
||||
|
||||
def truncate(_, :second), do: {0, 0}
|
||||
|
||||
@doc """
|
||||
Sets the current time zone database.
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec put_time_zone_database(time_zone_database()) :: :ok
|
||||
def put_time_zone_database(database) when is_atom(database) do
|
||||
Application.put_env(:elixir, :time_zone_database, database)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the current time zone database.
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec get_time_zone_database() :: time_zone_database()
|
||||
def get_time_zone_database() do
|
||||
Application.fetch_env!(:elixir, :time_zone_database)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats received datetime into a string.
|
||||
|
||||
The datetime can be any of the Calendar types (`Time`, `Date`,
|
||||
`NaiveDateTime`, and `DateTime`) or any map, as long as they
|
||||
contain all of the relevant fields necessary for formatting.
|
||||
For example, if you use `%Y` to format the year, the datetime
|
||||
must have the `:year` field. Therefore, if you pass a `Time`,
|
||||
or a map without the `:year` field to a format that expects `%Y`,
|
||||
an error will be raised.
|
||||
|
||||
## Options
|
||||
|
||||
* `:preferred_datetime` - a string for the preferred format to show datetimes,
|
||||
it can't contain the `%c` format and defaults to `"%Y-%m-%d %H:%M:%S"`
|
||||
if the option is not received
|
||||
|
||||
* `:preferred_date` - a string for the preferred format to show dates,
|
||||
it can't contain the `%x` format and defaults to `"%Y-%m-%d"`
|
||||
if the option is not received
|
||||
|
||||
* `:preferred_time` - a string for the preferred format to show times,
|
||||
it can't contain the `%X` format and defaults to `"%H:%M:%S"`
|
||||
if the option is not received
|
||||
|
||||
* `:am_pm_names` - a function that receives either `:am` or `:pm` and returns
|
||||
the name of the period of the day, if the option is not received it defaults
|
||||
to a function that returns `"am"` and `"pm"`, respectively
|
||||
|
||||
* `:month_names` - a function that receives a number and returns the name of
|
||||
the corresponding month, if the option is not received it defaults to a
|
||||
function that returns the month names in English
|
||||
|
||||
* `:abbreviated_month_names` - a function that receives a number and returns the
|
||||
abbreviated name of the corresponding month, if the option is not received it
|
||||
defaults to a function that returns the abbreviated month names in English
|
||||
|
||||
* `:day_of_week_names` - a function that receives a number and returns the name of
|
||||
the corresponding day of week, if the option is not received it defaults to a
|
||||
function that returns the day of week names in English
|
||||
|
||||
* `:abbreviated_day_of_week_names` - a function that receives a number and returns
|
||||
the abbreviated name of the corresponding day of week, if the option is not received
|
||||
it defaults to a function that returns the abbreviated day of week names in English
|
||||
|
||||
## Formatting syntax
|
||||
|
||||
The formatting syntax for strftime is a sequence of characters in the following format:
|
||||
|
||||
%<padding><width><format>
|
||||
|
||||
where:
|
||||
|
||||
* `%`: indicates the start of a formatted section
|
||||
* `<padding>`: set the padding (see below)
|
||||
* `<width>`: a number indicating the minimum size of the formatted section
|
||||
* `<format>`: the format itself (see below)
|
||||
|
||||
### Accepted padding options
|
||||
|
||||
* `-`: no padding, removes all padding from the format
|
||||
* `_`: pad with spaces
|
||||
* `0`: pad with zeroes
|
||||
|
||||
### Accepted formats
|
||||
|
||||
The accepted formats are:
|
||||
|
||||
Format | Description | Examples (in ISO)
|
||||
:----- | :-----------------------------------------------------------------------| :------------------------
|
||||
a | Abbreviated name of day | Mon
|
||||
A | Full name of day | Monday
|
||||
b | Abbreviated month name | Jan
|
||||
B | Full month name | January
|
||||
c | Preferred date+time representation | 2018-10-17 12:34:56
|
||||
d | Day of the month | 01, 31
|
||||
f | Microseconds *(does not support width and padding modifiers)* | 000000, 999999, 0123
|
||||
H | Hour using a 24-hour clock | 00, 23
|
||||
I | Hour using a 12-hour clock | 01, 12
|
||||
j | Day of the year | 001, 366
|
||||
m | Month | 01, 12
|
||||
M | Minute | 00, 59
|
||||
p | "AM" or "PM" (noon is "PM", midnight as "AM") | AM, PM
|
||||
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
|
||||
q | Quarter | 1, 2, 3, 4
|
||||
S | Second | 00, 59, 60
|
||||
u | Day of the week | 1 (Monday), 7 (Sunday)
|
||||
x | Preferred date (without time) representation | 2018-10-17
|
||||
X | Preferred time (without date) representation | 12:34:56
|
||||
y | Year as 2-digits | 01, 01, 86, 18
|
||||
Y | Year | -0001, 0001, 1986
|
||||
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
|
||||
Z | Time zone abbreviation (empty string if naive) | CET, BRST
|
||||
% | Literal "%" character | %
|
||||
|
||||
Any other character will be interpreted as an invalid format and raise an error
|
||||
|
||||
## Examples
|
||||
|
||||
Without options:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%y-%m-%d %I:%M:%S %p")
|
||||
"19-08-26 01:52:06 PM"
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%a, %B %d %Y")
|
||||
"Mon, August 26 2019"
|
||||
|
||||
iex> Calendar.strftime(~U[2020-04-02 13:52:06.0Z], "%B %-d, %Y")
|
||||
"April 2, 2020"
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c")
|
||||
"2019-08-26 13:52:06"
|
||||
|
||||
With options:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c", preferred_datetime: "%H:%M:%S %d-%m-%y")
|
||||
"13:52:06 26-08-19"
|
||||
|
||||
iex> Calendar.strftime(
|
||||
...> ~U[2019-08-26 13:52:06.0Z],
|
||||
...> "%A",
|
||||
...> day_of_week_names: fn day_of_week ->
|
||||
...> {"segunda-feira", "terça-feira", "quarta-feira", "quinta-feira",
|
||||
...> "sexta-feira", "sábado", "domingo"}
|
||||
...> |> elem(day_of_week - 1)
|
||||
...> end
|
||||
...>)
|
||||
"segunda-feira"
|
||||
|
||||
iex> Calendar.strftime(
|
||||
...> ~U[2019-08-26 13:52:06.0Z],
|
||||
...> "%B",
|
||||
...> month_names: fn month ->
|
||||
...> {"январь", "февраль", "март", "апрель", "май", "июнь",
|
||||
...> "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"}
|
||||
...> |> elem(month - 1)
|
||||
...> end
|
||||
...>)
|
||||
"август"
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec strftime(map(), String.t(), keyword()) :: String.t()
|
||||
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
|
||||
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
|
||||
parse(
|
||||
string_format,
|
||||
date_or_time_or_datetime,
|
||||
options(user_options),
|
||||
[]
|
||||
)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp parse("", _datetime, _format_options, acc),
|
||||
do: Enum.reverse(acc)
|
||||
|
||||
defp parse("%" <> rest, datetime, format_options, acc),
|
||||
do: parse_modifiers(rest, nil, nil, {datetime, format_options, acc})
|
||||
|
||||
defp parse(<<char, rest::binary>>, datetime, format_options, acc),
|
||||
do: parse(rest, datetime, format_options, [char | acc])
|
||||
|
||||
defp parse_modifiers("-" <> rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, "", parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers("0" <> rest, nil, nil, parser_data) do
|
||||
parse_modifiers(rest, nil, ?0, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers("_" <> rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, ?\s, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
||||
new_width = (width || 0) * 10 + (digit - ?0)
|
||||
|
||||
parse_modifiers(rest, new_width, pad, parser_data)
|
||||
end
|
||||
|
||||
# set default padding if none was specified
|
||||
defp parse_modifiers(<<format, _::binary>> = rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, default_pad(format), parser_data)
|
||||
end
|
||||
|
||||
# set default width if none was specified
|
||||
defp parse_modifiers(<<format, _::binary>> = rest, nil, pad, parser_data) do
|
||||
parse_modifiers(rest, default_width(format), pad, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers(rest, width, pad, {datetime, format_options, acc}) do
|
||||
format_modifiers(rest, width, pad, datetime, format_options, acc)
|
||||
end
|
||||
|
||||
defp am_pm(hour, format_options) when hour > 11 do
|
||||
format_options.am_pm_names.(:pm)
|
||||
end
|
||||
|
||||
defp am_pm(hour, format_options) when hour <= 11 do
|
||||
format_options.am_pm_names.(:am)
|
||||
end
|
||||
|
||||
defp default_pad(format) when format in 'aAbBpPZ', do: ?\s
|
||||
defp default_pad(_format), do: ?0
|
||||
|
||||
defp default_width(format) when format in 'dHImMSy', do: 2
|
||||
defp default_width(?j), do: 3
|
||||
defp default_width(format) when format in 'Yz', do: 4
|
||||
defp default_width(_format), do: 0
|
||||
|
||||
# Literally just %
|
||||
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
|
||||
end
|
||||
|
||||
# Abbreviated name of day
|
||||
defp format_modifiers("a" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime
|
||||
|> Date.day_of_week()
|
||||
|> format_options.abbreviated_day_of_week_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Full name of day
|
||||
defp format_modifiers("A" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime
|
||||
|> Date.day_of_week()
|
||||
|> format_options.day_of_week_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Abbreviated month name
|
||||
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime.month
|
||||
|> format_options.abbreviated_month_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Full month name
|
||||
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> format_options.month_names.() |> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred date+time representation
|
||||
defp format_modifiers(
|
||||
"c" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_datetime_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_datetime within another preferred_datetime format"
|
||||
end
|
||||
|
||||
defp format_modifiers("c" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_datetime
|
||||
|> parse(datetime, %{format_options | preferred_datetime_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the month
|
||||
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Microseconds
|
||||
defp format_modifiers("f" <> rest, _width, _pad, datetime, format_options, acc) do
|
||||
{microsecond, precision} = datetime.microsecond
|
||||
|
||||
result =
|
||||
microsecond
|
||||
|> Integer.to_string()
|
||||
|> String.pad_leading(6, "0")
|
||||
|> binary_part(0, max(precision, 1))
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Hour using a 24-hour clock
|
||||
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Hour using a 12-hour clock
|
||||
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the year
|
||||
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Month
|
||||
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Minute
|
||||
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# "AM" or "PM" (noon is "PM", midnight as "AM")
|
||||
defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.hour |> am_pm(format_options) |> String.upcase() |> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# "am" or "pm" (noon is "pm", midnight as "am")
|
||||
defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime.hour
|
||||
|> am_pm(format_options)
|
||||
|> String.downcase()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Quarter
|
||||
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Second
|
||||
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the week
|
||||
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred date (without time) representation
|
||||
defp format_modifiers(
|
||||
"x" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_date_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_date within another preferred_date format"
|
||||
end
|
||||
|
||||
defp format_modifiers("x" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_date
|
||||
|> parse(datetime, %{format_options | preferred_date_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred time (without date) representation
|
||||
defp format_modifiers(
|
||||
"X" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_time_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_time within another preferred_time format"
|
||||
end
|
||||
|
||||
defp format_modifiers("X" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_time
|
||||
|> parse(datetime, %{format_options | preferred_time_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year as 2-digits
|
||||
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year
|
||||
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
{sign, year} =
|
||||
if datetime.year < 0 do
|
||||
{?-, -datetime.year}
|
||||
else
|
||||
{[], datetime.year}
|
||||
end
|
||||
|
||||
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# +hhmm/-hhmm time zone offset from UTC (empty string if naive)
|
||||
defp format_modifiers(
|
||||
"z" <> rest,
|
||||
width,
|
||||
pad,
|
||||
datetime = %{utc_offset: utc_offset, std_offset: std_offset},
|
||||
format_options,
|
||||
acc
|
||||
) do
|
||||
absolute_offset = abs(utc_offset + std_offset)
|
||||
|
||||
offset_number =
|
||||
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
|
||||
|
||||
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
|
||||
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
defp format_modifiers("z" <> rest, _width, _pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, ["" | acc])
|
||||
end
|
||||
|
||||
# Time zone abbreviation (empty string if naive)
|
||||
defp format_modifiers("Z" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Map.get(:zone_abbr, "") |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
defp format_modifiers(rest, _width, _pad, _datetime, _format_options, _acc) do
|
||||
{next, _rest} = String.next_grapheme(rest) || {"", ""}
|
||||
raise ArgumentError, "invalid strftime format: %#{next}"
|
||||
end
|
||||
|
||||
defp pad_preferred(result, width, pad) when length(result) < width do
|
||||
pad_preferred([pad | result], width, pad)
|
||||
end
|
||||
|
||||
defp pad_preferred(result, _width, _pad), do: result
|
||||
|
||||
defp pad_leading(string, count, padding) do
|
||||
to_pad = count - byte_size(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
defp do_pad_leading(0, _, acc), do: acc
|
||||
|
||||
defp do_pad_leading(count, padding, acc),
|
||||
do: do_pad_leading(count - 1, padding, [padding | acc])
|
||||
|
||||
defp options(user_options) do
|
||||
default_options = %{
|
||||
preferred_date: "%Y-%m-%d",
|
||||
preferred_time: "%H:%M:%S",
|
||||
preferred_datetime: "%Y-%m-%d %H:%M:%S",
|
||||
am_pm_names: fn
|
||||
:am -> "am"
|
||||
:pm -> "pm"
|
||||
end,
|
||||
month_names: fn month ->
|
||||
{"January", "February", "March", "April", "May", "June", "July", "August", "September",
|
||||
"October", "November", "December"}
|
||||
|> elem(month - 1)
|
||||
end,
|
||||
day_of_week_names: fn day_of_week ->
|
||||
{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
|
||||
|> elem(day_of_week - 1)
|
||||
end,
|
||||
abbreviated_month_names: fn month ->
|
||||
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|
||||
|> elem(month - 1)
|
||||
end,
|
||||
abbreviated_day_of_week_names: fn day_of_week ->
|
||||
{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} |> elem(day_of_week - 1)
|
||||
end,
|
||||
preferred_datetime_invoked: false,
|
||||
preferred_date_invoked: false,
|
||||
preferred_time_invoked: false
|
||||
}
|
||||
|
||||
Enum.reduce(user_options, default_options, fn {key, value}, acc ->
|
||||
if Map.has_key?(acc, key) do
|
||||
%{acc | key => value}
|
||||
else
|
||||
raise ArgumentError, "unknown option #{inspect(key)} given to Calendar.strftime/3"
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,227 +0,0 @@
|
||||
defmodule Date.Range do
|
||||
@moduledoc """
|
||||
Returns an inclusive range between dates.
|
||||
|
||||
Ranges must be created with the `Date.range/2` or `Date.range/3` function.
|
||||
|
||||
The following fields are public:
|
||||
|
||||
* `:first` - the initial date on the range
|
||||
* `:last` - the last date on the range
|
||||
* `:step` - (since v1.12.0) the step
|
||||
|
||||
The remaining fields are private and should not be accessed.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
first: Date.t(),
|
||||
last: Date.t(),
|
||||
first_in_iso_days: days(),
|
||||
last_in_iso_days: days(),
|
||||
step: pos_integer | neg_integer
|
||||
}
|
||||
|
||||
@typep days() :: integer()
|
||||
|
||||
@enforce_keys [:first, :last, :first_in_iso_days, :last_in_iso_days, :step]
|
||||
defstruct [:first, :last, :first_in_iso_days, :last_in_iso_days, :step]
|
||||
|
||||
defimpl Enumerable do
|
||||
def member?(
|
||||
%Date.Range{
|
||||
first: %{calendar: calendar},
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
} = range,
|
||||
%Date{calendar: calendar} = date
|
||||
) do
|
||||
{days, _} = Date.to_iso_days(date)
|
||||
|
||||
cond do
|
||||
empty?(range) ->
|
||||
{:ok, false}
|
||||
|
||||
first_days <= last_days ->
|
||||
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
|
||||
|
||||
true ->
|
||||
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
|
||||
end
|
||||
end
|
||||
|
||||
def member?(%Date.Range{step: _}, _) do
|
||||
{:ok, false}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def member?(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range,
|
||||
date
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
member?(Map.put(date_range, :step, step), date)
|
||||
end
|
||||
|
||||
def count(range) do
|
||||
{:ok, size(range)}
|
||||
end
|
||||
|
||||
def slice(
|
||||
%Date.Range{
|
||||
first_in_iso_days: first,
|
||||
first: %{calendar: calendar},
|
||||
step: step
|
||||
} = range
|
||||
) do
|
||||
{:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def slice(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
slice(Map.put(date_range, :step, step))
|
||||
end
|
||||
|
||||
defp slice(current, _step, 1, calendar) do
|
||||
[date_from_iso_days(current, calendar)]
|
||||
end
|
||||
|
||||
defp slice(current, step, remaining, calendar) do
|
||||
[
|
||||
date_from_iso_days(current, calendar)
|
||||
| slice(current + step, step, remaining - 1, calendar)
|
||||
]
|
||||
end
|
||||
|
||||
def reduce(
|
||||
%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
first: %{calendar: calendar},
|
||||
step: step
|
||||
},
|
||||
acc,
|
||||
fun
|
||||
) do
|
||||
reduce(first_days, last_days, acc, fun, step, calendar)
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def reduce(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range,
|
||||
acc,
|
||||
fun
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
reduce(Map.put(date_range, :step, step), acc, fun)
|
||||
end
|
||||
|
||||
defp reduce(_first_days, _last_days, {:halt, acc}, _fun, _step, _calendar) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp reduce(first_days, last_days, {:suspend, acc}, fun, step, calendar) do
|
||||
{:suspended, acc, &reduce(first_days, last_days, &1, fun, step, calendar)}
|
||||
end
|
||||
|
||||
defp reduce(first_days, last_days, {:cont, acc}, fun, step, calendar)
|
||||
when step > 0 and first_days <= last_days
|
||||
when step < 0 and first_days >= last_days do
|
||||
reduce(
|
||||
first_days + step,
|
||||
last_days,
|
||||
fun.(date_from_iso_days(first_days, calendar), acc),
|
||||
fun,
|
||||
step,
|
||||
calendar
|
||||
)
|
||||
end
|
||||
|
||||
defp reduce(_, _, {:cont, acc}, _fun, _step, _calendar) do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
defp date_from_iso_days(days, Calendar.ISO) do
|
||||
{year, month, day} = Calendar.ISO.date_from_iso_days(days)
|
||||
%Date{year: year, month: month, day: day, calendar: Calendar.ISO}
|
||||
end
|
||||
|
||||
defp date_from_iso_days(days, calendar) do
|
||||
{year, month, day, _, _, _, _} =
|
||||
calendar.naive_datetime_from_iso_days({days, {0, 86_400_000_000}})
|
||||
|
||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||
end
|
||||
|
||||
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step})
|
||||
when step > 0 and first_days > last_days,
|
||||
do: 0
|
||||
|
||||
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step})
|
||||
when step < 0 and first_days < last_days,
|
||||
do: 0
|
||||
|
||||
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step}),
|
||||
do: abs(div(last_days - first_days, step)) + 1
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
defp size(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
size(Map.put(date_range, :step, step))
|
||||
end
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step > 0 and first_days > last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step < 0 and first_days < last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{step: _}), do: false
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
defp empty?(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
empty?(Map.put(date_range, :step, step))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
import Kernel, except: [inspect: 2]
|
||||
|
||||
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
|
||||
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
|
||||
end
|
||||
|
||||
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
|
||||
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def inspect(%{__struct__: Date.Range, first: first, last: last} = date_range, opts) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
inspect(Map.put(date_range, :step, step), opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,841 +0,0 @@
|
||||
defmodule Time do
|
||||
@moduledoc """
|
||||
A Time struct and functions.
|
||||
|
||||
The Time struct contains the fields hour, minute, second and microseconds.
|
||||
New times can be built with the `new/4` function or using the
|
||||
`~T` (see `sigil_T/2`) sigil:
|
||||
|
||||
iex> ~T[23:00:07.001]
|
||||
~T[23:00:07.001]
|
||||
|
||||
Both `new/4` and sigil return a struct where the time fields can
|
||||
be accessed directly:
|
||||
|
||||
iex> time = ~T[23:00:07.001]
|
||||
iex> time.hour
|
||||
23
|
||||
iex> time.microsecond
|
||||
{1000, 3}
|
||||
|
||||
The functions on this module work with the `Time` struct as well
|
||||
as any struct that contains the same fields as the `Time` struct,
|
||||
such as `NaiveDateTime` and `DateTime`. Such functions expect
|
||||
`t:Calendar.time/0` in their typespecs (instead of `t:t/0`).
|
||||
|
||||
Developers should avoid creating the Time structs directly
|
||||
and instead rely on the functions provided by this module as well
|
||||
as the ones in third-party calendar libraries.
|
||||
|
||||
## Comparing times
|
||||
|
||||
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
|
||||
and based on the `Time` struct fields. For proper comparison between
|
||||
times, use the `compare/2` function. The existence of the `compare/2`
|
||||
function in this module also allows using `Enum.min/2` and `Enum.max/2`
|
||||
functions to get the minimum and maximum time of an `Enum`. For example:
|
||||
|
||||
iex> Enum.min([~T[23:00:07.001], ~T[10:00:07.001]], Time)
|
||||
~T[10:00:07.001]
|
||||
"""
|
||||
|
||||
@enforce_keys [:hour, :minute, :second]
|
||||
defstruct [:hour, :minute, :second, microsecond: {0, 0}, calendar: Calendar.ISO]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
hour: Calendar.hour(),
|
||||
minute: Calendar.minute(),
|
||||
second: Calendar.second(),
|
||||
microsecond: Calendar.microsecond(),
|
||||
calendar: Calendar.calendar()
|
||||
}
|
||||
|
||||
@parts_per_day 86_400_000_000
|
||||
@seconds_per_day 24 * 60 * 60
|
||||
|
||||
@doc """
|
||||
Returns the current time in UTC.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> time = Time.utc_now()
|
||||
iex> time.hour >= 0
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec utc_now(Calendar.calendar()) :: t
|
||||
def utc_now(calendar \\ Calendar.ISO) do
|
||||
{:ok, _, time, microsecond} = Calendar.ISO.from_unix(:os.system_time(), :native)
|
||||
{hour, minute, second} = time
|
||||
|
||||
iso_time = %Time{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
calendar: Calendar.ISO
|
||||
}
|
||||
|
||||
convert!(iso_time, calendar)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new time.
|
||||
|
||||
Expects all values to be integers. Returns `{:ok, time}` if each
|
||||
entry fits its appropriate range, returns `{:error, reason}` otherwise.
|
||||
|
||||
Microseconds can also be given with a precision, which must be an
|
||||
integer between 0 and 6.
|
||||
|
||||
The built-in calendar does not support leap seconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.new(0, 0, 0, 0)
|
||||
{:ok, ~T[00:00:00.000000]}
|
||||
iex> Time.new(23, 59, 59, 999_999)
|
||||
{:ok, ~T[23:59:59.999999]}
|
||||
|
||||
iex> Time.new(24, 59, 59, 999_999)
|
||||
{:error, :invalid_time}
|
||||
iex> Time.new(23, 60, 59, 999_999)
|
||||
{:error, :invalid_time}
|
||||
iex> Time.new(23, 59, 60, 999_999)
|
||||
{:error, :invalid_time}
|
||||
iex> Time.new(23, 59, 59, 1_000_000)
|
||||
{:error, :invalid_time}
|
||||
|
||||
# Invalid precision
|
||||
Time.new(23, 59, 59, {999_999, 10})
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@spec new(
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.calendar()
|
||||
) :: {:ok, t} | {:error, atom}
|
||||
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
|
||||
def new(hour, minute, second, microsecond, calendar) when is_integer(microsecond) do
|
||||
new(hour, minute, second, {microsecond, 6}, calendar)
|
||||
end
|
||||
|
||||
def new(hour, minute, second, {microsecond, precision}, calendar)
|
||||
when is_integer(hour) and is_integer(minute) and is_integer(second) and
|
||||
is_integer(microsecond) and is_integer(precision) do
|
||||
case calendar.valid_time?(hour, minute, second, {microsecond, precision}) do
|
||||
true ->
|
||||
time = %Time{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision},
|
||||
calendar: calendar
|
||||
}
|
||||
|
||||
{:ok, time}
|
||||
|
||||
false ->
|
||||
{:error, :invalid_time}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new time.
|
||||
|
||||
Expects all values to be integers. Returns `time` if each
|
||||
entry fits its appropriate range, raises if the time is invalid.
|
||||
|
||||
Microseconds can also be given with a precision, which must be an
|
||||
integer between 0 and 6.
|
||||
|
||||
The built-in calendar does not support leap seconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.new!(0, 0, 0, 0)
|
||||
~T[00:00:00.000000]
|
||||
iex> Time.new!(23, 59, 59, 999_999)
|
||||
~T[23:59:59.999999]
|
||||
iex> Time.new!(24, 59, 59, 999_999)
|
||||
** (ArgumentError) cannot build time, reason: :invalid_time
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def new!(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
|
||||
case new(hour, minute, second, microsecond, calendar) do
|
||||
{:ok, time} ->
|
||||
time
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, "cannot build time, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given `time` to a string.
|
||||
|
||||
### Examples
|
||||
|
||||
iex> Time.to_string(~T[23:00:00])
|
||||
"23:00:00"
|
||||
iex> Time.to_string(~T[23:00:00.001])
|
||||
"23:00:00.001"
|
||||
iex> Time.to_string(~T[23:00:00.123456])
|
||||
"23:00:00.123456"
|
||||
|
||||
iex> Time.to_string(~N[2015-01-01 23:00:00.001])
|
||||
"23:00:00.001"
|
||||
iex> Time.to_string(~N[2015-01-01 23:00:00.123456])
|
||||
"23:00:00.123456"
|
||||
|
||||
"""
|
||||
@spec to_string(Calendar.time()) :: String.t()
|
||||
def to_string(time)
|
||||
|
||||
def to_string(%{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
calendar: calendar
|
||||
}) do
|
||||
calendar.time_to_string(hour, minute, second, microsecond)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses the extended "Local time" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
Time zone offset may be included in the string but they will be
|
||||
simply discarded as such information is not included in times.
|
||||
|
||||
As specified in the standard, the separator "T" may be omitted if
|
||||
desired as there is no ambiguity within this function.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_iso8601("23:50:07")
|
||||
{:ok, ~T[23:50:07]}
|
||||
iex> Time.from_iso8601("23:50:07Z")
|
||||
{:ok, ~T[23:50:07]}
|
||||
iex> Time.from_iso8601("T23:50:07Z")
|
||||
{:ok, ~T[23:50:07]}
|
||||
|
||||
iex> Time.from_iso8601("23:50:07,0123456")
|
||||
{:ok, ~T[23:50:07.012345]}
|
||||
iex> Time.from_iso8601("23:50:07.0123456")
|
||||
{:ok, ~T[23:50:07.012345]}
|
||||
iex> Time.from_iso8601("23:50:07.123Z")
|
||||
{:ok, ~T[23:50:07.123]}
|
||||
|
||||
iex> Time.from_iso8601("2015:01:23 23-50-07")
|
||||
{:error, :invalid_format}
|
||||
iex> Time.from_iso8601("23:50:07A")
|
||||
{:error, :invalid_format}
|
||||
iex> Time.from_iso8601("23:50:07.")
|
||||
{:error, :invalid_format}
|
||||
iex> Time.from_iso8601("23:50:61")
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@spec from_iso8601(String.t(), Calendar.calendar()) :: {:ok, t} | {:error, atom}
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO) do
|
||||
with {:ok, {hour, minute, second, microsecond}} <- Calendar.ISO.parse_time(string) do
|
||||
convert(
|
||||
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond},
|
||||
calendar
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses the extended "Local time" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
Raises if the format is invalid.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_iso8601!("23:50:07,123Z")
|
||||
~T[23:50:07.123]
|
||||
iex> Time.from_iso8601!("23:50:07.123Z")
|
||||
~T[23:50:07.123]
|
||||
iex> Time.from_iso8601!("2015:01:23 23-50-07")
|
||||
** (ArgumentError) cannot parse "2015:01:23 23-50-07" as time, reason: :invalid_format
|
||||
|
||||
"""
|
||||
@spec from_iso8601!(String.t(), Calendar.calendar()) :: t
|
||||
def from_iso8601!(string, calendar \\ Calendar.ISO) do
|
||||
case from_iso8601(string, calendar) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, "cannot parse #{inspect(string)} as time, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given time to
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
By default, `Time.to_iso8601/2` returns times formatted in the "extended"
|
||||
format, for human readability. It also supports the "basic" format through
|
||||
passing the `:basic` option.
|
||||
|
||||
### Examples
|
||||
|
||||
iex> Time.to_iso8601(~T[23:00:13])
|
||||
"23:00:13"
|
||||
|
||||
iex> Time.to_iso8601(~T[23:00:13.001])
|
||||
"23:00:13.001"
|
||||
|
||||
iex> Time.to_iso8601(~T[23:00:13.001], :basic)
|
||||
"230013.001"
|
||||
|
||||
iex> Time.to_iso8601(~N[2010-04-17 23:00:13])
|
||||
"23:00:13"
|
||||
|
||||
"""
|
||||
@spec to_iso8601(Calendar.time(), :extended | :basic) :: String.t()
|
||||
def to_iso8601(time, format \\ :extended)
|
||||
|
||||
def to_iso8601(%{calendar: Calendar.ISO} = time, format) when format in [:extended, :basic] do
|
||||
%{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
} = time
|
||||
|
||||
Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
|
||||
end
|
||||
|
||||
def to_iso8601(%{calendar: _} = time, format) when format in [:extended, :basic] do
|
||||
time
|
||||
|> convert!(Calendar.ISO)
|
||||
|> to_iso8601(format)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts given `time` to an Erlang time tuple.
|
||||
|
||||
WARNING: Loss of precision may occur, as Erlang time tuples
|
||||
only contain hours/minutes/seconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.to_erl(~T[23:30:15.999])
|
||||
{23, 30, 15}
|
||||
|
||||
iex> Time.to_erl(~N[2010-04-17 23:30:15.999])
|
||||
{23, 30, 15}
|
||||
|
||||
"""
|
||||
@spec to_erl(Calendar.time()) :: :calendar.time()
|
||||
def to_erl(time) do
|
||||
%{hour: hour, minute: minute, second: second} = convert!(time, Calendar.ISO)
|
||||
{hour, minute, second}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an Erlang time tuple to a `Time` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_erl({23, 30, 15}, {5000, 3})
|
||||
{:ok, ~T[23:30:15.005]}
|
||||
iex> Time.from_erl({24, 30, 15})
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
|
||||
{:ok, t} | {:error, atom}
|
||||
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
|
||||
def from_erl({hour, minute, second}, microsecond, calendar) do
|
||||
with {:ok, time} <- new(hour, minute, second, microsecond, Calendar.ISO),
|
||||
do: convert(time, calendar)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an Erlang time tuple to a `Time` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_erl!({23, 30, 15})
|
||||
~T[23:30:15]
|
||||
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
|
||||
~T[23:30:15.005]
|
||||
iex> Time.from_erl!({24, 30, 15})
|
||||
** (ArgumentError) cannot convert {24, 30, 15} to time, reason: :invalid_time
|
||||
|
||||
"""
|
||||
@spec from_erl!(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) :: t
|
||||
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
|
||||
case from_erl(tuple, microsecond, calendar) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot convert #{inspect(tuple)} to time, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of seconds after midnight to a `Time` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_seconds_after_midnight(10_000)
|
||||
~T[02:46:40]
|
||||
iex> Time.from_seconds_after_midnight(30_000, {5000, 3})
|
||||
~T[08:20:00.005]
|
||||
iex> Time.from_seconds_after_midnight(-1)
|
||||
~T[23:59:59]
|
||||
iex> Time.from_seconds_after_midnight(100_000)
|
||||
~T[03:46:40]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec from_seconds_after_midnight(
|
||||
integer(),
|
||||
Calendar.microsecond(),
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
when is_integer(seconds) do
|
||||
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
|
||||
|
||||
{hour, minute, second, {_, _}} =
|
||||
calendar.time_from_day_fraction({seconds_in_day, @seconds_per_day})
|
||||
|
||||
%Time{
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `Time` struct to a number of seconds after midnight.
|
||||
|
||||
The returned value is a two-element tuple with the number of seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.to_seconds_after_midnight(~T[23:30:15])
|
||||
{84615, 0}
|
||||
iex> Time.to_seconds_after_midnight(~N[2010-04-17 23:30:15.999])
|
||||
{84615, 999000}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
|
||||
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
|
||||
iso_days = {0, to_day_fraction(time)}
|
||||
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds the `amount_to_add` of `unit`s to the given `time`.
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function always consider the unit to be computed according
|
||||
to the `Calendar.ISO`.
|
||||
|
||||
Note the result value represents the time of day, meaning that it is cyclic,
|
||||
for instance, it will never go over 24 hours for the ISO calendar.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.add(~T[10:00:00], 27000)
|
||||
~T[17:30:00]
|
||||
iex> Time.add(~T[11:00:00.005], 2400)
|
||||
~T[11:40:00.005]
|
||||
iex> Time.add(~T[00:00:00.000], 86_399_999, :millisecond)
|
||||
~T[23:59:59.999]
|
||||
|
||||
Negative values are allowed:
|
||||
|
||||
iex> Time.add(~T[23:00:00], -60)
|
||||
~T[22:59:00]
|
||||
|
||||
Note that the time is cyclic:
|
||||
|
||||
iex> Time.add(~T[17:10:05], 86400)
|
||||
~T[17:10:05]
|
||||
|
||||
Hours and minutes are also supported:
|
||||
|
||||
iex> Time.add(~T[17:10:05], 2, :hour)
|
||||
~T[19:10:05]
|
||||
iex> Time.add(~T[17:10:05], 30, :minute)
|
||||
~T[17:40:05]
|
||||
|
||||
This operation merges the precision of the time with the given unit:
|
||||
|
||||
iex> result = Time.add(~T[00:29:10], 21, :millisecond)
|
||||
~T[00:29:10.021]
|
||||
iex> result.microsecond
|
||||
{21000, 3}
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
|
||||
def add(time, amount_to_add, unit \\ :second)
|
||||
|
||||
def add(time, amount_to_add, :hour) when is_integer(amount_to_add) do
|
||||
add(time, amount_to_add * 3600, :second)
|
||||
end
|
||||
|
||||
def add(time, amount_to_add, :minute) when is_integer(amount_to_add) do
|
||||
add(time, amount_to_add * 60, :second)
|
||||
end
|
||||
|
||||
def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit)
|
||||
when is_integer(amount_to_add) do
|
||||
amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond)
|
||||
total = time_to_microseconds(time) + amount_to_add
|
||||
parts = Integer.mod(total, @parts_per_day)
|
||||
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
|
||||
|
||||
{hour, minute, second, {microsecond, _}} =
|
||||
calendar.time_from_day_fraction({parts, @parts_per_day})
|
||||
|
||||
%Time{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision},
|
||||
calendar: calendar
|
||||
}
|
||||
end
|
||||
|
||||
defp time_to_microseconds(%{
|
||||
calendar: Calendar.ISO,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
microsecond: {0, _}
|
||||
}) do
|
||||
0
|
||||
end
|
||||
|
||||
defp time_to_microseconds(time) do
|
||||
iso_days = {0, to_day_fraction(time)}
|
||||
Calendar.ISO.iso_days_to_unit(iso_days, :microsecond)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compares two time structs.
|
||||
|
||||
Returns `:gt` if first time is later than the second
|
||||
and `:lt` for vice versa. If the two times are equal
|
||||
`:eq` is returned.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.compare(~T[16:04:16], ~T[16:04:28])
|
||||
:lt
|
||||
iex> Time.compare(~T[16:04:16], ~T[16:04:16])
|
||||
:eq
|
||||
iex> Time.compare(~T[16:04:16.01], ~T[16:04:16.001])
|
||||
:gt
|
||||
|
||||
This function can also be used to compare across more
|
||||
complex calendar types by considering only the time fields:
|
||||
|
||||
iex> Time.compare(~N[1900-01-01 16:04:16], ~N[2015-01-01 16:04:16])
|
||||
:eq
|
||||
iex> Time.compare(~N[2015-01-01 16:04:16], ~N[2015-01-01 16:04:28])
|
||||
:lt
|
||||
iex> Time.compare(~N[2015-01-01 16:04:16.01], ~N[2000-01-01 16:04:16.001])
|
||||
:gt
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
|
||||
def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
|
||||
%{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
|
||||
%{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
|
||||
|
||||
case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
_ -> :eq
|
||||
end
|
||||
end
|
||||
|
||||
def compare(time1, time2) do
|
||||
{parts1, ppd1} = to_day_fraction(time1)
|
||||
{parts2, ppd2} = to_day_fraction(time2)
|
||||
|
||||
case {parts1 * ppd2, parts2 * ppd1} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
_ -> :eq
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts given `time` to a different calendar.
|
||||
|
||||
Returns `{:ok, time}` if the conversion was successful,
|
||||
or `{:error, reason}` if it was not, for some reason.
|
||||
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
|
||||
{:ok, %Time{calendar: Calendar.Holocene, hour: 13, minute: 30, second: 15, microsecond: {0, 0}}}
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec convert(Calendar.time(), Calendar.calendar()) :: {:ok, t} | {:error, atom}
|
||||
|
||||
# Keep it multiline for proper function clause errors.
|
||||
def convert(
|
||||
%{
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
},
|
||||
calendar
|
||||
) do
|
||||
time = %Time{
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}
|
||||
|
||||
{:ok, time}
|
||||
end
|
||||
|
||||
def convert(%{microsecond: {_, precision}} = time, calendar) do
|
||||
{hour, minute, second, {microsecond, _}} =
|
||||
time
|
||||
|> to_day_fraction()
|
||||
|> calendar.time_from_day_fraction()
|
||||
|
||||
time = %Time{
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
}
|
||||
|
||||
{:ok, time}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Similar to `Time.convert/2`, but raises an `ArgumentError`
|
||||
if the conversion between the two calendars is not possible.
|
||||
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
|
||||
%Time{calendar: Calendar.Holocene, hour: 13, minute: 30, second: 15, microsecond: {0, 0}}
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
|
||||
def convert!(time, calendar) do
|
||||
case convert(time, calendar) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
|
||||
"reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the difference between two times, considering only the hour, minute,
|
||||
second and microsecond.
|
||||
|
||||
As with the `compare/2` function both `Time` structs and other structures
|
||||
containing time can be used. If for instance a `NaiveDateTime` or `DateTime`
|
||||
is passed, only the hour, minute, second, and microsecond is considered. Any
|
||||
additional information about a date or time zone is ignored when calculating
|
||||
the difference.
|
||||
|
||||
The answer can be returned in any `:hour`, `:minute`, `:second` or any
|
||||
subsecond `unit` available from `t:System.time_unit/0`. If the first time
|
||||
value is earlier than the second, a negative number is returned.
|
||||
|
||||
The unit is measured according to `Calendar.ISO` and defaults to `:second`.
|
||||
Fractional results are not supported and are truncated.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.diff(~T[00:29:12], ~T[00:29:10])
|
||||
2
|
||||
|
||||
# When passing a `NaiveDateTime` the date part is ignored.
|
||||
iex> Time.diff(~N[2017-01-01 00:29:12], ~T[00:29:10])
|
||||
2
|
||||
|
||||
# Two `NaiveDateTime` structs could have big differences in the date
|
||||
# but only the time part is considered.
|
||||
iex> Time.diff(~N[2017-01-01 00:29:12], ~N[1900-02-03 00:29:10])
|
||||
2
|
||||
|
||||
iex> Time.diff(~T[00:29:12], ~T[00:29:10], :microsecond)
|
||||
2_000_000
|
||||
iex> Time.diff(~T[00:29:10], ~T[00:29:12], :microsecond)
|
||||
-2_000_000
|
||||
|
||||
iex> Time.diff(~T[02:29:10], ~T[00:29:10], :hour)
|
||||
2
|
||||
iex> Time.diff(~T[02:29:10], ~T[00:29:11], :hour)
|
||||
1
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec diff(Calendar.time(), Calendar.time(), :hour | :minute | System.time_unit()) :: integer
|
||||
def diff(time1, time2, unit \\ :second)
|
||||
|
||||
def diff(time1, time2, :hour) do
|
||||
diff(time1, time2, :second) |> div(3600)
|
||||
end
|
||||
|
||||
def diff(time1, time2, :minute) do
|
||||
diff(time1, time2, :second) |> div(60)
|
||||
end
|
||||
|
||||
def diff(
|
||||
%{
|
||||
calendar: Calendar.ISO,
|
||||
hour: hour1,
|
||||
minute: minute1,
|
||||
second: second1,
|
||||
microsecond: {microsecond1, _}
|
||||
},
|
||||
%{
|
||||
calendar: Calendar.ISO,
|
||||
hour: hour2,
|
||||
minute: minute2,
|
||||
second: second2,
|
||||
microsecond: {microsecond2, _}
|
||||
},
|
||||
unit
|
||||
) do
|
||||
total =
|
||||
(hour1 - hour2) * 3_600_000_000 + (minute1 - minute2) * 60_000_000 +
|
||||
(second1 - second2) * 1_000_000 + (microsecond1 - microsecond2)
|
||||
|
||||
System.convert_time_unit(total, :microsecond, unit)
|
||||
end
|
||||
|
||||
def diff(time1, time2, unit) do
|
||||
fraction1 = to_day_fraction(time1)
|
||||
fraction2 = to_day_fraction(time2)
|
||||
|
||||
Calendar.ISO.iso_days_to_unit({0, fraction1}, unit) -
|
||||
Calendar.ISO.iso_days_to_unit({0, fraction2}, unit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the given time with the microsecond field truncated to the given
|
||||
precision (`:microsecond`, `millisecond` or `:second`).
|
||||
|
||||
The given time is returned unchanged if it already has lower precision than
|
||||
the given precision.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.truncate(~T[01:01:01.123456], :microsecond)
|
||||
~T[01:01:01.123456]
|
||||
|
||||
iex> Time.truncate(~T[01:01:01.123456], :millisecond)
|
||||
~T[01:01:01.123]
|
||||
|
||||
iex> Time.truncate(~T[01:01:01.123456], :second)
|
||||
~T[01:01:01]
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec truncate(t(), :microsecond | :millisecond | :second) :: t()
|
||||
def truncate(%Time{microsecond: microsecond} = time, precision) do
|
||||
%{time | microsecond: Calendar.truncate(microsecond, precision)}
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp to_day_fraction(%{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {_, _} = microsecond,
|
||||
calendar: calendar
|
||||
}) do
|
||||
calendar.time_to_day_fraction(hour, minute, second, microsecond)
|
||||
end
|
||||
|
||||
defimpl String.Chars do
|
||||
def to_string(time) do
|
||||
%{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
calendar: calendar
|
||||
} = time
|
||||
|
||||
calendar.time_to_string(hour, minute, second, microsecond)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
def inspect(time, _) do
|
||||
%{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
calendar: calendar
|
||||
} = time
|
||||
|
||||
"~T[" <>
|
||||
calendar.time_to_string(hour, minute, second, microsecond) <> suffix(calendar) <> "]"
|
||||
end
|
||||
|
||||
defp suffix(Calendar.ISO), do: ""
|
||||
defp suffix(calendar), do: " " <> inspect(calendar)
|
||||
end
|
||||
end
|
||||
@@ -1,96 +0,0 @@
|
||||
defmodule Calendar.TimeZoneDatabase do
|
||||
@moduledoc """
|
||||
This module defines a behaviour for providing time zone data.
|
||||
|
||||
IANA provides time zone data that includes data about different
|
||||
UTC offsets and standard offsets for time zones.
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
A period where a certain combination of UTC offset, standard offset and zone
|
||||
abbreviation is in effect.
|
||||
|
||||
For instance one period could be the summer of 2018 in "Europe/London" where summer time /
|
||||
daylight saving time is in effect and lasts from spring to autumn. At autumn the `std_offset`
|
||||
changes along with the `zone_abbr` so a different period is needed during winter.
|
||||
"""
|
||||
@type time_zone_period :: %{
|
||||
optional(any) => any,
|
||||
utc_offset: Calendar.utc_offset(),
|
||||
std_offset: Calendar.std_offset(),
|
||||
zone_abbr: Calendar.zone_abbr()
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
Limit for when a certain time zone period begins or ends.
|
||||
|
||||
A beginning is inclusive. An ending is exclusive. Eg. if a period is from
|
||||
2015-03-29 01:00:00 and until 2015-10-25 01:00:00, the period includes and
|
||||
begins from the beginning of 2015-03-29 01:00:00 and lasts until just before
|
||||
2015-10-25 01:00:00.
|
||||
|
||||
A beginning or end for certain periods are infinite. For instance the latest
|
||||
period for time zones without DST or plans to change. However for the purpose
|
||||
of this behaviour they are only used for gaps in wall time where the needed
|
||||
period limits are at a certain time.
|
||||
"""
|
||||
@type time_zone_period_limit :: Calendar.naive_datetime()
|
||||
|
||||
@doc """
|
||||
Time zone period for a point in time in UTC for a specific time zone.
|
||||
|
||||
Takes a time zone name and a point in time for UTC and returns a
|
||||
`time_zone_period` for that point in time.
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@callback time_zone_period_from_utc_iso_days(Calendar.iso_days(), Calendar.time_zone()) ::
|
||||
{:ok, time_zone_period}
|
||||
| {:error, :time_zone_not_found | :utc_only_time_zone_database}
|
||||
|
||||
@doc """
|
||||
Possible time zone periods for a certain time zone and wall clock date and time.
|
||||
|
||||
When the provided `datetime` is ambiguous a tuple with `:ambiguous` and two possible
|
||||
periods. The periods in the list are sorted with the first element being the one that begins first.
|
||||
|
||||
When the provided `datetime` is in a gap - for instance during the "spring forward" when going
|
||||
from winter time to summer time, a tuple with `:gap` and two periods with limits are returned
|
||||
in a nested tuple. The first nested two-tuple is the period before the gap and a naive datetime
|
||||
with a limit for when the period ends (wall time). The second nested two-tuple is the period
|
||||
just after the gap and a datetime (wall time) for when the period begins just after the gap.
|
||||
|
||||
If there is only a single possible period for the provided `datetime`, then a tuple with `:ok`
|
||||
and the `time_zone_period` is returned.
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@callback time_zone_periods_from_wall_datetime(Calendar.naive_datetime(), Calendar.time_zone()) ::
|
||||
{:ok, time_zone_period}
|
||||
| {:ambiguous, time_zone_period, time_zone_period}
|
||||
| {:gap, {time_zone_period, time_zone_period_limit},
|
||||
{time_zone_period, time_zone_period_limit}}
|
||||
| {:error, :time_zone_not_found | :utc_only_time_zone_database}
|
||||
end
|
||||
|
||||
defmodule Calendar.UTCOnlyTimeZoneDatabase do
|
||||
@moduledoc """
|
||||
Built-in time zone database that works only in Etc/UTC.
|
||||
|
||||
For all other time zones, it returns `{:error, :utc_only_time_zone_database}`.
|
||||
"""
|
||||
|
||||
@behaviour Calendar.TimeZoneDatabase
|
||||
|
||||
@impl true
|
||||
def time_zone_period_from_utc_iso_days(_, "Etc/UTC"),
|
||||
do: {:ok, %{std_offset: 0, utc_offset: 0, zone_abbr: "UTC"}}
|
||||
|
||||
def time_zone_period_from_utc_iso_days(_, _),
|
||||
do: {:error, :utc_only_time_zone_database}
|
||||
|
||||
@impl true
|
||||
def time_zone_periods_from_wall_datetime(_, "Etc/UTC"),
|
||||
do: {:ok, %{std_offset: 0, utc_offset: 0, zone_abbr: "UTC"}}
|
||||
|
||||
def time_zone_periods_from_wall_datetime(_, _),
|
||||
do: {:error, :utc_only_time_zone_database}
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,152 +0,0 @@
|
||||
defmodule Code.Identifier do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Checks if the given identifier is an unary op.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Code.Identifier.unary_op(:+)
|
||||
{:non_associative, 300}
|
||||
|
||||
"""
|
||||
@spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
|
||||
def unary_op(op) do
|
||||
cond do
|
||||
op in [:&] -> {:non_associative, 90}
|
||||
op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300}
|
||||
op in [:@] -> {:non_associative, 320}
|
||||
true -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the given identifier is a binary op.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Code.Identifier.binary_op(:+)
|
||||
{:left, 210}
|
||||
|
||||
"""
|
||||
@spec binary_op(atom) :: {:left | :right, precedence :: pos_integer} | :error
|
||||
def binary_op(op) do
|
||||
cond do
|
||||
op in [:<-, :\\] -> {:left, 40}
|
||||
op in [:when] -> {:right, 50}
|
||||
op in [:"::"] -> {:right, 60}
|
||||
op in [:|] -> {:right, 70}
|
||||
op in [:=] -> {:right, 100}
|
||||
op in [:||, :|||, :or] -> {:left, 120}
|
||||
op in [:&&, :&&&, :and] -> {:left, 130}
|
||||
op in [:==, :!=, :=~, :===, :!==] -> {:left, 140}
|
||||
op in [:<, :<=, :>=, :>] -> {:left, 150}
|
||||
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160}
|
||||
op in [:in] -> {:left, 170}
|
||||
op in [:"^^^"] -> {:left, 180}
|
||||
op in [:"//"] -> {:right, 190}
|
||||
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
|
||||
op in [:+, :-] -> {:left, 210}
|
||||
op in [:*, :/] -> {:left, 220}
|
||||
op in [:**] -> {:left, 230}
|
||||
op in [:.] -> {:left, 310}
|
||||
true -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts the name and arity of the parent from the anonymous function identifier.
|
||||
"""
|
||||
# Example of this format: -NAME/ARITY-fun-COUNT-
|
||||
def extract_anonymous_fun_parent(atom) when is_atom(atom) do
|
||||
with "-" <> rest <- Atom.to_string(atom),
|
||||
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
|
||||
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Escapes the given identifier.
|
||||
"""
|
||||
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
|
||||
{escaped :: iolist(), remaining :: binary()}
|
||||
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
|
||||
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
|
||||
(is_integer(limit) and limit >= 0) do
|
||||
escape(binary, char, limit, [], fun)
|
||||
end
|
||||
|
||||
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
|
||||
{acc, binary}
|
||||
end
|
||||
|
||||
defp escape(<<char, t::binary>>, char, count, acc, fun) do
|
||||
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
|
||||
end
|
||||
|
||||
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
|
||||
escape(t, char, decrement(count), [acc | '\\\#{'], fun)
|
||||
end
|
||||
|
||||
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
|
||||
escaped = if value = fun.(h), do: value, else: escape_char(h)
|
||||
escape(t, char, decrement(count), [acc | escaped], fun)
|
||||
end
|
||||
|
||||
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
|
||||
escape(t, char, decrement(count), [acc | ['\\x', to_hex(a), to_hex(b)]], fun)
|
||||
end
|
||||
|
||||
defp escape(<<>>, _char, _count, acc, _fun) do
|
||||
{acc, <<>>}
|
||||
end
|
||||
|
||||
defp escape_char(0), do: '\\0'
|
||||
|
||||
defp escape_char(65279), do: '\\uFEFF'
|
||||
|
||||
defp escape_char(char)
|
||||
when char in 0x20..0x7E
|
||||
when char in 0xA0..0xD7FF
|
||||
when char in 0xE000..0xFFFD
|
||||
when char in 0x10000..0x10FFFF do
|
||||
<<char::utf8>>
|
||||
end
|
||||
|
||||
defp escape_char(char) when char < 0x100 do
|
||||
<<a::4, b::4>> = <<char::8>>
|
||||
['\\x', to_hex(a), to_hex(b)]
|
||||
end
|
||||
|
||||
defp escape_char(char) when char < 0x10000 do
|
||||
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
||||
['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
|
||||
end
|
||||
|
||||
defp escape_char(char) when char < 0x1000000 do
|
||||
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
|
||||
['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
|
||||
end
|
||||
|
||||
defp escape_map(?\a), do: '\\a'
|
||||
defp escape_map(?\b), do: '\\b'
|
||||
defp escape_map(?\d), do: '\\d'
|
||||
defp escape_map(?\e), do: '\\e'
|
||||
defp escape_map(?\f), do: '\\f'
|
||||
defp escape_map(?\n), do: '\\n'
|
||||
defp escape_map(?\r), do: '\\r'
|
||||
defp escape_map(?\t), do: '\\t'
|
||||
defp escape_map(?\v), do: '\\v'
|
||||
defp escape_map(?\\), do: '\\\\'
|
||||
defp escape_map(_), do: false
|
||||
|
||||
@compile {:inline, to_hex: 1, decrement: 1}
|
||||
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 decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
end
|
||||
@@ -1,600 +0,0 @@
|
||||
defmodule Code.Normalizer do
|
||||
@moduledoc false
|
||||
|
||||
defguard is_literal(x)
|
||||
when is_integer(x) or
|
||||
is_float(x) or
|
||||
is_binary(x) or
|
||||
is_atom(x)
|
||||
|
||||
@doc """
|
||||
Wraps literals in the quoted expression to conform to the AST format expected
|
||||
by the formatter.
|
||||
"""
|
||||
def normalize(quoted, opts \\ []) do
|
||||
line = Keyword.get(opts, :line, nil)
|
||||
escape = Keyword.get(opts, :escape, true)
|
||||
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
|
||||
|
||||
state = %{
|
||||
escape: escape,
|
||||
parent_meta: [line: line],
|
||||
locals_without_parens: locals_without_parens ++ Code.Formatter.locals_without_parens()
|
||||
}
|
||||
|
||||
do_normalize(quoted, state)
|
||||
end
|
||||
|
||||
# Wrapped literals should receive the block meta
|
||||
defp do_normalize({:__block__, meta, [literal]}, state)
|
||||
when not is_tuple(literal) or tuple_size(literal) == 2 do
|
||||
normalize_literal(literal, meta, state)
|
||||
end
|
||||
|
||||
# Only normalize the first argument of an alias if it's not an atom
|
||||
defp do_normalize({:__aliases__, meta, [first | rest]}, state) when not is_atom(first) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
first = do_normalize(first, %{state | parent_meta: meta})
|
||||
{:__aliases__, meta, [first | rest]}
|
||||
end
|
||||
|
||||
defp do_normalize({:__aliases__, _, _} = quoted, _state) do
|
||||
quoted
|
||||
end
|
||||
|
||||
# Skip captured arguments like &1
|
||||
defp do_normalize({:&, meta, [term]}, state) when is_integer(term) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
{:&, meta, [term]}
|
||||
end
|
||||
|
||||
# Ranges
|
||||
defp do_normalize(left..right//step, state) do
|
||||
left = do_normalize(left, state)
|
||||
right = do_normalize(right, state)
|
||||
meta = meta_line(state)
|
||||
|
||||
if step == 1 do
|
||||
{:.., meta, [left, right]}
|
||||
else
|
||||
step = do_normalize(step, state)
|
||||
{:"..//", meta, [left, right, step]}
|
||||
end
|
||||
end
|
||||
|
||||
# Bit containers
|
||||
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
|
||||
normalize_bitstring(quoted, state)
|
||||
end
|
||||
|
||||
# Atoms with interpolations
|
||||
defp do_normalize(
|
||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta,
|
||||
[{:<<>>, _, parts} = string, :utf8]},
|
||||
state
|
||||
)
|
||||
when is_list(parts) do
|
||||
dot_meta = patch_meta_line(dot_meta, state.parent_meta)
|
||||
call_meta = patch_meta_line(call_meta, dot_meta)
|
||||
|
||||
utf8 =
|
||||
if parts == [] or binary_interpolated?(parts) do
|
||||
# a non-normalized :utf8 atom signals an atom interpolation
|
||||
:utf8
|
||||
else
|
||||
normalize_literal(:utf8, [], state)
|
||||
end
|
||||
|
||||
string =
|
||||
if state.escape do
|
||||
normalize_bitstring(string, state, true)
|
||||
else
|
||||
normalize_bitstring(string, state)
|
||||
end
|
||||
|
||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
|
||||
end
|
||||
|
||||
# Charlists with interpolations
|
||||
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
|
||||
if list_interpolated?(parts) do
|
||||
parts =
|
||||
Enum.map(parts, fn
|
||||
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args} ->
|
||||
args = normalize_args(args, state)
|
||||
|
||||
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args}
|
||||
|
||||
part when is_binary(part) ->
|
||||
if state.escape do
|
||||
maybe_escape_literal(part, state)
|
||||
else
|
||||
part
|
||||
end
|
||||
end)
|
||||
|
||||
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
|
||||
else
|
||||
normalize_call(quoted, state)
|
||||
end
|
||||
end
|
||||
|
||||
# Don't normalize the `Access` atom in access syntax
|
||||
defp do_normalize({:., meta, [Access, :get]}, state) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
{:., meta, [Access, :get]}
|
||||
end
|
||||
|
||||
# Only normalize the left side of the dot operator
|
||||
# The right hand side is an atom in the AST but it's not an atom literal, so
|
||||
# it should not be wrapped
|
||||
defp do_normalize({:., meta, [left, right]}, state) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
left = do_normalize(left, %{state | parent_meta: meta})
|
||||
|
||||
{:., meta, [left, right]}
|
||||
end
|
||||
|
||||
# A list of left to right arrows is not considered as a list literal, so it's not wrapped
|
||||
defp do_normalize([{:->, _, [_ | _]} | _] = quoted, state) do
|
||||
normalize_args(quoted, state)
|
||||
end
|
||||
|
||||
# left -> right
|
||||
defp do_normalize({:->, meta, [left, right]}, state) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
left = normalize_args(left, %{state | parent_meta: meta})
|
||||
right = do_normalize(right, %{state | parent_meta: meta})
|
||||
{:->, meta, [left, right]}
|
||||
end
|
||||
|
||||
# Maps
|
||||
defp do_normalize({:%{}, meta, args}, state) when is_list(args) do
|
||||
meta =
|
||||
if meta == [] do
|
||||
line = state.parent_meta[:line]
|
||||
[line: line, closing: [line: line]]
|
||||
else
|
||||
meta
|
||||
end
|
||||
|
||||
state = %{state | parent_meta: meta}
|
||||
|
||||
args =
|
||||
case args do
|
||||
[{:|, pipe_meta, [left, right]}] ->
|
||||
left = do_normalize(left, state)
|
||||
right = normalize_map_args(right, state)
|
||||
[{:|, pipe_meta, [left, right]}]
|
||||
|
||||
[{_, _, _} = call] ->
|
||||
[do_normalize(call, state)]
|
||||
|
||||
args ->
|
||||
normalize_map_args(args, state)
|
||||
end
|
||||
|
||||
{:%{}, meta, args}
|
||||
end
|
||||
|
||||
# Sigils
|
||||
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
|
||||
when is_list(args) and is_atom(sigil) do
|
||||
case Atom.to_string(sigil) do
|
||||
<<"sigil_", _name>> ->
|
||||
meta =
|
||||
meta
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|> Keyword.put_new(:delimiter, "\"")
|
||||
|
||||
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||
|
||||
_ ->
|
||||
normalize_call(quoted, state)
|
||||
end
|
||||
end
|
||||
|
||||
# Tuples
|
||||
defp do_normalize({:{}, meta, args} = quoted, state) when is_list(args) do
|
||||
{last_arg, args} = List.pop_at(args, -1)
|
||||
|
||||
if args != [] and match?([_ | _], last_arg) and keyword?(last_arg) do
|
||||
args = normalize_args(args, state)
|
||||
kw_list = normalize_kw_args(last_arg, state, true)
|
||||
{:{}, meta, args ++ kw_list}
|
||||
else
|
||||
normalize_call(quoted, state)
|
||||
end
|
||||
end
|
||||
|
||||
# Module attributes
|
||||
defp do_normalize({:@, meta, [{name, name_meta, [value]}]}, state) do
|
||||
value =
|
||||
cond do
|
||||
keyword?(value) and value != [] ->
|
||||
normalize_kw_args(value, state, true)
|
||||
|
||||
is_list(value) ->
|
||||
normalize_literal(value, meta, state)
|
||||
|
||||
true ->
|
||||
do_normalize(value, state)
|
||||
end
|
||||
|
||||
{:@, meta, [{name, name_meta, [value]}]}
|
||||
end
|
||||
|
||||
# Regular blocks
|
||||
defp do_normalize({:__block__, meta, args}, state) when is_list(args) do
|
||||
{:__block__, meta, normalize_args(args, state)}
|
||||
end
|
||||
|
||||
# Calls
|
||||
defp do_normalize({_, _, args} = quoted, state) when is_list(args) do
|
||||
normalize_call(quoted, state)
|
||||
end
|
||||
|
||||
# Vars
|
||||
defp do_normalize({_, _, context} = quoted, _state) when is_atom(context) do
|
||||
quoted
|
||||
end
|
||||
|
||||
# Literals
|
||||
defp do_normalize(quoted, state) do
|
||||
normalize_literal(quoted, [], state)
|
||||
end
|
||||
|
||||
# Numbers
|
||||
defp normalize_literal(number, meta, state) when is_number(number) do
|
||||
meta =
|
||||
meta
|
||||
|> Keyword.put_new(:token, inspect(number))
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|
||||
{:__block__, meta, [number]}
|
||||
end
|
||||
|
||||
# Atom, Strings
|
||||
defp normalize_literal(literal, meta, state) when is_atom(literal) or is_binary(literal) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
literal = maybe_escape_literal(literal, state)
|
||||
|
||||
if is_atom(literal) and Macro.classify_atom(literal) == :alias and
|
||||
is_nil(meta[:delimiter]) do
|
||||
segments =
|
||||
case Atom.to_string(literal) do
|
||||
"Elixir" ->
|
||||
[:"Elixir"]
|
||||
|
||||
"Elixir." <> segments ->
|
||||
segments
|
||||
|> String.split(".")
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
end
|
||||
|
||||
{:__aliases__, meta, segments}
|
||||
else
|
||||
{:__block__, meta, [literal]}
|
||||
end
|
||||
end
|
||||
|
||||
# 2-tuples
|
||||
defp normalize_literal({left, right}, meta, state) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
state = %{state | parent_meta: meta}
|
||||
|
||||
if match?([_ | _], right) and keyword?(right) do
|
||||
{:__block__, meta, [{do_normalize(left, state), normalize_kw_args(right, state, true)}]}
|
||||
else
|
||||
{:__block__, meta, [{do_normalize(left, state), do_normalize(right, state)}]}
|
||||
end
|
||||
end
|
||||
|
||||
# Lists
|
||||
defp normalize_literal(list, meta, state) when is_list(list) do
|
||||
if list != [] and List.ascii_printable?(list) do
|
||||
# It's a charlist
|
||||
list =
|
||||
if state.escape do
|
||||
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
|
||||
IO.iodata_to_binary(string) |> to_charlist()
|
||||
else
|
||||
list
|
||||
end
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> Keyword.put_new(:delimiter, "'")
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|
||||
{:__block__, meta, [list]}
|
||||
else
|
||||
meta =
|
||||
if line = state.parent_meta[:line] do
|
||||
meta
|
||||
|> Keyword.put_new(:closing, line: line)
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
else
|
||||
meta
|
||||
end
|
||||
|
||||
{:__block__, meta, [normalize_kw_args(list, state, false)]}
|
||||
end
|
||||
end
|
||||
|
||||
# Probably an invalid value, wrap it and send it upstream
|
||||
defp normalize_literal(quoted, meta, _state) do
|
||||
{:__block__, meta, [quoted]}
|
||||
end
|
||||
|
||||
defp normalize_call({form, meta, args}, state) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
arity = length(args)
|
||||
|
||||
# Only normalize the form if it's a qualified call
|
||||
form =
|
||||
if is_atom(form) do
|
||||
form
|
||||
else
|
||||
do_normalize(form, %{state | parent_meta: meta})
|
||||
end
|
||||
|
||||
meta =
|
||||
if is_nil(meta[:no_parens]) and is_nil(meta[:closing]) and is_nil(meta[:do]) and
|
||||
not Code.Formatter.local_without_parens?(form, arity, state.locals_without_parens) do
|
||||
[closing: [line: meta[:line]]] ++ meta
|
||||
else
|
||||
meta
|
||||
end
|
||||
|
||||
cond do
|
||||
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], List.last(args)) ->
|
||||
# def foo do :ok end
|
||||
# def foo, do: :ok
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
match?([{:do, _} | _], List.last(args)) ->
|
||||
# Non normalized kw blocks
|
||||
line = state.parent_meta[:line]
|
||||
meta = meta ++ [do: [line: line], end: [line: line]]
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
allow_keyword?(form, arity) ->
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{last_arg, leading_args} = List.pop_at(args, -1, [])
|
||||
|
||||
last_args =
|
||||
case last_arg do
|
||||
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
|
||||
if key_meta[:format] == :keyword do
|
||||
last_args
|
||||
else
|
||||
[last_arg]
|
||||
end
|
||||
|
||||
[] ->
|
||||
[]
|
||||
|
||||
_ ->
|
||||
[last_arg]
|
||||
end
|
||||
|
||||
{form, meta, leading_args ++ last_args}
|
||||
|
||||
true ->
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
end
|
||||
end
|
||||
|
||||
defp allow_keyword?(:when, 2), do: true
|
||||
defp allow_keyword?(:{}, _), do: false
|
||||
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
|
||||
|
||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
parts =
|
||||
if binary_interpolated?(parts) do
|
||||
normalize_interpolation_parts(parts, %{state | parent_meta: meta}, escape_interpolation)
|
||||
else
|
||||
state = %{state | parent_meta: meta}
|
||||
|
||||
Enum.map(parts, fn part ->
|
||||
with {:"::", meta, [left, _]} <- part,
|
||||
true <- meta[:inferred_bitstring_spec] do
|
||||
do_normalize(left, state)
|
||||
else
|
||||
_ -> do_normalize(part, state)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:<<>>, meta, parts}
|
||||
end
|
||||
|
||||
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
|
||||
Enum.map(parts, fn
|
||||
{:"::", interpolation_meta,
|
||||
[
|
||||
{{:., dot_meta, [Kernel, :to_string]}, middle_meta, [middle]},
|
||||
{:binary, binary_meta, context}
|
||||
]} ->
|
||||
middle = do_normalize(middle, %{state | parent_meta: dot_meta})
|
||||
|
||||
{:"::", interpolation_meta,
|
||||
[
|
||||
{{:., dot_meta, [Kernel, :to_string]}, middle_meta, [middle]},
|
||||
{:binary, binary_meta, context}
|
||||
]}
|
||||
|
||||
part ->
|
||||
if escape_interpolation do
|
||||
maybe_escape_literal(part, state)
|
||||
else
|
||||
part
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp normalize_map_args(args, state) do
|
||||
Enum.map(normalize_kw_args(args, state, false), fn
|
||||
{:__block__, _, [{_, _} = pair]} -> pair
|
||||
pair -> pair
|
||||
end)
|
||||
end
|
||||
|
||||
defp normalize_kw_blocks(form, meta, args, state) do
|
||||
{kw_blocks, leading_args} = List.pop_at(args, -1)
|
||||
|
||||
kw_blocks =
|
||||
Enum.map(kw_blocks, fn {tag, block} ->
|
||||
block = do_normalize(block, %{state | parent_meta: meta})
|
||||
|
||||
block =
|
||||
case block do
|
||||
{_, _, [[{:->, _, _} | _] = block]} -> block
|
||||
block -> block
|
||||
end
|
||||
|
||||
# Only wrap the tag if it isn't already wrapped
|
||||
tag =
|
||||
case tag do
|
||||
{:__block__, _, _} -> tag
|
||||
_ -> {:__block__, [line: meta[:line]], [tag]}
|
||||
end
|
||||
|
||||
{tag, block}
|
||||
end)
|
||||
|
||||
leading_args = normalize_args(leading_args, %{state | parent_meta: meta})
|
||||
{form, meta, leading_args ++ [kw_blocks]}
|
||||
end
|
||||
|
||||
defp normalize_kw_args(elems, state, keyword?)
|
||||
|
||||
defp normalize_kw_args(
|
||||
[{{:__block__, key_meta, [key]}, value} = first | rest] = current,
|
||||
state,
|
||||
keyword?
|
||||
)
|
||||
when is_atom(key) do
|
||||
keyword? = keyword? or keyword?(current)
|
||||
|
||||
first =
|
||||
if key_meta[:format] == :keyword and not keyword? do
|
||||
key_meta = Keyword.delete(key_meta, :format)
|
||||
line = key_meta[:line] || meta_line(state)
|
||||
{:__block__, [line: line], [{{:__block__, key_meta, [key]}, value}]}
|
||||
else
|
||||
first
|
||||
end
|
||||
|
||||
[first | normalize_kw_args(rest, state, keyword?)]
|
||||
end
|
||||
|
||||
defp normalize_kw_args([{left, right} | rest] = current, state, keyword?) do
|
||||
keyword? = keyword? or keyword?(current)
|
||||
|
||||
left =
|
||||
if keyword? do
|
||||
meta = [format: :keyword] ++ meta_line(state)
|
||||
{:__block__, meta, [maybe_escape_literal(left, state)]}
|
||||
else
|
||||
do_normalize(left, state)
|
||||
end
|
||||
|
||||
right = do_normalize(right, state)
|
||||
|
||||
pair =
|
||||
with {:__block__, meta, _} <- left,
|
||||
:keyword <- meta[:format] do
|
||||
{left, right}
|
||||
else
|
||||
_ -> {:__block__, meta_line(state), [{left, right}]}
|
||||
end
|
||||
|
||||
[pair | normalize_kw_args(rest, state, keyword?)]
|
||||
end
|
||||
|
||||
defp normalize_kw_args([first | rest], state, keyword?) do
|
||||
[do_normalize(first, state) | normalize_kw_args(rest, state, keyword?)]
|
||||
end
|
||||
|
||||
defp normalize_kw_args([], _state, _keyword?) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp normalize_args(args, state) do
|
||||
Enum.map(args, &do_normalize(&1, state))
|
||||
end
|
||||
|
||||
defp maybe_escape_literal(string, %{escape: true}) when is_binary(string) do
|
||||
{string, _} = Code.Identifier.escape(string, nil)
|
||||
IO.iodata_to_binary(string)
|
||||
end
|
||||
|
||||
defp maybe_escape_literal(atom, %{escape: true} = state) when is_atom(atom) do
|
||||
atom
|
||||
|> Atom.to_string()
|
||||
|> maybe_escape_literal(state)
|
||||
|> String.to_atom()
|
||||
end
|
||||
|
||||
defp maybe_escape_literal(term, _) do
|
||||
term
|
||||
end
|
||||
|
||||
defp binary_interpolated?(parts) do
|
||||
Enum.all?(parts, fn
|
||||
{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [_]}, {:binary, _, _}]} -> true
|
||||
binary when is_binary(binary) -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp list_interpolated?(parts) do
|
||||
Enum.all?(parts, fn
|
||||
{{:., _, [Kernel, :to_string]}, _, [_]} -> true
|
||||
binary when is_binary(binary) -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp patch_meta_line(meta, parent_meta) do
|
||||
with nil <- meta[:line],
|
||||
line when is_integer(line) <- parent_meta[:line] do
|
||||
[line: line] ++ meta
|
||||
else
|
||||
_ -> meta
|
||||
end
|
||||
end
|
||||
|
||||
defp meta_line(state) do
|
||||
if line = state.parent_meta[:line] do
|
||||
[line: line]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp keyword?([{{:__block__, key_meta, [key]}, _} | rest]) when is_atom(key) do
|
||||
if key_meta[:format] == :keyword do
|
||||
keyword?(rest)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
case Atom.to_charlist(key) do
|
||||
'Elixir.' ++ _ -> false
|
||||
_ -> keyword?(rest)
|
||||
end
|
||||
end
|
||||
|
||||
defp keyword?([]), do: true
|
||||
defp keyword?(_other), do: false
|
||||
end
|
||||
@@ -1,423 +0,0 @@
|
||||
defmodule Code.Typespec do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Converts a spec clause back to Elixir quoted expression.
|
||||
"""
|
||||
@spec spec_to_quoted(atom, tuple) :: {atom, keyword, [Macro.t()]}
|
||||
def spec_to_quoted(name, spec)
|
||||
|
||||
def spec_to_quoted(name, {:type, anno, :fun, [{:type, _, :product, args}, result]})
|
||||
when is_atom(name) do
|
||||
meta = meta(anno)
|
||||
body = {name, meta, Enum.map(args, &typespec_to_quoted/1)}
|
||||
|
||||
vars =
|
||||
for type_expr <- args ++ [result],
|
||||
var <- collect_vars(type_expr),
|
||||
uniq: true,
|
||||
do: {var, {:var, meta, nil}}
|
||||
|
||||
spec = {:"::", meta, [body, typespec_to_quoted(result)]}
|
||||
|
||||
if vars == [] do
|
||||
spec
|
||||
else
|
||||
{:when, meta, [spec, vars]}
|
||||
end
|
||||
end
|
||||
|
||||
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
|
||||
meta = meta(anno)
|
||||
{:"::", meta, [{name, meta, []}, quote(do: term)]}
|
||||
end
|
||||
|
||||
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
|
||||
meta = meta(anno)
|
||||
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
|
||||
|
||||
guards =
|
||||
for {:type, _, :constraint, [{:atom, _, :is_subtype}, [{:var, _, var}, type]]} <- constrs do
|
||||
{erl_to_ex_var(var), typespec_to_quoted(type)}
|
||||
end
|
||||
|
||||
ignore_vars = Keyword.keys(guards)
|
||||
|
||||
vars =
|
||||
for type_expr <- args ++ [result],
|
||||
var <- collect_vars(type_expr),
|
||||
var not in ignore_vars,
|
||||
uniq: true,
|
||||
do: {var, {:var, meta, nil}}
|
||||
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
|
||||
when_args = [
|
||||
{:"::", meta, [{name, meta, args}, typespec_to_quoted(result)]},
|
||||
guards ++ vars
|
||||
]
|
||||
|
||||
{:when, meta, when_args}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a type clause back to Elixir AST.
|
||||
"""
|
||||
def type_to_quoted(type)
|
||||
|
||||
def type_to_quoted({{:record, record}, fields, args}) when is_atom(record) do
|
||||
fields = for field <- fields, do: typespec_to_quoted(field)
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
type = {:{}, [], [record | fields]}
|
||||
quote(do: unquote(record)(unquote_splicing(args)) :: unquote(type))
|
||||
end
|
||||
|
||||
def type_to_quoted({name, type, args}) when is_atom(name) do
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
quote(do: unquote(name)(unquote_splicing(args)) :: unquote(typespec_to_quoted(type)))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all types available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is the type (`:typep`, `:type` and `:opaque`).
|
||||
|
||||
The module must have a corresponding BEAM file which can be
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
Abstract Format.
|
||||
"""
|
||||
@spec fetch_types(module | binary) :: {:ok, [tuple]} | :error
|
||||
def fetch_types(module) when is_atom(module) or is_binary(module) do
|
||||
case typespecs_abstract_code(module) do
|
||||
{:ok, abstract_code} ->
|
||||
exported_types = for {:attribute, _, :export_type, types} <- abstract_code, do: types
|
||||
exported_types = List.flatten(exported_types)
|
||||
|
||||
types =
|
||||
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
|
||||
kind in [:opaque, :type] do
|
||||
cond do
|
||||
kind == :opaque -> {:opaque, type}
|
||||
{name, length(args)} in exported_types -> {:type, type}
|
||||
true -> {:typep, type}
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, types}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all specs available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is spec name and arity and the second is the spec.
|
||||
|
||||
The module must have a corresponding BEAM file which can be
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
Abstract Format.
|
||||
"""
|
||||
@spec fetch_specs(module) :: {:ok, [tuple]} | :error
|
||||
def fetch_specs(module) when is_atom(module) or is_binary(module) do
|
||||
case typespecs_abstract_code(module) do
|
||||
{:ok, abstract_code} ->
|
||||
{:ok, for({:attribute, _, :spec, value} <- abstract_code, do: value)}
|
||||
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all callbacks available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is spec name and arity and the second is the spec.
|
||||
|
||||
The module must have a corresponding BEAM file
|
||||
which can be located by the runtime system. The types will be
|
||||
in the Erlang Abstract Format.
|
||||
"""
|
||||
@spec fetch_callbacks(module) :: {:ok, [tuple]} | :error
|
||||
def fetch_callbacks(module) when is_atom(module) or is_binary(module) do
|
||||
case typespecs_abstract_code(module) do
|
||||
{:ok, abstract_code} ->
|
||||
{:ok, for({:attribute, _, :callback, value} <- abstract_code, do: value)}
|
||||
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp typespecs_abstract_code(module) do
|
||||
with {module, binary} <- get_module_and_beam(module),
|
||||
{:ok, {_, [debug_info: {:debug_info_v1, backend, data}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info]) do
|
||||
case data do
|
||||
{:elixir_v1, %{}, specs} ->
|
||||
# Fast path to avoid translation to Erlang from Elixir.
|
||||
{:ok, specs}
|
||||
|
||||
_ ->
|
||||
case backend.debug_info(:erlang_v1, module, data, []) do
|
||||
{:ok, abstract_code} -> {:ok, abstract_code}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp get_module_and_beam(module) when is_atom(module) do
|
||||
with {^module, beam, _filename} <- :code.get_object_code(module),
|
||||
{:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do
|
||||
{module, beam}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp get_module_and_beam(beam) when is_binary(beam) do
|
||||
case :beam_lib.info(beam) do
|
||||
[_ | _] = info -> {info[:module], beam}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
## To AST conversion
|
||||
|
||||
defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
|
||||
Enum.flat_map(args, &collect_vars/1)
|
||||
end
|
||||
|
||||
defp collect_vars({:remote_type, _anno, args}) when is_list(args) do
|
||||
Enum.flat_map(args, &collect_vars/1)
|
||||
end
|
||||
|
||||
defp collect_vars({:typed_record_field, _anno, type}) do
|
||||
collect_vars(type)
|
||||
end
|
||||
|
||||
defp collect_vars({:paren_type, _anno, [type]}) do
|
||||
collect_vars(type)
|
||||
end
|
||||
|
||||
defp collect_vars({:var, _anno, var}) do
|
||||
[erl_to_ex_var(var)]
|
||||
end
|
||||
|
||||
defp collect_vars(_) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:user_type, anno, name, args}) do
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
{name, meta(anno), args}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :tuple, :any}) do
|
||||
{:tuple, meta(anno), []}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :tuple, args}) do
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
{:{}, meta(anno), args}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, _anno, :list, [{:type, _, :union, unions} = arg]}) do
|
||||
case unpack_typespec_kw(unions, []) do
|
||||
{:ok, ast} -> ast
|
||||
:error -> [typespec_to_quoted(arg)]
|
||||
end
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :list, []}) do
|
||||
{:list, meta(anno), []}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, _anno, :list, [arg]}) do
|
||||
[typespec_to_quoted(arg)]
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :nonempty_list, []}) do
|
||||
[{:..., meta(anno), nil}]
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :nonempty_list, [arg]}) do
|
||||
[typespec_to_quoted(arg), {:..., meta(anno), nil}]
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :map, :any}) do
|
||||
{:map, meta(anno), []}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :map, fields}) do
|
||||
fields =
|
||||
Enum.map(fields, fn
|
||||
{:type, _, :map_field_assoc, :any} ->
|
||||
{{:optional, [], [{:any, [], []}]}, {:any, [], []}}
|
||||
|
||||
{:type, _, :map_field_exact, [{:atom, _, k}, v]} ->
|
||||
{k, typespec_to_quoted(v)}
|
||||
|
||||
{:type, _, :map_field_exact, [k, v]} ->
|
||||
{{:required, [], [typespec_to_quoted(k)]}, typespec_to_quoted(v)}
|
||||
|
||||
{:type, _, :map_field_assoc, [k, v]} ->
|
||||
{{:optional, [], [typespec_to_quoted(k)]}, typespec_to_quoted(v)}
|
||||
end)
|
||||
|
||||
case List.keytake(fields, :__struct__, 0) do
|
||||
{{:__struct__, struct}, fields_pruned} when is_atom(struct) and struct != nil ->
|
||||
map_pruned = {:%{}, meta(anno), fields_pruned}
|
||||
{:%, meta(anno), [struct, map_pruned]}
|
||||
|
||||
_ ->
|
||||
{:%{}, meta(anno), fields}
|
||||
end
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
|
||||
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
|
||||
line = meta(anno)[:line]
|
||||
|
||||
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
|
||||
{arg1, 0} ->
|
||||
quote(line: line, do: <<_::unquote(arg1)>>)
|
||||
|
||||
{0, arg2} ->
|
||||
quote(line: line, do: <<_::_*unquote(arg2)>>)
|
||||
|
||||
{arg1, arg2} ->
|
||||
quote(line: line, do: <<_::unquote(arg1), _::_*unquote(arg2)>>)
|
||||
end
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :union, args}) do
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
Enum.reduce(Enum.reverse(args), fn arg, expr -> {:|, meta(anno), [arg, expr]} end)
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :fun, [{:type, _, :product, args}, result]}) do
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
[{:->, meta(anno), [args, typespec_to_quoted(result)]}]
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :fun, [args, result]}) do
|
||||
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :fun, []}) do
|
||||
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
|
||||
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, _anno, nil, []}) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, name, args}) do
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
{name, meta(anno), args}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:var, anno, var}) do
|
||||
{erl_to_ex_var(var), meta(anno), nil}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:op, anno, op, arg}) do
|
||||
{op, meta(anno), [typespec_to_quoted(arg)]}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
|
||||
remote_type(anno, mod, name, args)
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:ann_type, anno, [var, type]}) do
|
||||
{:"::", meta(anno), [typespec_to_quoted(var), typespec_to_quoted(type)]}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted(
|
||||
{:typed_record_field, {:record_field, anno1, {:atom, anno2, name}}, type}
|
||||
) do
|
||||
typespec_to_quoted({:ann_type, anno1, [{:var, anno2, name}, type]})
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, _, :any}) do
|
||||
quote(do: ...)
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:paren_type, _, [type]}) do
|
||||
typespec_to_quoted(type)
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({type, _anno, atom}) when is_atom(type) do
|
||||
atom
|
||||
end
|
||||
|
||||
defp typespec_to_quoted(other), do: other
|
||||
|
||||
## Helpers
|
||||
|
||||
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :charlist}, []) do
|
||||
typespec_to_quoted({:type, anno, :charlist, []})
|
||||
end
|
||||
|
||||
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :nonempty_charlist}, []) do
|
||||
typespec_to_quoted({:type, anno, :nonempty_charlist, []})
|
||||
end
|
||||
|
||||
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :struct}, []) do
|
||||
typespec_to_quoted({:type, anno, :struct, []})
|
||||
end
|
||||
|
||||
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :as_boolean}, [arg]) do
|
||||
typespec_to_quoted({:type, anno, :as_boolean, [arg]})
|
||||
end
|
||||
|
||||
defp remote_type(anno, {:atom, _, :elixir}, {:atom, _, :keyword}, args) do
|
||||
typespec_to_quoted({:type, anno, :keyword, args})
|
||||
end
|
||||
|
||||
defp remote_type(anno, mod, name, args) do
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
dot = {:., meta(anno), [typespec_to_quoted(mod), typespec_to_quoted(name)]}
|
||||
{dot, meta(anno), args}
|
||||
end
|
||||
|
||||
defp erl_to_ex_var(var) do
|
||||
case Atom.to_string(var) do
|
||||
<<"_", c::utf8, rest::binary>> ->
|
||||
String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
|
||||
<<c::utf8, rest::binary>> ->
|
||||
String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
end
|
||||
end
|
||||
|
||||
defp unpack_typespec_kw([{:type, _, :tuple, [{:atom, _, atom}, type]} | t], acc) do
|
||||
unpack_typespec_kw(t, [{atom, typespec_to_quoted(type)} | acc])
|
||||
end
|
||||
|
||||
defp unpack_typespec_kw([], acc) do
|
||||
{:ok, Enum.reverse(acc)}
|
||||
end
|
||||
|
||||
defp unpack_typespec_kw(_, _acc) do
|
||||
:error
|
||||
end
|
||||
|
||||
defp meta(anno), do: [line: :erl_anno.line(anno)]
|
||||
end
|
||||
@@ -1,179 +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
|
||||
map 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 only the range limits and step are stored.
|
||||
|
||||
The `Collectable` module was designed to fill the gap left by the
|
||||
`Enumerable` protocol. `Collectable.into/1` can be seen as the opposite of
|
||||
`Enumerable.reduce/3`. If the functions in `Enumerable` are about taking values out,
|
||||
then `Collectable.into/1` is about collecting those values into a structure.
|
||||
|
||||
## Examples
|
||||
|
||||
To show how to manually use the `Collectable` protocol, let's play with a
|
||||
simplified implementation for `MapSet`.
|
||||
|
||||
iex> {initial_acc, collector_fun} = Collectable.into(MapSet.new())
|
||||
iex> updated_acc = Enum.reduce([1, 2, 3], initial_acc, fn elem, acc ->
|
||||
...> collector_fun.(acc, {:cont, elem})
|
||||
...> end)
|
||||
iex> collector_fun.(updated_acc, :done)
|
||||
MapSet.new([1, 2, 3])
|
||||
|
||||
To show how the protocol can be implemented, we can again look at the
|
||||
simplified implementation for `MapSet`. In this implementation "collecting" elements
|
||||
simply means inserting them in the set through `MapSet.put/2`.
|
||||
|
||||
defimpl Collectable, for: MapSet do
|
||||
def into(map_set) do
|
||||
collector_fun = fn
|
||||
map_set_acc, {:cont, elem} ->
|
||||
MapSet.put(map_set_acc, elem)
|
||||
|
||||
map_set_acc, :done ->
|
||||
map_set_acc
|
||||
|
||||
_map_set_acc, :halt ->
|
||||
:ok
|
||||
end
|
||||
|
||||
initial_acc = map_set
|
||||
|
||||
{initial_acc, collector_fun}
|
||||
end
|
||||
end
|
||||
|
||||
So now we can call `Enum.into/2`:
|
||||
|
||||
iex> Enum.into([1, 2, 3], MapSet.new())
|
||||
MapSet.new([1, 2, 3])
|
||||
|
||||
"""
|
||||
|
||||
@type command :: {:cont, term} | :done | :halt
|
||||
|
||||
@doc """
|
||||
Returns an initial accumulator and a "collector" function.
|
||||
|
||||
Receives a `collectable` which can be used as the initial accumulator that will
|
||||
be passed to the function.
|
||||
|
||||
The collector function receives a term and a command and injects the term into
|
||||
the collectable accumulator on every `{:cont, term}` command.
|
||||
|
||||
`:done` is passed as a command when no further values will be injected. This
|
||||
is useful when there's a need to close resources or normalizing values. A
|
||||
collectable must be returned when the command is `:done`.
|
||||
|
||||
If injection is suddenly interrupted, `:halt` is passed and the function
|
||||
can return any value as it won't be used.
|
||||
|
||||
For examples on how to use the `Collectable` protocol and `into/1` see the
|
||||
module documentation.
|
||||
"""
|
||||
@spec into(t) :: {initial_acc :: term, collector :: (term, command -> t | term)}
|
||||
def into(collectable)
|
||||
end
|
||||
|
||||
defimpl Collectable, for: List do
|
||||
def into(list) do
|
||||
# TODO: Change the behaviour so the into always comes last on Elixir v2.0
|
||||
if list != [] do
|
||||
IO.warn(
|
||||
"the Collectable protocol is deprecated for non-empty lists. The behaviour of " <>
|
||||
"Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <>
|
||||
"when collecting into non-empty lists. If you're collecting into a non-empty keyword " <>
|
||||
"list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <>
|
||||
"list, consider concatenating the two lists with the ++ operator."
|
||||
)
|
||||
end
|
||||
|
||||
fun = fn
|
||||
list_acc, {:cont, elem} ->
|
||||
[elem | list_acc]
|
||||
|
||||
list_acc, :done ->
|
||||
list ++ :lists.reverse(list_acc)
|
||||
|
||||
_list_acc, :halt ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{[], fun}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: BitString do
|
||||
def into(binary) when is_binary(binary) do
|
||||
fun = fn
|
||||
acc, {:cont, x} when is_binary(x) and is_list(acc) ->
|
||||
[acc | x]
|
||||
|
||||
acc, {:cont, x} when is_bitstring(x) and is_bitstring(acc) ->
|
||||
<<acc::bitstring, x::bitstring>>
|
||||
|
||||
acc, {:cont, x} when is_bitstring(x) ->
|
||||
<<IO.iodata_to_binary(acc)::bitstring, x::bitstring>>
|
||||
|
||||
acc, :done when is_bitstring(acc) ->
|
||||
acc
|
||||
|
||||
acc, :done ->
|
||||
IO.iodata_to_binary(acc)
|
||||
|
||||
__acc, :halt ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{[binary], fun}
|
||||
end
|
||||
|
||||
def into(bitstring) do
|
||||
fun = fn
|
||||
acc, {:cont, x} when is_bitstring(x) ->
|
||||
<<acc::bitstring, x::bitstring>>
|
||||
|
||||
acc, :done ->
|
||||
acc
|
||||
|
||||
_acc, :halt ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{bitstring, fun}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: Map do
|
||||
def into(map) do
|
||||
fun = fn
|
||||
map_acc, {:cont, {key, value}} ->
|
||||
Map.put(map_acc, key, value)
|
||||
|
||||
map_acc, :done ->
|
||||
map_acc
|
||||
|
||||
_map_acc, :halt ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{map, fun}
|
||||
end
|
||||
end
|
||||
@@ -1,359 +0,0 @@
|
||||
defmodule Config do
|
||||
@moduledoc ~S"""
|
||||
A simple keyword-based configuration API.
|
||||
|
||||
## Example
|
||||
|
||||
This module is most commonly used to define application configuration,
|
||||
typically in `config/config.exs`:
|
||||
|
||||
import Config
|
||||
|
||||
config :some_app,
|
||||
key1: "value1",
|
||||
key2: "value2"
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
|
||||
`import Config` will import the functions `config/2`, `config/3`
|
||||
`config_env/0`, `config_target/0`, and `import_config/1`
|
||||
to help you manage your configuration.
|
||||
|
||||
`config/2` and `config/3` are used to define key-value configuration
|
||||
for a given application. Once Mix starts, it will automatically
|
||||
evaluate the configuration file and persist the configuration above
|
||||
into `:some_app`'s application environment, which can be accessed in
|
||||
as follows:
|
||||
|
||||
"value1" = Application.fetch_env!(:some_app, :key1)
|
||||
|
||||
Finally, the line `import_config "#{config_env()}.exs"` will import
|
||||
other config files based on the current configuration environment,
|
||||
such as `config/dev.exs` and `config/test.exs`.
|
||||
|
||||
`Config` also provides a low-level API for evaluating and reading
|
||||
configuration, under the `Config.Reader` module.
|
||||
|
||||
> **Important:** if you are writing a library to be used by other developers,
|
||||
> it is generally recommended to avoid the application environment, as the
|
||||
> application environment is effectively a global storage. Also note that
|
||||
> the `config/config.exs` of a library is not evaluated when the library is
|
||||
> used as a dependency, as configuration is always meant to configure the
|
||||
> current project. For more information, read our [library guidelines](library-guidelines.md).
|
||||
|
||||
## Migrating from `use Mix.Config`
|
||||
|
||||
The `Config` module in Elixir was introduced in v1.9 as a replacement to
|
||||
`Mix.Config`, which was specific to Mix and has been deprecated.
|
||||
|
||||
You can leverage `Config` instead of `Mix.Config` in three steps. The first
|
||||
step is to replace `use Mix.Config` at the top of your config files by
|
||||
`import Config`.
|
||||
|
||||
The second is to make sure your `import_config/1` calls do not have a
|
||||
wildcard character. If so, you need to perform the wildcard lookup
|
||||
manually. For example, if you did:
|
||||
|
||||
import_config "../apps/*/config/config.exs"
|
||||
|
||||
It has to be replaced by:
|
||||
|
||||
for config <- "../apps/*/config/config.exs" |> Path.expand(__DIR__) |> Path.wildcard() do
|
||||
import_config config
|
||||
end
|
||||
|
||||
The last step is to replace all `Mix.env()` calls in the config files with `config_env()`.
|
||||
|
||||
Keep in mind you must also avoid using `Mix.env()` inside your project files.
|
||||
To check the environment at _runtime_, you may add a configuration key:
|
||||
|
||||
# config.exs
|
||||
...
|
||||
config :my_app, env: config_env()
|
||||
|
||||
Then, in other scripts and modules, you may get the environment with
|
||||
`Application.fetch_env!/2`:
|
||||
|
||||
# router.exs
|
||||
...
|
||||
if Application.fetch_env!(:my_app, :env) == :prod do
|
||||
...
|
||||
end
|
||||
|
||||
The only files where you may access functions from the `Mix` module are
|
||||
the `mix.exs` file and inside custom Mix tasks, which always within the
|
||||
`Mix.Tasks` namespace.
|
||||
|
||||
## config/runtime.exs
|
||||
|
||||
For runtime configuration, you can use the `config/runtime.exs` file.
|
||||
It is executed right before applications start in both Mix and releases
|
||||
(assembled with `mix release`).
|
||||
"""
|
||||
|
||||
@opts_key {__MODULE__, :opts}
|
||||
@config_key {__MODULE__, :config}
|
||||
@imports_key {__MODULE__, :imports}
|
||||
|
||||
defp get_opts!(), do: Process.get(@opts_key) || raise_improper_use!()
|
||||
defp put_opts(value), do: Process.put(@opts_key, value)
|
||||
defp delete_opts(), do: Process.delete(@opts_key)
|
||||
|
||||
defp get_config!(), do: Process.get(@config_key) || raise_improper_use!()
|
||||
defp put_config(value), do: Process.put(@config_key, value)
|
||||
defp delete_config(), do: Process.delete(@config_key)
|
||||
|
||||
defp get_imports!(), do: Process.get(@imports_key) || raise_improper_use!()
|
||||
defp put_imports(value), do: Process.put(@imports_key, value)
|
||||
defp delete_imports(), do: Process.delete(@imports_key)
|
||||
|
||||
defp raise_improper_use!() do
|
||||
raise "could not set configuration via Config. " <>
|
||||
"This usually means you are trying to execute a configuration file " <>
|
||||
"directly, instead of reading it with Config.Reader"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Configures the given `root_key`.
|
||||
|
||||
Keyword lists are always deep-merged.
|
||||
|
||||
## Examples
|
||||
|
||||
The given `opts` are merged into the existing configuration
|
||||
for the given `root_key`. Conflicting keys are overridden by the
|
||||
ones specified in `opts`, unless they are keywords, which are
|
||||
deep merged recursively. For example, the application configuration
|
||||
below
|
||||
|
||||
config :logger,
|
||||
level: :warn,
|
||||
backends: [:console]
|
||||
|
||||
config :logger,
|
||||
level: :info,
|
||||
truncate: 1024
|
||||
|
||||
will have a final configuration for `:logger` of:
|
||||
|
||||
[level: :info, backends: [:console], truncate: 1024]
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
|
||||
unless Keyword.keyword?(opts) do
|
||||
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
|
||||
end
|
||||
|
||||
get_config!()
|
||||
|> __merge__([{root_key, opts}])
|
||||
|> put_config()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Configures the given `key` for the given `root_key`.
|
||||
|
||||
Keyword lists are always deep merged.
|
||||
|
||||
## Examples
|
||||
|
||||
The given `opts` are merged into the existing values for `key`
|
||||
in the given `root_key`. Conflicting keys are overridden by the
|
||||
ones specified in `opts`, unless they are keywords, which are
|
||||
deep merged recursively. For example, the application configuration
|
||||
below
|
||||
|
||||
config :ecto, Repo,
|
||||
log_level: :warn,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
metadata: [read_only: true]
|
||||
|
||||
config :ecto, Repo,
|
||||
log_level: :info,
|
||||
pool_size: 10,
|
||||
metadata: [replica: true]
|
||||
|
||||
will have a final value of the configuration for the `Repo`
|
||||
key in the `:ecto` application of:
|
||||
|
||||
Application.get_env(:ecto, Repo)
|
||||
#=> [
|
||||
#=> log_level: :info,
|
||||
#=> pool_size: 10,
|
||||
#=> adapter: Ecto.Adapters.Postgres,
|
||||
#=> metadata: [read_only: true, replica: true]
|
||||
#=> ]
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
|
||||
get_config!()
|
||||
|> __merge__([{root_key, [{key, opts}]}])
|
||||
|> put_config()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the environment this configuration file is executed on.
|
||||
|
||||
In Mix projects this function returns the environment this configuration
|
||||
file is executed on. In releases, the environment when `mix release` ran.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
|
||||
if config_env() == :prod do
|
||||
config :my_app, :debug, false
|
||||
end
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
defmacro config_env() do
|
||||
quote do
|
||||
Config.__env__!()
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __env__!() :: atom()
|
||||
def __env__!() do
|
||||
elem(get_opts!(), 0) || raise "no :env key was given to this configuration file"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the target this configuration file is executed on.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
|
||||
if config_target() == :host do
|
||||
config :my_app, :debug, false
|
||||
end
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
defmacro config_target() do
|
||||
quote do
|
||||
Config.__target__!()
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __target__!() :: atom()
|
||||
def __target__!() do
|
||||
elem(get_opts!(), 1) || raise "no :target key was given to this configuration file"
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Imports configuration from the given file.
|
||||
|
||||
In case the file doesn't exist, an error is raised.
|
||||
|
||||
If file is a relative, it will be expanded relatively to the
|
||||
directory the current configuration file is in.
|
||||
|
||||
## Examples
|
||||
|
||||
This is often used to emulate configuration across environments:
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
|
||||
Note, however, some configuration files, such as `config/runtime.exs`
|
||||
does not support imports, as they are meant to be copied across
|
||||
systems.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
defmacro import_config(file) do
|
||||
quote do
|
||||
Config.__import__!(Path.expand(unquote(file), __DIR__))
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __import__!(Path.t()) :: {term, Code.binding()}
|
||||
def __import__!(file) when is_binary(file) do
|
||||
import_config!(file, File.read!(file), true)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
|
||||
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
env = Keyword.get(opts, :env)
|
||||
target = Keyword.get(opts, :target)
|
||||
imports = Keyword.get(opts, :imports, [])
|
||||
|
||||
previous_opts = put_opts({env, target})
|
||||
previous_config = put_config([])
|
||||
previous_imports = put_imports(imports)
|
||||
|
||||
try do
|
||||
{eval_config, _} = import_config!(file, content, false)
|
||||
|
||||
case get_config!() do
|
||||
[] when is_list(eval_config) ->
|
||||
{validate!(eval_config, file), get_imports!()}
|
||||
|
||||
pdict_config ->
|
||||
{pdict_config, get_imports!()}
|
||||
end
|
||||
after
|
||||
if previous_opts, do: put_opts(previous_opts), else: delete_opts()
|
||||
if previous_config, do: put_config(previous_config), else: delete_config()
|
||||
if previous_imports, do: put_imports(previous_imports), else: delete_imports()
|
||||
end
|
||||
end
|
||||
|
||||
defp import_config!(file, contents, raise_when_disabled?) do
|
||||
current_imports = get_imports!()
|
||||
|
||||
cond do
|
||||
current_imports == :disabled ->
|
||||
if raise_when_disabled? do
|
||||
raise "import_config/1 is not enabled for this configuration file. " <>
|
||||
"Some configuration files do not allow importing other files " <>
|
||||
"as they are often copied to external systems"
|
||||
end
|
||||
|
||||
file in current_imports ->
|
||||
raise ArgumentError,
|
||||
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
|
||||
|
||||
true ->
|
||||
put_imports([file | current_imports])
|
||||
:ok
|
||||
end
|
||||
|
||||
Code.eval_string(contents, [], file: file)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __merge__(config1, config2) when is_list(config1) and is_list(config2) do
|
||||
Keyword.merge(config1, config2, fn _, app1, app2 ->
|
||||
Keyword.merge(app1, app2, &deep_merge/3)
|
||||
end)
|
||||
end
|
||||
|
||||
defp deep_merge(_key, value1, value2) do
|
||||
if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
|
||||
Keyword.merge(value1, value2, &deep_merge/3)
|
||||
else
|
||||
value2
|
||||
end
|
||||
end
|
||||
|
||||
defp validate!(config, file) do
|
||||
Enum.all?(config, fn
|
||||
{app, value} when is_atom(app) ->
|
||||
if Keyword.keyword?(value) do
|
||||
true
|
||||
else
|
||||
raise ArgumentError,
|
||||
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
|
||||
"to return keyword list, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|
||||
config
|
||||
end
|
||||
end
|
||||
@@ -1,415 +0,0 @@
|
||||
defmodule Config.Provider do
|
||||
@moduledoc """
|
||||
Specifies a provider API that loads configuration during boot.
|
||||
|
||||
Config providers are typically used during releases to load
|
||||
external configuration while the system boots. This is done
|
||||
by starting the VM with the minimum amount of applications
|
||||
running, then invoking all of the providers, and then
|
||||
restarting the system. This requires a mutable configuration
|
||||
file on disk, as the results of the providers are written to
|
||||
the file system. For more information on runtime configuration,
|
||||
see `mix release`.
|
||||
|
||||
## Multiple config files
|
||||
|
||||
One common use of config providers is to specify multiple
|
||||
configuration files in a release. Elixir ships with one provider,
|
||||
called `Config.Reader`, which is capable of handling Elixir's
|
||||
built-in config files.
|
||||
|
||||
For example, imagine you want to list some basic configuration
|
||||
on Mix's built-in `config/runtime.exs` file, but you also want
|
||||
to support additional configuration files. To do so, you can add
|
||||
this inside the `def project` portion of your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
config_providers: [
|
||||
{Config.Reader, {:system, "RELEASE_ROOT", "/extra_config.exs"}}
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
You can place this `extra_config.exs` file in your release in
|
||||
multiple ways:
|
||||
|
||||
1. If it is available on the host when assembling the release,
|
||||
you can place it on "rel/overlays/extra_config.exs" and it
|
||||
will be automatically copied to the release root
|
||||
|
||||
2. If it is available on the target during deployment, you can
|
||||
simply copy it to the release root as a step in your deployment
|
||||
|
||||
Now once the system boots, it will load both `config/runtime.exs`
|
||||
and `extra_config.exs` early in the boot process. You can learn
|
||||
more options on `Config.Reader`.
|
||||
|
||||
## Custom config provider
|
||||
|
||||
You can also implement custom config providers, similar to how
|
||||
`Config.Reader` works. For example, imagine you need to load
|
||||
some configuration from a JSON file and load that into the system.
|
||||
Said configuration provider would look like:
|
||||
|
||||
defmodule JSONConfigProvider do
|
||||
@behaviour Config.Provider
|
||||
|
||||
# Let's pass the path to the JSON file as config
|
||||
@impl true
|
||||
def init(path) when is_binary(path), do: path
|
||||
|
||||
@impl true
|
||||
def load(config, path) do
|
||||
# We need to start any app we may depend on.
|
||||
{:ok, _} = Application.ensure_all_started(:jason)
|
||||
|
||||
json = path |> File.read!() |> Jason.decode!()
|
||||
|
||||
Config.Reader.merge(
|
||||
config,
|
||||
my_app: [
|
||||
some_value: json["my_app_some_value"],
|
||||
another_value: json["my_app_another_value"],
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
Then, when specifying your release, you can specify the provider in
|
||||
the release configuration:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
config_providers: [
|
||||
{JSONConfigProvider, "/etc/config.json"}
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
@type config :: keyword
|
||||
@type state :: term
|
||||
|
||||
@typedoc """
|
||||
A path pointing to a configuration file.
|
||||
|
||||
Since configuration files are often accessed on target machines,
|
||||
it can be expressed either as:
|
||||
|
||||
* a binary representing an absolute path
|
||||
|
||||
* a `{:system, system_var, path}` tuple where the config is the
|
||||
concatenation of the environment variable `system_var` with
|
||||
the given `path`
|
||||
|
||||
"""
|
||||
@type config_path :: {:system, binary(), binary()} | binary()
|
||||
|
||||
@doc """
|
||||
Invoked when initializing a config provider.
|
||||
|
||||
A config provider is typically initialized on the machine
|
||||
where the system is assembled and not on the target machine.
|
||||
The `c:init/1` callback is useful to verify the arguments
|
||||
given to the provider and prepare the state that will be
|
||||
given to `c:load/2`.
|
||||
|
||||
Furthermore, because the state returned by `c:init/1` can
|
||||
be written to text-based config files, it should be
|
||||
restricted only to simple data types, such as integers,
|
||||
strings, atoms, tuples, maps, and lists. Entries such as
|
||||
PIDs, references, and functions cannot be serialized.
|
||||
"""
|
||||
@callback init(term) :: state
|
||||
|
||||
@doc """
|
||||
Loads configuration (typically during system boot).
|
||||
|
||||
It receives the current `config` and the `state` returned by
|
||||
`c:init/1`. Then, you typically read the extra configuration
|
||||
from an external source and merge it into the received `config`.
|
||||
Merging should be done with `Config.Reader.merge/2`, as it
|
||||
performs deep merge. It should return the updated config.
|
||||
|
||||
Note that `c:load/2` is typically invoked very early in the
|
||||
boot process, therefore if you need to use an application
|
||||
in the provider, it is your responsibility to start it.
|
||||
"""
|
||||
@callback load(config, state) :: config
|
||||
|
||||
@doc false
|
||||
defstruct [
|
||||
:providers,
|
||||
:config_path,
|
||||
extra_config: [],
|
||||
prune_runtime_sys_config_after_boot: false,
|
||||
reboot_system_after_config: false,
|
||||
validate_compile_env: false
|
||||
]
|
||||
|
||||
@reserved_apps [:kernel, :stdlib]
|
||||
|
||||
@doc """
|
||||
Validates a `t:config_path/0`.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec validate_config_path!(config_path) :: :ok
|
||||
def validate_config_path!({:system, name, path})
|
||||
when is_binary(name) and is_binary(path),
|
||||
do: :ok
|
||||
|
||||
def validate_config_path!(path) do
|
||||
if is_binary(path) and Path.type(path) != :relative do
|
||||
:ok
|
||||
else
|
||||
raise ArgumentError, """
|
||||
expected configuration path to be:
|
||||
|
||||
* a binary representing an absolute path
|
||||
* a tuple {:system, system_var, path} where the config is the \
|
||||
concatenation of the `system_var` with the given `path`
|
||||
|
||||
Got: #{inspect(path)}
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resolves a `t:config_path/0` to an actual path.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec resolve_config_path!(config_path) :: binary
|
||||
def resolve_config_path!(path) when is_binary(path), do: path
|
||||
def resolve_config_path!({:system, name, path}), do: System.fetch_env!(name) <> path
|
||||
|
||||
# Private keys
|
||||
@init_key :config_provider_init
|
||||
@booted_key :config_provider_booted
|
||||
|
||||
# Public keys
|
||||
@reboot_mode_key :config_provider_reboot_mode
|
||||
|
||||
@doc false
|
||||
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
|
||||
validate_config_path!(config_path)
|
||||
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
|
||||
init = struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
|
||||
[elixir: [{@init_key, init}]]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def boot(reboot_fun \\ &restart_and_sleep/0) do
|
||||
# The config provider typically runs very early in the
|
||||
# release process, so we need to make sure Elixir is started
|
||||
# before we go around running Elixir code.
|
||||
{:ok, _} = :application.ensure_all_started(:elixir)
|
||||
|
||||
case Application.fetch_env(:elixir, @booted_key) do
|
||||
{:ok, {:booted, path}} ->
|
||||
path && File.rm(path)
|
||||
|
||||
with {:ok, %Config.Provider{} = provider} <- Application.fetch_env(:elixir, @init_key) do
|
||||
maybe_validate_compile_env(provider)
|
||||
end
|
||||
|
||||
:booted
|
||||
|
||||
_ ->
|
||||
case Application.fetch_env(:elixir, @init_key) do
|
||||
{:ok, %Config.Provider{} = provider} ->
|
||||
path = resolve_config_path!(provider.config_path)
|
||||
reboot_config = [elixir: [{@booted_key, booted_value(provider, path)}]]
|
||||
boot_providers(path, provider, reboot_config, reboot_fun)
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp boot_providers(path, provider, reboot_config, reboot_fun) do
|
||||
original_config = read_config!(path)
|
||||
|
||||
config =
|
||||
original_config
|
||||
|> Config.__merge__(provider.extra_config)
|
||||
|> run_providers(provider)
|
||||
|
||||
if provider.reboot_system_after_config do
|
||||
config
|
||||
|> Config.__merge__(reboot_config)
|
||||
|> write_config!(path)
|
||||
|
||||
reboot_fun.()
|
||||
else
|
||||
for app <- @reserved_apps, config[app] != original_config[app] do
|
||||
abort("""
|
||||
Cannot configure #{inspect(app)} because :reboot_system_after_config has been set \
|
||||
to false and #{inspect(app)} has already been loaded, meaning any further \
|
||||
configuration won't have an effect.
|
||||
|
||||
The configuration for #{inspect(app)} before config providers was:
|
||||
|
||||
#{inspect(original_config[app])}
|
||||
|
||||
The configuration for #{inspect(app)} after config providers was:
|
||||
|
||||
#{inspect(config[app])}
|
||||
""")
|
||||
end
|
||||
|
||||
_ = Application.put_all_env(config, persistent: true)
|
||||
maybe_validate_compile_env(provider)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_validate_compile_env(provider) do
|
||||
with [_ | _] = compile_env <- provider.validate_compile_env do
|
||||
validate_compile_env(compile_env)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def validate_compile_env(compile_env, ensure_loaded? \\ true) do
|
||||
for {app, [key | path], compile_return} <- compile_env,
|
||||
ensure_app_loaded?(app, ensure_loaded?) do
|
||||
try do
|
||||
traverse_env(Application.fetch_env(app, key), path)
|
||||
rescue
|
||||
e ->
|
||||
abort("""
|
||||
application #{inspect(app)} failed reading its compile environment #{path(key, path)}:
|
||||
|
||||
#{Exception.format(:error, e, __STACKTRACE__)}
|
||||
|
||||
Expected it to match the compile time value of #{return_to_text(compile_return)}.
|
||||
|
||||
#{compile_env_tips(app)}
|
||||
""")
|
||||
else
|
||||
^compile_return ->
|
||||
:ok
|
||||
|
||||
runtime_return ->
|
||||
abort("""
|
||||
the application #{inspect(app)} has a different value set #{path(key, path)} \
|
||||
during runtime compared to compile time. Since this application environment entry was \
|
||||
marked as compile time, this difference can lead to different behaviour than expected:
|
||||
|
||||
* Compile time value #{return_to_text(compile_return)}
|
||||
* Runtime value #{return_to_text(runtime_return)}
|
||||
|
||||
#{compile_env_tips(app)}
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_app_loaded?(app, true), do: Application.ensure_loaded(app) == :ok
|
||||
defp ensure_app_loaded?(app, false), do: Application.spec(app, :vsn) != nil
|
||||
|
||||
defp path(key, []), do: "for key #{inspect(key)}"
|
||||
defp path(key, path), do: "for path #{inspect(path)} inside key #{inspect(key)}"
|
||||
|
||||
defp compile_env_tips(app),
|
||||
do: """
|
||||
To fix this error, you might:
|
||||
|
||||
* Make the runtime value match the compile time one
|
||||
|
||||
* Recompile your project. If the misconfigured application is a dependency, \
|
||||
you may need to run "mix deps.compile #{app} --force"
|
||||
|
||||
* Alternatively, you can disable this check. If you are using releases, you can \
|
||||
set :validate_compile_env to false in your release configuration. If you are \
|
||||
using Mix to start your system, you can pass the --no-validate-compile-env flag
|
||||
"""
|
||||
|
||||
defp return_to_text({:ok, value}), do: "was set to: #{inspect(value)}"
|
||||
defp return_to_text(:error), do: "was not set"
|
||||
|
||||
defp traverse_env(return, []), do: return
|
||||
defp traverse_env(:error, _paths), do: :error
|
||||
defp traverse_env({:ok, value}, [key | keys]), do: traverse_env(Access.fetch(value, key), keys)
|
||||
|
||||
@compile {:no_warn_undefined, {:init, :restart, 1}}
|
||||
defp restart_and_sleep() do
|
||||
mode = Application.get_env(:elixir, @reboot_mode_key)
|
||||
|
||||
if mode in [:embedded, :interactive] do
|
||||
:init.restart(mode: mode)
|
||||
else
|
||||
:init.restart()
|
||||
end
|
||||
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
|
||||
defp booted_value(%{prune_runtime_sys_config_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_value(%{prune_runtime_sys_config_after_boot: false}, _path), do: {:booted, nil}
|
||||
|
||||
defp read_config!(path) do
|
||||
case :file.consult(path) do
|
||||
{:ok, [inner]} ->
|
||||
inner
|
||||
|
||||
{:error, reason} ->
|
||||
bad_path_abort(
|
||||
"Could not read runtime configuration due to reason: #{inspect(reason)}",
|
||||
path
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp run_providers(config, %{providers: providers}) do
|
||||
Enum.reduce(providers, config, fn {provider, state}, acc ->
|
||||
try do
|
||||
provider.load(acc, state)
|
||||
catch
|
||||
kind, error ->
|
||||
IO.puts(:stderr, "ERROR! Config provider #{inspect(provider)} failed with:")
|
||||
IO.puts(:stderr, Exception.format(kind, error, __STACKTRACE__))
|
||||
:erlang.raise(kind, error, __STACKTRACE__)
|
||||
else
|
||||
term when is_list(term) ->
|
||||
term
|
||||
|
||||
term ->
|
||||
abort("Expected provider #{inspect(provider)} to return a list, got: #{inspect(term)}")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_config!(config, path) do
|
||||
contents = :io_lib.format("%% coding: utf-8~n~tw.~n", [config])
|
||||
|
||||
case File.write(path, IO.chardata_to_string(contents)) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
bad_path_abort(
|
||||
"Could not write runtime configuration due to reason: #{inspect(reason)}",
|
||||
path
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp bad_path_abort(msg, path) do
|
||||
abort(
|
||||
msg <>
|
||||
". Please make sure #{inspect(path)} is writable and accessible " <>
|
||||
"or choose a different path"
|
||||
)
|
||||
end
|
||||
|
||||
defp abort(msg) do
|
||||
IO.puts("ERROR! " <> msg)
|
||||
:erlang.raise(:error, "aborting boot", [{Config.Provider, :boot, 2, []}])
|
||||
end
|
||||
end
|
||||
@@ -1,138 +0,0 @@
|
||||
defmodule Config.Reader do
|
||||
@moduledoc """
|
||||
API for reading config files defined with `Config`.
|
||||
|
||||
## As a provider
|
||||
|
||||
`Config.Reader` can also be used as a `Config.Provider`. A config
|
||||
provider is used during releases to customize how applications are
|
||||
configured. When used as a provider, it expects a single argument:
|
||||
the configuration path (as outlined in `t:Config.Provider.config_path/0`)
|
||||
for the file to be read and loaded during the system boot.
|
||||
|
||||
For example, if you expect the target system to have a config file
|
||||
in an absolute path, you can add this inside the `def project` portion
|
||||
of your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
config_providers: [
|
||||
{Config.Reader, "/etc/config.exs"}
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Or if you want to read a custom path inside the release:
|
||||
|
||||
config_providers: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]
|
||||
|
||||
You can also pass a keyword list of options to the reader,
|
||||
where the `:path` is a required key:
|
||||
|
||||
config_providers: [
|
||||
{Config.Reader,
|
||||
path: "/etc/config.exs",
|
||||
env: :prod,
|
||||
imports: :disabled}
|
||||
]
|
||||
|
||||
Remember Mix already loads `config/runtime.exs` by default.
|
||||
For more examples and scenarios, see the `Config.Provider` module.
|
||||
"""
|
||||
|
||||
@behaviour Config.Provider
|
||||
|
||||
@impl true
|
||||
def init(opts) when is_list(opts) do
|
||||
{path, opts} = Keyword.pop!(opts, :path)
|
||||
Config.Provider.validate_config_path!(path)
|
||||
{path, opts}
|
||||
end
|
||||
|
||||
def init(path) do
|
||||
init(path: path)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def load(config, {path, opts}) do
|
||||
merge(config, path |> Config.Provider.resolve_config_path!() |> read!(opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluates the configuration `contents` for the given `file`.
|
||||
|
||||
Accepts the same options as `read!/2`.
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec eval!(Path.t(), binary, keyword) :: keyword
|
||||
def eval!(file, contents, opts \\ [])
|
||||
when is_binary(file) and is_binary(contents) and is_list(opts) do
|
||||
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the configuration file.
|
||||
|
||||
## Options
|
||||
|
||||
* `:imports` - a list of already imported paths or `:disabled`
|
||||
to disable imports
|
||||
|
||||
* `:env` - the environment the configuration file runs on.
|
||||
See `Config.config_env/0` for sample usage
|
||||
|
||||
* `:target` - the target the configuration file runs on.
|
||||
See `Config.config_target/0` for sample usage
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec read!(Path.t(), keyword) :: keyword
|
||||
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
file = Path.expand(file)
|
||||
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the given configuration file and returns the configuration
|
||||
with its imports.
|
||||
|
||||
Accepts the same options as `read!/2`. Although note the `:imports`
|
||||
option cannot be disabled in `read_imports!/2`.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
|
||||
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
if opts[:imports] == :disabled do
|
||||
raise ArgumentError, ":imports must be a list of paths"
|
||||
end
|
||||
|
||||
file = Path.expand(file)
|
||||
Config.__eval__!(file, File.read!(file), opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges two configurations.
|
||||
|
||||
The configurations are merged together with the values in
|
||||
the second one having higher preference than the first in
|
||||
case of conflicts. In case both values are set to keyword
|
||||
lists, it deep merges them.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Config.Reader.merge([app: [k: :v1]], [app: [k: :v2]])
|
||||
[app: [k: :v2]]
|
||||
|
||||
iex> Config.Reader.merge([app: [k: [v1: 1, v2: 2]]], [app: [k: [v2: :a, v3: :b]]])
|
||||
[app: [k: [v1: 1, v2: :a, v3: :b]]]
|
||||
|
||||
iex> Config.Reader.merge([app1: []], [app2: []])
|
||||
[app1: [], app2: []]
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec merge(keyword, keyword) :: keyword
|
||||
def merge(config1, config2) when is_list(config1) and is_list(config2) do
|
||||
Config.__merge__(config1, config2)
|
||||
end
|
||||
end
|
||||
@@ -1,443 +0,0 @@
|
||||
defmodule Dict do
|
||||
@moduledoc ~S"""
|
||||
Generic API for dictionaries.
|
||||
|
||||
If you need a general dictionary, use the `Map` module.
|
||||
If you need to manipulate keyword lists, use `Keyword`.
|
||||
|
||||
To convert maps into keywords and vice-versa, use the
|
||||
`new` function in the respective modules.
|
||||
"""
|
||||
|
||||
@moduledoc deprecated: "Use Map or Keyword modules instead"
|
||||
|
||||
@type key :: any
|
||||
@type value :: any
|
||||
@type t :: list | map
|
||||
|
||||
message =
|
||||
"Use the Map module for working with maps or the Keyword module for working with keyword lists"
|
||||
|
||||
defmacro __using__(_) do
|
||||
# Use this import to guarantee proper code expansion
|
||||
import Kernel, except: [size: 1]
|
||||
|
||||
if __CALLER__.module != HashDict do
|
||||
IO.warn("use Dict is deprecated. " <> unquote(message), __CALLER__)
|
||||
end
|
||||
|
||||
quote do
|
||||
message = "Use maps and the Map module instead"
|
||||
|
||||
@deprecated message
|
||||
def get(dict, key, default \\ nil) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def get_lazy(dict, key, fun) when is_function(fun, 0) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> fun.()
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def get_and_update(dict, key, fun) do
|
||||
current_value = get(dict, key)
|
||||
{get, new_value} = fun.(current_value)
|
||||
{get, put(dict, key, new_value)}
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def fetch!(dict, key) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> raise KeyError, key: key, term: dict
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def has_key?(dict, key) do
|
||||
match?({:ok, _}, fetch(dict, key))
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def put_new(dict, key, value) do
|
||||
case has_key?(dict, key) do
|
||||
true -> dict
|
||||
false -> put(dict, key, value)
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def put_new_lazy(dict, key, fun) when is_function(fun, 0) do
|
||||
case has_key?(dict, key) do
|
||||
true -> dict
|
||||
false -> put(dict, key, fun.())
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def drop(dict, keys) do
|
||||
Enum.reduce(keys, dict, &delete(&2, &1))
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
def to_list(dict) do
|
||||
reduce(dict, {:cont, []}, fn kv, acc -> {:cont, [kv | acc]} end)
|
||||
|> elem(1)
|
||||
|> :lists.reverse()
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def keys(dict) do
|
||||
reduce(dict, {:cont, []}, fn {k, _}, acc -> {:cont, [k | acc]} end)
|
||||
|> elem(1)
|
||||
|> :lists.reverse()
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def values(dict) do
|
||||
reduce(dict, {:cont, []}, fn {_, v}, acc -> {:cont, [v | acc]} end)
|
||||
|> elem(1)
|
||||
|> :lists.reverse()
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
def update(dict, key, default, fun) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} ->
|
||||
put(dict, key, fun.(value))
|
||||
|
||||
:error ->
|
||||
put(dict, key, default)
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
def pop(dict, key, default \\ nil) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} ->
|
||||
{value, delete(dict, key)}
|
||||
|
||||
:error ->
|
||||
{default, dict}
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def pop_lazy(dict, key, fun) when is_function(fun, 0) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} ->
|
||||
{value, delete(dict, key)}
|
||||
|
||||
:error ->
|
||||
{fun.(), dict}
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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,
|
||||
get_and_update: 3,
|
||||
get_lazy: 3,
|
||||
pop_lazy: 3,
|
||||
put_new_lazy: 3
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop target(dict) do
|
||||
quote do
|
||||
case unquote(dict) do
|
||||
%module{} -> module
|
||||
%{} -> Map
|
||||
dict when is_list(dict) -> Keyword
|
||||
dict -> unsupported_dict(dict)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec keys(t) :: [key]
|
||||
def keys(dict) do
|
||||
target(dict).keys(dict)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec values(t) :: [value]
|
||||
def values(dict) do
|
||||
target(dict).values(dict)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec size(t) :: non_neg_integer
|
||||
def size(dict) do
|
||||
target(dict).size(dict)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec has_key?(t, key) :: boolean
|
||||
def has_key?(dict, key) do
|
||||
target(dict).has_key?(dict, key)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec get(t, key, value) :: value
|
||||
def get(dict, key, default \\ nil) do
|
||||
target(dict).get(dict, key, default)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec get_lazy(t, key, (() -> value)) :: value
|
||||
def get_lazy(dict, key, fun) do
|
||||
target(dict).get_lazy(dict, key, fun)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec get_and_update(t, key, (value -> {value, value})) :: {value, t}
|
||||
def get_and_update(dict, key, fun) do
|
||||
target(dict).get_and_update(dict, key, fun)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec fetch(t, key) :: value
|
||||
def fetch(dict, key) do
|
||||
target(dict).fetch(dict, key)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec fetch!(t, key) :: value
|
||||
def fetch!(dict, key) do
|
||||
target(dict).fetch!(dict, key)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec put(t, key, value) :: t
|
||||
def put(dict, key, val) do
|
||||
target(dict).put(dict, key, val)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec put_new(t, key, value) :: t
|
||||
def put_new(dict, key, val) do
|
||||
target(dict).put_new(dict, key, val)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec put_new_lazy(t, key, (() -> value)) :: t
|
||||
def put_new_lazy(dict, key, fun) do
|
||||
target(dict).put_new_lazy(dict, key, fun)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec delete(t, key) :: t
|
||||
def delete(dict, key) do
|
||||
target(dict).delete(dict, key)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec merge(t, t) :: t
|
||||
def merge(dict1, dict2) do
|
||||
target1 = target(dict1)
|
||||
target2 = target(dict2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.merge(dict1, dict2)
|
||||
else
|
||||
do_merge(target1, dict1, dict2, fn _k, _v1, v2 -> v2 end)
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec merge(t, t, (key, value, value -> value)) :: t
|
||||
def merge(dict1, dict2, fun) do
|
||||
target1 = target(dict1)
|
||||
target2 = target(dict2)
|
||||
|
||||
if target1 == target2 do
|
||||
target1.merge(dict1, dict2, fun)
|
||||
else
|
||||
do_merge(target1, dict1, dict2, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_merge(target1, dict1, dict2, fun) do
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
@spec pop(t, key, value) :: {value, t}
|
||||
def pop(dict, key, default \\ nil) do
|
||||
target(dict).pop(dict, key, default)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec pop_lazy(t, key, (() -> value)) :: {value, t}
|
||||
def pop_lazy(dict, key, fun) do
|
||||
target(dict).pop_lazy(dict, key, fun)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec update!(t, key, (value -> value)) :: t
|
||||
def update!(dict, key, fun) do
|
||||
target(dict).update!(dict, key, fun)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update(dict, key, default, fun) do
|
||||
target(dict).update(dict, key, default, fun)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec split(t, [key]) :: {t, t}
|
||||
def split(dict, keys) do
|
||||
target(dict).split(dict, keys)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec drop(t, [key]) :: t
|
||||
def drop(dict, keys) do
|
||||
target(dict).drop(dict, keys)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec take(t, [key]) :: t
|
||||
def take(dict, keys) do
|
||||
target(dict).take(dict, keys)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@spec empty(t) :: t
|
||||
def empty(dict) do
|
||||
target(dict).empty(dict)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
@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
|
||||
|
||||
@deprecated message
|
||||
@spec to_list(t) :: list
|
||||
def to_list(dict) do
|
||||
target(dict).to_list(dict)
|
||||
end
|
||||
|
||||
@spec unsupported_dict(t) :: no_return
|
||||
defp unsupported_dict(dict) do
|
||||
raise ArgumentError, "unsupported dict: #{inspect(dict)}"
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,95 +0,0 @@
|
||||
require Record
|
||||
|
||||
defmodule File.Stat do
|
||||
@moduledoc """
|
||||
A struct that holds file information.
|
||||
|
||||
In Erlang, this struct is represented by a `:file_info` record.
|
||||
Therefore this module also provides functions for converting
|
||||
between the Erlang record and the Elixir struct.
|
||||
|
||||
Its fields are:
|
||||
|
||||
* `size` - size of file in bytes.
|
||||
|
||||
* `type` - `:device | :directory | :regular | :other | :symlink`; 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-like operating systems, 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-like systems. In all other
|
||||
cases, this field is zero.
|
||||
|
||||
* `inode` - gives the inode number. On non-Unix-like file systems, this field
|
||||
will be zero.
|
||||
|
||||
* `uid` - indicates the owner of the file. Will be zero for non-Unix-like file
|
||||
systems.
|
||||
|
||||
* `gid` - indicates the group that owns the file. Will be zero for
|
||||
non-Unix-like 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 `:universal`.
|
||||
"""
|
||||
|
||||
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__{
|
||||
size: non_neg_integer(),
|
||||
type: :device | :directory | :regular | :other | :symlink,
|
||||
access: :read | :write | :read_write | :none,
|
||||
atime: :calendar.datetime() | integer(),
|
||||
mtime: :calendar.datetime() | integer(),
|
||||
ctime: :calendar.datetime() | integer(),
|
||||
mode: non_neg_integer(),
|
||||
links: non_neg_integer(),
|
||||
major_device: non_neg_integer(),
|
||||
minor_device: non_neg_integer(),
|
||||
inode: non_neg_integer(),
|
||||
uid: non_neg_integer(),
|
||||
gid: non_neg_integer()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Converts a `File.Stat` struct to a `:file_info` record.
|
||||
"""
|
||||
@spec to_record(t()) :: :file.file_info()
|
||||
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`.
|
||||
"""
|
||||
@spec from_record(:file.file_info()) :: t()
|
||||
def from_record(file_info)
|
||||
|
||||
def from_record({:file_info, unquote_splicing(vals)}) do
|
||||
%File.Stat{unquote_splicing(pairs)}
|
||||
end
|
||||
end
|
||||
@@ -1,190 +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 number 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 =
|
||||
case raw do
|
||||
true ->
|
||||
case :lists.keyfind(:read_ahead, 1, modes) do
|
||||
{:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
|
||||
{:read_ahead, _} -> [:raw | modes]
|
||||
false -> [:raw, :read_ahead | modes]
|
||||
end
|
||||
|
||||
false ->
|
||||
modes
|
||||
end
|
||||
|
||||
%File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes}
|
||||
end
|
||||
|
||||
defimpl Collectable do
|
||||
def into(%{path: path, modes: modes, raw: raw} = stream) do
|
||||
modes = for mode <- modes, mode not 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
|
||||
@read_ahead_size 64 * 1024
|
||||
|
||||
def reduce(%{path: path, modes: modes, line_or_bytes: line_or_bytes, raw: raw}, acc, fun) do
|
||||
start_fun = fn ->
|
||||
case :file.open(path, read_modes(modes)) do
|
||||
{:ok, device} ->
|
||||
if :trim_bom in modes, do: trim_bom(device, raw) |> elem(0), else: 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(%{path: path, modes: modes, line_or_bytes: :line} = stream) do
|
||||
pattern = :binary.compile_pattern("\n")
|
||||
counter = &count_lines(&1, path, pattern, read_function(stream), 0)
|
||||
|
||||
case File.open(path, read_modes(modes), counter) do
|
||||
{:ok, count} ->
|
||||
{:ok, count}
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
end
|
||||
end
|
||||
|
||||
def count(%{path: path, line_or_bytes: bytes, raw: true, modes: modes}) do
|
||||
case File.stat(path) do
|
||||
{:ok, %{size: 0}} ->
|
||||
{:error, __MODULE__}
|
||||
|
||||
{:ok, %{size: size}} ->
|
||||
remainder = if rem(size, bytes) == 0, do: 0, else: 1
|
||||
{:ok, div(size, bytes) + remainder - count_raw_bom(path, modes)}
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
end
|
||||
end
|
||||
|
||||
def count(_stream) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
|
||||
def member?(_stream, _term) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
|
||||
def slice(_stream) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
|
||||
defp count_raw_bom(path, modes) do
|
||||
if :trim_bom in modes do
|
||||
File.open!(path, read_modes(modes), &(&1 |> trim_bom(true) |> elem(1)))
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_bom(device, true) do
|
||||
bom_length = device |> IO.binread(4) |> bom_length()
|
||||
{:ok, new_pos} = :file.position(device, bom_length)
|
||||
{device, new_pos}
|
||||
end
|
||||
|
||||
defp trim_bom(device, false) do
|
||||
# Or we read the bom in the correct amount or it isn't there
|
||||
case bom_length(IO.read(device, 1)) do
|
||||
0 ->
|
||||
{:ok, _} = :file.position(device, 0)
|
||||
{device, 0}
|
||||
|
||||
_ ->
|
||||
{device, 1}
|
||||
end
|
||||
end
|
||||
|
||||
defp bom_length(<<239, 187, 191, _rest::binary>>), do: 3
|
||||
defp bom_length(<<254, 255, _rest::binary>>), do: 2
|
||||
defp bom_length(<<255, 254, _rest::binary>>), do: 2
|
||||
defp bom_length(<<0, 0, 254, 255, _rest::binary>>), do: 4
|
||||
defp bom_length(<<254, 255, 0, 0, _rest::binary>>), do: 4
|
||||
defp bom_length(_binary), do: 0
|
||||
|
||||
defp read_modes(modes) do
|
||||
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
|
||||
end
|
||||
|
||||
defp count_lines(device, path, pattern, read, count) do
|
||||
case read.(device) do
|
||||
data when is_binary(data) ->
|
||||
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
|
||||
|
||||
:eof ->
|
||||
count
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
end
|
||||
end
|
||||
|
||||
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
|
||||
|
||||
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
|
||||
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
|
||||
end
|
||||
end
|
||||
@@ -1,628 +0,0 @@
|
||||
import Kernel, except: [round: 1]
|
||||
|
||||
defmodule Float do
|
||||
@moduledoc """
|
||||
Functions for working with floating-point numbers.
|
||||
|
||||
## Kernel functions
|
||||
|
||||
There are functions related to floating-point numbers on the `Kernel` module
|
||||
too. Here is a list of them:
|
||||
|
||||
* `Kernel.round/1`: rounds a number to the nearest integer.
|
||||
* `Kernel.trunc/1`: returns the integer part of a number.
|
||||
|
||||
## Known issues
|
||||
|
||||
There are some very well known problems with floating-point numbers
|
||||
and arithmetic due to the fact most decimal fractions cannot be
|
||||
represented by a floating-point binary and most operations are not exact,
|
||||
but operate on approximations. Those issues are not specific
|
||||
to Elixir, they are a property of floating point representation itself.
|
||||
|
||||
For example, the numbers 0.1 and 0.01 are two of them, what means the result
|
||||
of squaring 0.1 does not give 0.01 neither the closest representable. Here is
|
||||
what happens in this case:
|
||||
|
||||
* The closest representable number to 0.1 is 0.1000000014
|
||||
* The closest representable number to 0.01 is 0.0099999997
|
||||
* Doing 0.1 * 0.1 should return 0.01, but because 0.1 is actually 0.1000000014,
|
||||
the result is 0.010000000000000002, and because this is not the closest
|
||||
representable number to 0.01, you'll get the wrong result for this operation
|
||||
|
||||
There are also other known problems like flooring or rounding numbers. See
|
||||
`round/2` and `floor/2` for more details about them.
|
||||
|
||||
To learn more about floating-point arithmetic visit:
|
||||
|
||||
* [0.30000000000000004.com](http://0.30000000000000004.com/)
|
||||
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
|
||||
|
||||
"""
|
||||
|
||||
import Bitwise
|
||||
|
||||
@power_of_2_to_52 4_503_599_627_370_496
|
||||
@precision_range 0..15
|
||||
@type precision_range :: 0..15
|
||||
|
||||
@min_finite then(<<0xFFEFFFFFFFFFFFFF::64>>, fn <<num::float>> -> num end)
|
||||
@max_finite then(<<0x7FEFFFFFFFFFFFFF::64>>, fn <<num::float>> -> num end)
|
||||
|
||||
@doc """
|
||||
Returns the maximum finite value for a float.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.max_finite()
|
||||
1.7976931348623157e308
|
||||
|
||||
"""
|
||||
def max_finite, do: @max_finite
|
||||
|
||||
@doc """
|
||||
Returns the minimum finite value for a float.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.min_finite()
|
||||
-1.7976931348623157e308
|
||||
|
||||
"""
|
||||
def min_finite, do: @min_finite
|
||||
|
||||
@doc """
|
||||
Computes `base` raised to power of `exponent`.
|
||||
|
||||
`base` must be a float and `exponent` can be any number.
|
||||
However, if a negative base and a fractional exponent
|
||||
are given, it raises `ArithmeticError`.
|
||||
|
||||
It always returns a float. See `Integer.pow/2` for
|
||||
exponentiation that returns integers.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.pow(2.0, 0)
|
||||
1.0
|
||||
iex> Float.pow(2.0, 1)
|
||||
2.0
|
||||
iex> Float.pow(2.0, 10)
|
||||
1024.0
|
||||
iex> Float.pow(2.0, -1)
|
||||
0.5
|
||||
iex> Float.pow(2.0, -3)
|
||||
0.125
|
||||
|
||||
iex> Float.pow(3.0, 1.5)
|
||||
5.196152422706632
|
||||
|
||||
iex> Float.pow(-2.0, 3)
|
||||
-8.0
|
||||
iex> Float.pow(-2.0, 4)
|
||||
16.0
|
||||
|
||||
iex> Float.pow(-1.0, 0.5)
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec pow(float, number) :: float
|
||||
def pow(base, exponent) when is_float(base) and is_number(exponent),
|
||||
do: :math.pow(base, exponent)
|
||||
|
||||
@doc """
|
||||
Parses a binary into a float.
|
||||
|
||||
If successful, returns a tuple in the form of `{float, remainder_of_binary}`;
|
||||
when the binary cannot be coerced into a valid float, the atom `:error` is
|
||||
returned.
|
||||
|
||||
If the size of float exceeds the maximum size of `1.7976931348623157e+308`,
|
||||
`:error` is returned even though the textual representation itself might be
|
||||
well formed.
|
||||
|
||||
If you want to convert a string-formatted float directly to a float,
|
||||
`String.to_float/1` can be used instead.
|
||||
|
||||
## 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
|
||||
iex> Float.parse("1.7976931348623159e+308")
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec parse(binary) :: {float, binary} | :error
|
||||
def parse("-" <> binary) do
|
||||
case parse_unsigned(binary) do
|
||||
:error -> :error
|
||||
{number, remainder} -> {-number, remainder}
|
||||
end
|
||||
end
|
||||
|
||||
def parse("+" <> binary) do
|
||||
parse_unsigned(binary)
|
||||
end
|
||||
|
||||
def parse(binary) do
|
||||
parse_unsigned(binary)
|
||||
end
|
||||
|
||||
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, false, false, <<digit>>)
|
||||
|
||||
defp parse_unsigned(binary) when is_binary(binary), do: :error
|
||||
|
||||
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, dot?, e?, <<acc::binary, digit>>)
|
||||
|
||||
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, false, <<acc::binary, ?., digit>>)
|
||||
|
||||
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
|
||||
when exp_marker in 'eE' and digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, digit>>)
|
||||
|
||||
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
|
||||
when exp_marker in 'eE' and sign in '-+' and digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)
|
||||
|
||||
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
|
||||
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
|
||||
# rescue the ArgumentError here and return an error.
|
||||
defp parse_unsigned(rest, dot?, true = _e?, acc) do
|
||||
:erlang.binary_to_float(add_dot(acc, dot?))
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
else
|
||||
float -> {float, rest}
|
||||
end
|
||||
|
||||
defp parse_unsigned(rest, dot?, false = _e?, acc),
|
||||
do: {:erlang.binary_to_float(add_dot(acc, dot?)), rest}
|
||||
|
||||
defp add_dot(acc, true), do: acc
|
||||
defp add_dot(acc, false), do: acc <> ".0"
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest number less than or equal to `num`.
|
||||
|
||||
`floor/2` also accepts a precision to round a floating-point value down
|
||||
to an arbitrary number of fractional digits (between 0 and 15).
|
||||
The operation is performed on the binary floating point, without a
|
||||
conversion to decimal.
|
||||
|
||||
This function always returns a float. `Kernel.trunc/1` may be used instead to
|
||||
truncate the result to an integer afterwards.
|
||||
|
||||
## Known issues
|
||||
|
||||
The behaviour of `floor/2` for floats can be surprising. For example:
|
||||
|
||||
iex> Float.floor(12.52, 2)
|
||||
12.51
|
||||
|
||||
One may have expected it to floor to 12.52. This is not a bug.
|
||||
Most decimal fractions cannot be represented as a binary floating point
|
||||
and therefore the number above is internally represented as 12.51999999,
|
||||
which explains the behaviour above.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.floor(34.25)
|
||||
34.0
|
||||
iex> Float.floor(-56.5)
|
||||
-57.0
|
||||
iex> Float.floor(34.259, 2)
|
||||
34.25
|
||||
|
||||
"""
|
||||
@spec floor(float, precision_range) :: float
|
||||
def floor(number, precision \\ 0)
|
||||
|
||||
def floor(number, 0) when is_float(number) do
|
||||
:math.floor(number)
|
||||
end
|
||||
|
||||
def floor(number, precision) when is_float(number) and precision in @precision_range do
|
||||
round(number, precision, :floor)
|
||||
end
|
||||
|
||||
def floor(number, precision) when is_float(number) do
|
||||
raise ArgumentError, invalid_precision_message(precision)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a float to the smallest integer greater than or equal to `num`.
|
||||
|
||||
`ceil/2` also accepts a precision to round a floating-point value down
|
||||
to an arbitrary number of fractional digits (between 0 and 15).
|
||||
|
||||
The operation is performed on the binary floating point, without a
|
||||
conversion to decimal.
|
||||
|
||||
The behaviour of `ceil/2` for floats can be surprising. For example:
|
||||
|
||||
iex> Float.ceil(-12.52, 2)
|
||||
-12.51
|
||||
|
||||
One may have expected it to ceil to -12.52. This is not a bug.
|
||||
Most decimal fractions cannot be represented as a binary floating point
|
||||
and therefore the number above is internally represented as -12.51999999,
|
||||
which explains the behaviour above.
|
||||
|
||||
This function always returns floats. `Kernel.trunc/1` may be used instead 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.251, 2)
|
||||
34.26
|
||||
|
||||
"""
|
||||
@spec ceil(float, precision_range) :: float
|
||||
def ceil(number, precision \\ 0)
|
||||
|
||||
def ceil(number, 0) when is_float(number) do
|
||||
:math.ceil(number)
|
||||
end
|
||||
|
||||
def ceil(number, precision) when is_float(number) and precision in @precision_range do
|
||||
round(number, precision, :ceil)
|
||||
end
|
||||
|
||||
def ceil(number, precision) when is_float(number) do
|
||||
raise ArgumentError, invalid_precision_message(precision)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a floating-point value to an arbitrary number of fractional
|
||||
digits (between 0 and 15).
|
||||
|
||||
The rounding direction always ties to half up. The operation is
|
||||
performed on the binary floating point, without a conversion to decimal.
|
||||
|
||||
This function only accepts floats and always returns a float. Use
|
||||
`Kernel.round/1` if you want a function that accepts both floats
|
||||
and integers and always returns an integer.
|
||||
|
||||
## Known issues
|
||||
|
||||
The behaviour of `round/2` for floats can be surprising. For example:
|
||||
|
||||
iex> Float.round(5.5675, 3)
|
||||
5.567
|
||||
|
||||
One may have expected it to round to the half up 5.568. This is not a bug.
|
||||
Most decimal fractions cannot be represented as a binary floating point
|
||||
and therefore the number above is internally represented as 5.567499999,
|
||||
which explains the behaviour above. If you want exact rounding for decimals,
|
||||
you must use a decimal library. The behaviour above is also in accordance
|
||||
to reference implementations, such as "Correctly Rounded Binary-Decimal and
|
||||
Decimal-Binary Conversions" by David M. Gay.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.round(12.5)
|
||||
13.0
|
||||
iex> Float.round(5.5674, 3)
|
||||
5.567
|
||||
iex> Float.round(5.5675, 3)
|
||||
5.567
|
||||
iex> Float.round(-5.5674, 3)
|
||||
-5.567
|
||||
iex> Float.round(-5.5675)
|
||||
-6.0
|
||||
iex> Float.round(12.341444444444441, 15)
|
||||
12.341444444444441
|
||||
|
||||
"""
|
||||
@spec round(float, precision_range) :: float
|
||||
# This implementation is slow since it relies on big integers.
|
||||
# Faster implementations are available on more recent papers
|
||||
# and could be implemented in the future.
|
||||
def round(float, precision \\ 0)
|
||||
|
||||
def round(float, 0) when is_float(float) do
|
||||
float |> :erlang.round() |> :erlang.float()
|
||||
end
|
||||
|
||||
def round(float, precision) when is_float(float) and precision in @precision_range do
|
||||
round(float, precision, :half_up)
|
||||
end
|
||||
|
||||
def round(float, precision) when is_float(float) do
|
||||
raise ArgumentError, invalid_precision_message(precision)
|
||||
end
|
||||
|
||||
defp round(0.0 = num, _precision, _rounding), do: num
|
||||
|
||||
defp round(float, precision, rounding) do
|
||||
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
|
||||
{num, count} = decompose(significant, 1)
|
||||
count = count - exp + 1023
|
||||
|
||||
cond do
|
||||
# Precision beyond 15 digits
|
||||
count >= 104 ->
|
||||
case rounding do
|
||||
:ceil when sign === 0 -> 1 / power_of_10(precision)
|
||||
:floor when sign === 1 -> -1 / power_of_10(precision)
|
||||
_ -> 0.0
|
||||
end
|
||||
|
||||
# We are asking more precision than we have
|
||||
count <= precision ->
|
||||
float
|
||||
|
||||
true ->
|
||||
# Difference in precision between float and asked precision
|
||||
# We subtract 1 because we need to calculate the remainder too
|
||||
diff = count - precision - 1
|
||||
|
||||
# Get up to latest so we calculate the remainder
|
||||
power_of_10 = power_of_10(diff)
|
||||
|
||||
# Convert the numerand to decimal base
|
||||
num = num * power_of_5(count)
|
||||
|
||||
# Move to the given precision - 1
|
||||
num = div(num, power_of_10)
|
||||
div = div(num, 10)
|
||||
num = rounding(rounding, sign, num, div)
|
||||
|
||||
# Convert back to float without loss
|
||||
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
|
||||
den = power_of_10(precision)
|
||||
boundary = den <<< 52
|
||||
|
||||
cond do
|
||||
num == 0 ->
|
||||
0.0
|
||||
|
||||
num >= boundary ->
|
||||
{den, exp} = scale_down(num, boundary, 52)
|
||||
decimal_to_float(sign, num, den, exp)
|
||||
|
||||
true ->
|
||||
{num, exp} = scale_up(num, boundary, 52)
|
||||
decimal_to_float(sign, num, den, exp)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp decompose(significant, initial) do
|
||||
decompose(significant, 1, 0, initial)
|
||||
end
|
||||
|
||||
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
|
||||
end
|
||||
|
||||
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, last_count, acc)
|
||||
end
|
||||
|
||||
defp decompose(<<>>, _count, last_count, acc) do
|
||||
{acc, last_count}
|
||||
end
|
||||
|
||||
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
|
||||
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
|
||||
|
||||
defp scale_down(num, den, exp) do
|
||||
new_den = den <<< 1
|
||||
|
||||
if num < new_den do
|
||||
{den >>> 52, exp}
|
||||
else
|
||||
scale_down(num, new_den, exp + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp decimal_to_float(sign, num, den, exp) do
|
||||
quo = div(num, den)
|
||||
rem = num - quo * den
|
||||
|
||||
tmp =
|
||||
case den >>> 1 do
|
||||
den when rem > den -> quo + 1
|
||||
den when rem < den -> quo
|
||||
_ when (quo &&& 1) === 1 -> quo + 1
|
||||
_ -> quo
|
||||
end
|
||||
|
||||
tmp = tmp - @power_of_2_to_52
|
||||
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
|
||||
tmp
|
||||
end
|
||||
|
||||
defp rounding(:floor, 1, _num, div), do: div + 1
|
||||
defp rounding(:ceil, 0, _num, div), do: div + 1
|
||||
|
||||
defp rounding(:half_up, _sign, num, div) do
|
||||
case rem(num, 10) do
|
||||
rem when rem < 5 -> div
|
||||
rem when rem >= 5 -> div + 1
|
||||
end
|
||||
end
|
||||
|
||||
defp rounding(_, _, _, div), do: div
|
||||
|
||||
Enum.reduce(0..104, 1, fn x, acc ->
|
||||
defp power_of_10(unquote(x)), do: unquote(acc)
|
||||
acc * 10
|
||||
end)
|
||||
|
||||
Enum.reduce(0..104, 1, fn x, acc ->
|
||||
defp power_of_5(unquote(x)), do: unquote(acc)
|
||||
acc * 5
|
||||
end)
|
||||
|
||||
@doc """
|
||||
Returns a pair of integers whose ratio is exactly equal
|
||||
to the original float and with a positive denominator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.ratio(0.0)
|
||||
{0, 1}
|
||||
iex> Float.ratio(3.14)
|
||||
{7070651414971679, 2251799813685248}
|
||||
iex> Float.ratio(-3.14)
|
||||
{-7070651414971679, 2251799813685248}
|
||||
iex> Float.ratio(1.5)
|
||||
{3, 2}
|
||||
iex> Float.ratio(-1.5)
|
||||
{-3, 2}
|
||||
iex> Float.ratio(16.0)
|
||||
{16, 1}
|
||||
iex> Float.ratio(-16.0)
|
||||
{-16, 1}
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec ratio(float) :: {integer, pos_integer}
|
||||
def ratio(0.0), do: {0, 1}
|
||||
|
||||
def ratio(float) when is_float(float) do
|
||||
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
||||
|
||||
{num, den_exp} =
|
||||
if exp != 0 do
|
||||
# Floats are expressed like this:
|
||||
# (2**52 + mantissa) * 2**(-52 + exp - 1023)
|
||||
#
|
||||
# We compute the root factors of the mantissa so we have this:
|
||||
# (2**52 + mantissa * 2**count) * 2**(-52 + exp - 1023)
|
||||
{mantissa, count} = root_factors(mantissa, 0)
|
||||
|
||||
# Now we can move the count around so we have this:
|
||||
# (2**(52-count) + mantissa) * 2**(count + -52 + exp - 1023)
|
||||
if mantissa == 0 do
|
||||
{1, exp - 1023}
|
||||
else
|
||||
num = (1 <<< (52 - count)) + mantissa
|
||||
den_exp = count - 52 + exp - 1023
|
||||
{num, den_exp}
|
||||
end
|
||||
else
|
||||
# Subnormals are expressed like this:
|
||||
# (mantissa) * 2**(-52 + 1 - 1023)
|
||||
#
|
||||
# So we compute it to this:
|
||||
# (mantissa * 2**(count)) * 2**(-52 + 1 - 1023)
|
||||
#
|
||||
# Which becomes:
|
||||
# mantissa * 2**(count-1074)
|
||||
root_factors(mantissa, -1074)
|
||||
end
|
||||
|
||||
if den_exp > 0 do
|
||||
{sign(sign, num <<< den_exp), 1}
|
||||
else
|
||||
{sign(sign, num), 1 <<< -den_exp}
|
||||
end
|
||||
end
|
||||
|
||||
defp root_factors(mantissa, count) when mantissa != 0 and (mantissa &&& 1) == 0,
|
||||
do: root_factors(mantissa >>> 1, count + 1)
|
||||
|
||||
defp root_factors(mantissa, count),
|
||||
do: {mantissa, count}
|
||||
|
||||
@compile {:inline, sign: 2}
|
||||
defp sign(0, num), do: num
|
||||
defp sign(1, num), do: -num
|
||||
|
||||
@doc """
|
||||
Returns a charlist which corresponds to the shortest text representation
|
||||
of the given float.
|
||||
|
||||
The underlying algorithm changes depending on the Erlang/OTP version:
|
||||
|
||||
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
|
||||
float-to-string conversion" in Proceedings of the SIGPLAN '2018
|
||||
Conference on Programming Language Design and Implementation.
|
||||
|
||||
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
|
||||
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
|
||||
Conference on Programming Language Design and Implementation.
|
||||
|
||||
For a configurable representation, use `:erlang.float_to_list/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.to_charlist(7.0)
|
||||
'7.0'
|
||||
|
||||
"""
|
||||
@spec to_charlist(float) :: charlist
|
||||
def to_charlist(float) when is_float(float) do
|
||||
:io_lib_format.fwrite_g(float)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a binary which corresponds to the shortest text representation
|
||||
of the given float.
|
||||
|
||||
The underlying algorithm changes depending on the Erlang/OTP version:
|
||||
|
||||
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
|
||||
float-to-string conversion" in Proceedings of the SIGPLAN '2018
|
||||
Conference on Programming Language Design and Implementation.
|
||||
|
||||
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
|
||||
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
|
||||
Conference on Programming Language Design and Implementation.
|
||||
|
||||
For a configurable representation, use `:erlang.float_to_binary/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.to_string(7.0)
|
||||
"7.0"
|
||||
|
||||
"""
|
||||
@spec to_string(float) :: String.t()
|
||||
def to_string(float) when is_float(float) do
|
||||
IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Float.to_charlist/1 instead"
|
||||
def to_char_list(float), do: Float.to_charlist(float)
|
||||
|
||||
@doc false
|
||||
@deprecated "Use :erlang.float_to_list/2 instead"
|
||||
def to_char_list(float, options) do
|
||||
:erlang.float_to_list(float, expand_compact(options))
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use :erlang.float_to_binary/2 instead"
|
||||
def to_string(float, options) do
|
||||
:erlang.float_to_binary(float, expand_compact(options))
|
||||
end
|
||||
|
||||
defp invalid_precision_message(precision) do
|
||||
"precision #{precision} is out of valid range of #{inspect(@precision_range)}"
|
||||
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
|
||||
@@ -1,208 +0,0 @@
|
||||
defmodule Function do
|
||||
@moduledoc """
|
||||
A set of functions for working with functions.
|
||||
|
||||
Anonymous functions are typically created by using `fn`:
|
||||
|
||||
iex> add = fn a, b -> a + b end
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
|
||||
Anonymous functions can also have multiple clauses. All clauses
|
||||
should expect the same number of arguments:
|
||||
|
||||
iex> negate = fn
|
||||
...> true -> false
|
||||
...> false -> true
|
||||
...> end
|
||||
iex> negate.(false)
|
||||
true
|
||||
|
||||
## The capture operator
|
||||
|
||||
It is also possible to capture public module functions and pass them
|
||||
around as if they were anonymous functions by using the capture
|
||||
operator `&/1`:
|
||||
|
||||
iex> add = &Kernel.+/2
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
|
||||
iex> length = &String.length/1
|
||||
iex> length.("hello")
|
||||
5
|
||||
|
||||
To capture a definition within the current module, you can skip the
|
||||
module prefix, such as `&my_fun/2`. In those cases, the captured
|
||||
function can be public (`def`) or private (`defp`).
|
||||
|
||||
The capture operator can also be used to create anonymous functions
|
||||
that expect at least one argument:
|
||||
|
||||
iex> add = &(&1 + &2)
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
|
||||
In such cases, using the capture operator is no different than using `fn`.
|
||||
|
||||
## Internal and external functions
|
||||
|
||||
We say that functions that point to definitions residing in modules, such
|
||||
as `&String.length/1`, are **external** functions. All other functions are
|
||||
**local** and they are always bound to the file or module that defined them.
|
||||
|
||||
Besides the functions in this module to work with functions, `Kernel` also
|
||||
has an `apply/2` function that invokes a function with a dynamic number of
|
||||
arguments, as well as `is_function/1` and `is_function/2`, to check
|
||||
respectively if a given value is a function or a function of a given arity.
|
||||
"""
|
||||
|
||||
@type information ::
|
||||
:arity
|
||||
| :env
|
||||
| :index
|
||||
| :module
|
||||
| :name
|
||||
| :new_index
|
||||
| :new_uniq
|
||||
| :pid
|
||||
| :type
|
||||
| :uniq
|
||||
|
||||
@doc """
|
||||
Captures the given function.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Function.capture(String, :length, 1)
|
||||
&String.length/1
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec capture(module, atom, arity) :: fun
|
||||
def capture(module, function_name, arity) do
|
||||
:erlang.make_fun(module, function_name, arity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a keyword list with information about a function.
|
||||
|
||||
The returned keys (with the corresponding possible values) for
|
||||
all types of functions (local and external) are the following:
|
||||
|
||||
* `:type` - `:local` (for anonymous functions) or `:external` (for
|
||||
named functions).
|
||||
|
||||
* `:module` - an atom which is the module where the function is defined when
|
||||
anonymous or the module which the function refers to when it's a named function.
|
||||
|
||||
* `:arity` - (integer) the number of arguments the function is to be called with.
|
||||
|
||||
* `:name` - (atom) the name of the function.
|
||||
|
||||
* `:env` - a list of the environment or free variables. For named
|
||||
functions, the returned list is always empty.
|
||||
|
||||
When `fun` is an anonymous function (that is, the type is `:local`), the following
|
||||
additional keys are returned:
|
||||
|
||||
* `:pid` - PID of the process that originally created the function.
|
||||
|
||||
* `:index` - (integer) an index into the module function table.
|
||||
|
||||
* `:new_index` - (integer) an index into the module function table.
|
||||
|
||||
* `:new_uniq` - (binary) a unique value for this function. It's
|
||||
calculated from the compiled code for the entire module.
|
||||
|
||||
* `:uniq` - (integer) a unique value for this function. This integer is
|
||||
calculated from the compiled code for the entire module.
|
||||
|
||||
**Note**: this function must be used only for debugging purposes.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> fun = fn x -> x end
|
||||
iex> info = Function.info(fun)
|
||||
iex> Keyword.get(info, :arity)
|
||||
1
|
||||
iex> Keyword.get(info, :type)
|
||||
:local
|
||||
|
||||
iex> fun = &String.length/1
|
||||
iex> info = Function.info(fun)
|
||||
iex> Keyword.get(info, :type)
|
||||
:external
|
||||
iex> Keyword.get(info, :name)
|
||||
:length
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec info(fun) :: [{information, term}]
|
||||
def info(fun), do: :erlang.fun_info(fun)
|
||||
|
||||
@doc """
|
||||
Returns a specific information about the function.
|
||||
|
||||
The returned information is a two-element tuple in the shape of
|
||||
`{info, value}`.
|
||||
|
||||
For any function, the information asked for can be any of the atoms
|
||||
`:module`, `:name`, `:arity`, `:env`, or `:type`.
|
||||
|
||||
For anonymous functions, there is also information about any of the
|
||||
atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
|
||||
For a named function, the value of any of these items is always the
|
||||
atom `:undefined`.
|
||||
|
||||
For more information on each of the possible returned values, see
|
||||
`info/1`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> f = fn x -> x end
|
||||
iex> Function.info(f, :arity)
|
||||
{:arity, 1}
|
||||
iex> Function.info(f, :type)
|
||||
{:type, :local}
|
||||
|
||||
iex> fun = &String.length/1
|
||||
iex> Function.info(fun, :name)
|
||||
{:name, :length}
|
||||
iex> Function.info(fun, :pid)
|
||||
{:pid, :undefined}
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec info(fun, item) :: {item, term} when item: information
|
||||
def info(fun, item), do: :erlang.fun_info(fun, item)
|
||||
|
||||
@doc """
|
||||
Returns its input `value`. This function can be passed as an anonymous function
|
||||
to transformation functions.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Function.identity("Hello world!")
|
||||
"Hello world!"
|
||||
|
||||
iex> 'abcdaabccc' |> Enum.sort() |> Enum.chunk_by(&Function.identity/1)
|
||||
['aaa', 'bb', 'cccc', 'd']
|
||||
|
||||
iex> Enum.group_by('abracadabra', &Function.identity/1)
|
||||
%{97 => 'aaaaa', 98 => 'bb', 99 => 'c', 100 => 'd', 114 => 'rr'}
|
||||
|
||||
iex> Enum.map([1, 2, 3, 4], &Function.identity/1)
|
||||
[1, 2, 3, 4]
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec identity(value) :: value when value: var
|
||||
def identity(value), do: value
|
||||
end
|
||||
@@ -1,903 +0,0 @@
|
||||
defmodule GenEvent do
|
||||
# Functions from this module are deprecated in elixir_dispatch.
|
||||
|
||||
@moduledoc """
|
||||
An event manager with event handlers behaviour.
|
||||
|
||||
If you are interested in implementing an event manager, please read the
|
||||
"Alternatives" section below. If you have to implement an event handler to
|
||||
integrate with an existing system, such as Elixir's Logger, please use
|
||||
[`:gen_event`](`:gen_event`) instead.
|
||||
|
||||
## Alternatives
|
||||
|
||||
There are a few suitable alternatives to replace GenEvent. Each of them can be
|
||||
the most beneficial based on the use case.
|
||||
|
||||
### Supervisor and GenServers
|
||||
|
||||
One alternative to GenEvent is a very minimal solution consisting of using a
|
||||
supervisor and multiple GenServers started under it. The supervisor acts as
|
||||
the "event manager" and the children GenServers act as the "event handlers".
|
||||
This approach has some shortcomings (it provides no backpressure for example)
|
||||
but can still replace GenEvent for low-profile usages of it. [This blog post
|
||||
by José
|
||||
Valim](http://blog.plataformatec.com.br/2016/11/replacing-genevent-by-a-supervisor-genserver/)
|
||||
has more detailed information on this approach.
|
||||
|
||||
### GenStage
|
||||
|
||||
If the use case where you were using GenEvent requires more complex logic,
|
||||
[GenStage](https://github.com/elixir-lang/gen_stage) provides a great
|
||||
alternative. GenStage is an external Elixir library maintained by the Elixir
|
||||
team; it provides a tool to implement systems that exchange events in a
|
||||
demand-driven way with built-in support for backpressure. See the [GenStage
|
||||
documentation](https://hexdocs.pm/gen_stage) for more information.
|
||||
|
||||
### `:gen_event`
|
||||
|
||||
If your use case requires exactly what GenEvent provided, or you have to
|
||||
integrate with an existing `:gen_event`-based system, you can still use the
|
||||
[`:gen_event`](`:gen_event`) Erlang module.
|
||||
"""
|
||||
|
||||
@moduledoc deprecated: "Use Erlang/OTP's :gen_event module instead"
|
||||
|
||||
@callback init(args :: term) ::
|
||||
{:ok, state}
|
||||
| {:ok, state, :hibernate}
|
||||
| {:error, reason :: any}
|
||||
when state: any
|
||||
|
||||
@callback handle_event(event :: term, state :: term) ::
|
||||
{:ok, new_state}
|
||||
| {:ok, new_state, :hibernate}
|
||||
| :remove_handler
|
||||
when new_state: term
|
||||
|
||||
@callback handle_call(request :: term, state :: term) ::
|
||||
{:ok, reply, new_state}
|
||||
| {:ok, reply, new_state, :hibernate}
|
||||
| {:remove_handler, reply}
|
||||
when reply: term, new_state: term
|
||||
|
||||
@callback handle_info(msg :: term, state :: term) ::
|
||||
{:ok, new_state}
|
||||
| {:ok, new_state, :hibernate}
|
||||
| :remove_handler
|
||||
when new_state: term
|
||||
|
||||
@callback terminate(reason, state :: term) :: term
|
||||
when reason: :stop | {:stop, term} | :remove_handler | {:error, term} | term
|
||||
|
||||
@callback code_change(old_vsn, state :: term, extra :: term) :: {:ok, new_state :: term}
|
||||
when old_vsn: term | {:down, term}
|
||||
|
||||
@type on_start :: {:ok, pid} | {:error, {:already_started, pid}}
|
||||
|
||||
@type name :: atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@type options :: [name: name]
|
||||
|
||||
@type manager :: pid | name | {atom, node}
|
||||
|
||||
@type handler :: atom | {atom, term}
|
||||
|
||||
message = "Use one of the alternatives described in the documentation for the GenEvent module"
|
||||
|
||||
@deprecated message
|
||||
@doc false
|
||||
defmacro __using__(_) do
|
||||
deprecation_message =
|
||||
"the GenEvent module is deprecated, see its documentation for alternatives"
|
||||
|
||||
IO.warn(deprecation_message, __CALLER__)
|
||||
|
||||
quote location: :keep do
|
||||
@behaviour :gen_event
|
||||
|
||||
@doc false
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_event(_event, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(msg, state) do
|
||||
proc =
|
||||
case Process.info(self(), :registered_name) do
|
||||
{_, []} -> self()
|
||||
{_, name} -> name
|
||||
end
|
||||
|
||||
# We do this to trick Dialyzer to not complain about non-local returns.
|
||||
case :erlang.phash2(1, 1) do
|
||||
0 ->
|
||||
raise "attempted to call GenEvent #{inspect(proc)} but no handle_call/2 clause was provided"
|
||||
|
||||
1 ->
|
||||
{:remove_handler, {:bad_call, msg}}
|
||||
end
|
||||
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
|
||||
|
||||
defoverridable init: 1,
|
||||
handle_event: 2,
|
||||
handle_call: 2,
|
||||
handle_info: 2,
|
||||
terminate: 2,
|
||||
code_change: 3
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec start_link(options) :: on_start
|
||||
def start_link(options \\ []) when is_list(options) do
|
||||
do_start(:link, options)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec start(options) :: on_start
|
||||
def start(options \\ []) when is_list(options) do
|
||||
do_start(:nolink, options)
|
||||
end
|
||||
|
||||
@no_callback :"no callback module"
|
||||
|
||||
defp do_start(mode, options) do
|
||||
case Keyword.get(options, :name) do
|
||||
nil ->
|
||||
:gen.start(GenEvent, mode, @no_callback, [], [])
|
||||
|
||||
atom when is_atom(atom) ->
|
||||
:gen.start(GenEvent, mode, {:local, atom}, @no_callback, [], [])
|
||||
|
||||
{:global, _term} = tuple ->
|
||||
:gen.start(GenEvent, mode, tuple, @no_callback, [], [])
|
||||
|
||||
{:via, via_module, _term} = tuple when is_atom(via_module) ->
|
||||
:gen.start(GenEvent, mode, tuple, @no_callback, [], [])
|
||||
|
||||
other ->
|
||||
raise ArgumentError, """
|
||||
expected :name option to be one of the following:
|
||||
|
||||
* nil
|
||||
* atom
|
||||
* {:global, term}
|
||||
* {:via, module, term}
|
||||
|
||||
Got: #{inspect(other)}
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec stream(manager, keyword) :: GenEvent.Stream.t()
|
||||
def stream(manager, options \\ []) do
|
||||
%GenEvent.Stream{manager: manager, timeout: Keyword.get(options, :timeout, :infinity)}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec add_handler(manager, handler, term) :: :ok | {:error, term}
|
||||
def add_handler(manager, handler, args) do
|
||||
rpc(manager, {:add_handler, handler, args})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec add_mon_handler(manager, handler, term) :: :ok | {:error, term}
|
||||
def add_mon_handler(manager, handler, args) do
|
||||
rpc(manager, {:add_mon_handler, handler, args, self()})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec notify(manager, term) :: :ok
|
||||
def notify(manager, event)
|
||||
|
||||
def notify({:global, name}, msg) do
|
||||
try do
|
||||
:global.send(name, {:notify, msg})
|
||||
:ok
|
||||
catch
|
||||
_, _ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def notify({:via, mod, name}, msg) when is_atom(mod) do
|
||||
try do
|
||||
mod.send(name, {:notify, msg})
|
||||
:ok
|
||||
catch
|
||||
_, _ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def notify(manager, msg)
|
||||
when is_pid(manager)
|
||||
when is_atom(manager)
|
||||
when tuple_size(manager) == 2 and is_atom(elem(manager, 0)) and is_atom(elem(manager, 1)) do
|
||||
send(manager, {:notify, msg})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec sync_notify(manager, term) :: :ok
|
||||
def sync_notify(manager, event) do
|
||||
rpc(manager, {:sync_notify, event})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec ack_notify(manager, term) :: :ok
|
||||
def ack_notify(manager, event) do
|
||||
rpc(manager, {:ack_notify, event})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec call(manager, handler, term, timeout) :: term | {:error, term}
|
||||
def call(manager, handler, request, timeout \\ 5000) do
|
||||
try do
|
||||
:gen.call(manager, self(), {:call, handler, request}, timeout)
|
||||
catch
|
||||
:exit, reason ->
|
||||
exit({reason, {__MODULE__, :call, [manager, handler, request, timeout]}})
|
||||
else
|
||||
{:ok, res} -> res
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec remove_handler(manager, handler, term) :: term | {:error, term}
|
||||
def remove_handler(manager, handler, args) do
|
||||
rpc(manager, {:delete_handler, handler, args})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec swap_handler(manager, handler, term, handler, term) :: :ok | {:error, term}
|
||||
def swap_handler(manager, handler1, args1, handler2, args2) do
|
||||
rpc(manager, {:swap_handler, handler1, args1, handler2, args2})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec swap_mon_handler(manager, handler, term, handler, term) :: :ok | {:error, term}
|
||||
def swap_mon_handler(manager, handler1, args1, handler2, args2) do
|
||||
rpc(manager, {:swap_mon_handler, handler1, args1, handler2, args2, self()})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec which_handlers(manager) :: [handler]
|
||||
def which_handlers(manager) do
|
||||
rpc(manager, :which_handlers)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
@spec stop(manager, reason :: term, timeout) :: :ok
|
||||
def stop(manager, reason \\ :normal, timeout \\ :infinity) do
|
||||
:gen.stop(manager, reason, timeout)
|
||||
end
|
||||
|
||||
defp rpc(module, cmd) do
|
||||
{:ok, reply} = :gen.call(module, self(), cmd, :infinity)
|
||||
reply
|
||||
end
|
||||
|
||||
## Init callbacks
|
||||
|
||||
require Record
|
||||
Record.defrecordp(:handler, [:module, :id, :state, :pid, :ref])
|
||||
|
||||
@doc false
|
||||
def init_it(starter, :self, name, mod, args, options) do
|
||||
init_it(starter, self(), name, mod, args, options)
|
||||
end
|
||||
|
||||
def init_it(starter, parent, name, _mod, _args, options) do
|
||||
Process.put(:"$initial_call", {__MODULE__, :init_it, 6})
|
||||
debug = :gen.debug_options(name, options)
|
||||
:proc_lib.init_ack(starter, {:ok, self()})
|
||||
loop(parent, name(name), [], debug, false)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def init_hib(parent, name, handlers, debug) do
|
||||
fetch_msg(parent, name, handlers, debug, true)
|
||||
end
|
||||
|
||||
defp name({:local, name}), do: name
|
||||
defp name({:global, name}), do: name
|
||||
defp name({:via, _, name}), do: name
|
||||
defp name(pid) when is_pid(pid), do: pid
|
||||
|
||||
## Loop
|
||||
|
||||
defp loop(parent, name, handlers, debug, true) do
|
||||
:proc_lib.hibernate(__MODULE__, :init_hib, [parent, name, handlers, debug])
|
||||
end
|
||||
|
||||
defp loop(parent, name, handlers, debug, false) do
|
||||
fetch_msg(parent, name, handlers, debug, false)
|
||||
end
|
||||
|
||||
defp fetch_msg(parent, name, handlers, debug, hib) do
|
||||
receive do
|
||||
{:system, from, req} ->
|
||||
:sys.handle_system_msg(req, from, parent, __MODULE__, debug, [name, handlers, hib], hib)
|
||||
|
||||
{:EXIT, ^parent, reason} ->
|
||||
server_terminate(reason, parent, handlers, name)
|
||||
|
||||
msg when debug == [] ->
|
||||
handle_msg(msg, parent, name, handlers, [])
|
||||
|
||||
msg ->
|
||||
debug = :sys.handle_debug(debug, &print_event/3, name, {:in, msg})
|
||||
handle_msg(msg, parent, name, handlers, debug)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_msg(msg, parent, name, handlers, debug) do
|
||||
case msg do
|
||||
{:notify, event} ->
|
||||
{hib, handlers} = server_event(:async, event, handlers, name)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, _tag, {:notify, event}} ->
|
||||
{hib, handlers} = server_event(:async, event, handlers, name)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, tag, {:ack_notify, event}} ->
|
||||
reply(tag, :ok)
|
||||
{hib, handlers} = server_event(:ack, event, handlers, name)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, tag, {:sync_notify, event}} ->
|
||||
{hib, handlers} = server_event(:sync, event, handlers, name)
|
||||
reply(tag, :ok)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{:DOWN, ref, :process, _pid, reason} = other ->
|
||||
case handle_down(ref, reason, handlers, name) do
|
||||
{:ok, handlers} ->
|
||||
loop(parent, name, handlers, debug, false)
|
||||
|
||||
:error ->
|
||||
{hib, handlers} = server_info(other, handlers, name)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
end
|
||||
|
||||
{_from, tag, {:call, handler, query}} ->
|
||||
{hib, reply, handlers} = server_call(handler, query, handlers, name)
|
||||
reply(tag, reply)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, tag, {:add_handler, handler, args}} ->
|
||||
{hib, reply, handlers} = server_add_handler(handler, args, handlers)
|
||||
reply(tag, reply)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, tag, {:add_mon_handler, handler, args, notify}} ->
|
||||
{hib, reply, handlers} = server_add_mon_handler(handler, args, handlers, notify)
|
||||
reply(tag, reply)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, tag, {:add_process_handler, pid, notify}} ->
|
||||
{hib, reply, handlers} = server_add_process_handler(pid, handlers, notify)
|
||||
reply(tag, reply)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, tag, {:delete_handler, handler, args}} ->
|
||||
{reply, handlers} = server_remove_handler(handler, args, handlers, name)
|
||||
reply(tag, reply)
|
||||
loop(parent, name, handlers, debug, false)
|
||||
|
||||
{_from, tag, {:swap_handler, handler1, args1, handler2, args2}} ->
|
||||
{hib, reply, handlers} =
|
||||
server_swap_handler(handler1, args1, handler2, args2, handlers, nil, name)
|
||||
|
||||
reply(tag, reply)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, tag, {:swap_mon_handler, handler1, args1, handler2, args2, mon}} ->
|
||||
{hib, reply, handlers} =
|
||||
server_swap_handler(handler1, args1, handler2, args2, handlers, mon, name)
|
||||
|
||||
reply(tag, reply)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
|
||||
{_from, tag, :which_handlers} ->
|
||||
reply(tag, server_which_handlers(handlers))
|
||||
loop(parent, name, handlers, debug, false)
|
||||
|
||||
{_from, tag, :get_modules} ->
|
||||
reply(tag, server_get_modules(handlers))
|
||||
loop(parent, name, handlers, debug, false)
|
||||
|
||||
other ->
|
||||
{hib, handlers} = server_info(other, handlers, name)
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
end
|
||||
end
|
||||
|
||||
## System callbacks
|
||||
|
||||
@doc false
|
||||
def system_continue(parent, debug, [name, handlers, hib]) do
|
||||
loop(parent, name, handlers, debug, hib)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def system_terminate(reason, parent, _debug, [name, handlers, _hib]) do
|
||||
server_terminate(reason, parent, handlers, name)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def system_code_change([name, handlers, hib], module, old_vsn, extra) do
|
||||
handlers =
|
||||
for handler <- handlers do
|
||||
if handler(handler, :module) == module do
|
||||
{:ok, state} = module.code_change(old_vsn, handler(handler, :state), extra)
|
||||
handler(handler, state: state)
|
||||
else
|
||||
handler
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, [name, handlers, hib]}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def system_get_state([_name, handlers, _hib]) do
|
||||
tuples =
|
||||
for handler(module: mod, id: id, state: state) <- handlers do
|
||||
{mod, id, state}
|
||||
end
|
||||
|
||||
{:ok, tuples}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def system_replace_state(fun, [name, handlers, hib]) do
|
||||
{handlers, states} =
|
||||
:lists.unzip(
|
||||
for handler <- handlers do
|
||||
handler(module: mod, id: id, state: state) = handler
|
||||
cur = {mod, id, state}
|
||||
|
||||
try do
|
||||
new = {^mod, ^id, new_state} = fun.(cur)
|
||||
{handler(handler, state: new_state), new}
|
||||
catch
|
||||
_, _ ->
|
||||
{handler, cur}
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
{:ok, states, [name, handlers, hib]}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_status(opt, status_data) do
|
||||
[pdict, sys_state, parent, _debug, [name, handlers, _hib]] = status_data
|
||||
header = :gen.format_status_header('Status for event handler', name)
|
||||
|
||||
formatted =
|
||||
for handler <- handlers do
|
||||
handler(module: module, state: state) = handler
|
||||
|
||||
if function_exported?(module, :format_status, 2) do
|
||||
try do
|
||||
state = module.format_status(opt, [pdict, state])
|
||||
handler(handler, state: state)
|
||||
catch
|
||||
_, _ -> handler
|
||||
end
|
||||
else
|
||||
handler
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
header: header,
|
||||
data: [{'Status', sys_state}, {'Parent', parent}],
|
||||
items: {'Installed handlers', formatted}
|
||||
]
|
||||
end
|
||||
|
||||
## Loop helpers
|
||||
|
||||
defp print_event(dev, {:in, msg}, name) do
|
||||
case msg do
|
||||
{:notify, event} ->
|
||||
IO.puts(dev, "*DBG* #{inspect(name)} got event #{inspect(event)}")
|
||||
|
||||
{_, _, {:call, handler, query}} ->
|
||||
IO.puts(
|
||||
dev,
|
||||
"*DBG* #{inspect(name)} (handler #{inspect(handler)}) got call #{inspect(query)}"
|
||||
)
|
||||
|
||||
_ ->
|
||||
IO.puts(dev, "*DBG* #{inspect(name)} got #{inspect(msg)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp print_event(dev, dbg, name) do
|
||||
IO.puts(dev, "*DBG* #{inspect(name)}: #{inspect(dbg)}")
|
||||
end
|
||||
|
||||
defp server_add_handler({module, id}, args, handlers) do
|
||||
handler = handler(module: module, id: {module, id})
|
||||
do_add_handler(module, handler, args, handlers, :ok)
|
||||
end
|
||||
|
||||
defp server_add_handler(module, args, handlers) do
|
||||
handler = handler(module: module, id: module)
|
||||
do_add_handler(module, handler, args, handlers, :ok)
|
||||
end
|
||||
|
||||
defp server_add_mon_handler({module, id}, args, handlers, notify) do
|
||||
ref = Process.monitor(notify)
|
||||
handler = handler(module: module, id: {module, id}, pid: notify, ref: ref)
|
||||
do_add_handler(module, handler, args, handlers, :ok)
|
||||
end
|
||||
|
||||
defp server_add_mon_handler(module, args, handlers, notify) do
|
||||
ref = Process.monitor(notify)
|
||||
handler = handler(module: module, id: module, pid: notify, ref: ref)
|
||||
do_add_handler(module, handler, args, handlers, :ok)
|
||||
end
|
||||
|
||||
defp server_add_process_handler(pid, handlers, notify) do
|
||||
ref = Process.monitor(pid)
|
||||
handler = handler(module: GenEvent.Stream, id: {self(), ref}, pid: notify, ref: ref)
|
||||
do_add_handler(GenEvent.Stream, handler, {pid, ref}, handlers, {self(), ref})
|
||||
end
|
||||
|
||||
defp server_remove_handler(module, args, handlers, name) do
|
||||
do_take_handler(module, args, handlers, name, :remove, :normal)
|
||||
end
|
||||
|
||||
defp server_swap_handler(module1, args1, module2, args2, handlers, sup, name) do
|
||||
{state, handlers} =
|
||||
do_take_handler(module1, args1, handlers, name, :swapped, {:swapped, module2, sup})
|
||||
|
||||
if sup do
|
||||
server_add_mon_handler(module2, {args2, state}, handlers, sup)
|
||||
else
|
||||
server_add_handler(module2, {args2, state}, handlers)
|
||||
end
|
||||
end
|
||||
|
||||
defp server_info(event, handlers, name) do
|
||||
handlers = :lists.reverse(handlers)
|
||||
server_notify(event, :handle_info, handlers, name, handlers, [], false)
|
||||
end
|
||||
|
||||
defp server_event(mode, event, handlers, name) do
|
||||
{handlers, streams} = server_split_process_handlers(mode, event, handlers, [], [])
|
||||
{hib, handlers} = server_notify(event, :handle_event, handlers, name, handlers, [], false)
|
||||
{hib, server_collect_process_handlers(mode, event, streams, handlers, name)}
|
||||
end
|
||||
|
||||
defp server_split_process_handlers(mode, event, [handler | t], handlers, streams) do
|
||||
case handler(handler, :id) do
|
||||
{pid, _ref} when is_pid(pid) ->
|
||||
server_process_notify(mode, event, handler)
|
||||
server_split_process_handlers(mode, event, t, handlers, [handler | streams])
|
||||
|
||||
_ ->
|
||||
server_split_process_handlers(mode, event, t, [handler | handlers], streams)
|
||||
end
|
||||
end
|
||||
|
||||
defp server_split_process_handlers(_mode, _event, [], handlers, streams) do
|
||||
{handlers, streams}
|
||||
end
|
||||
|
||||
defp server_process_notify(mode, event, handler(state: {pid, ref})) do
|
||||
send(pid, {self(), {self(), ref}, {mode_to_tag(mode), event}})
|
||||
end
|
||||
|
||||
defp mode_to_tag(:ack), do: :ack_notify
|
||||
defp mode_to_tag(:sync), do: :sync_notify
|
||||
defp mode_to_tag(:async), do: :notify
|
||||
|
||||
defp server_notify(event, fun, [handler | t], name, handlers, acc, hib) do
|
||||
case server_update(handler, fun, event, name, handlers) do
|
||||
{new_hib, handler} ->
|
||||
server_notify(event, fun, t, name, handlers, [handler | acc], hib or new_hib)
|
||||
|
||||
:error ->
|
||||
server_notify(event, fun, t, name, handlers, acc, hib)
|
||||
end
|
||||
end
|
||||
|
||||
defp server_notify(_, _, [], _, _, acc, hib) do
|
||||
{hib, acc}
|
||||
end
|
||||
|
||||
defp server_update(handler, fun, event, name, _handlers) do
|
||||
handler(module: module, state: state) = handler
|
||||
|
||||
case do_handler(module, fun, [event, state]) do
|
||||
{:ok, res} ->
|
||||
case res do
|
||||
{:ok, state} ->
|
||||
{false, handler(handler, state: state)}
|
||||
|
||||
{:ok, state, :hibernate} ->
|
||||
{true, handler(handler, state: state)}
|
||||
|
||||
:remove_handler ->
|
||||
do_terminate(handler, :remove_handler, event, name, :normal)
|
||||
:error
|
||||
|
||||
other ->
|
||||
reason = {:bad_return_value, other}
|
||||
do_terminate(handler, {:error, reason}, event, name, reason)
|
||||
:error
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
do_terminate(handler, {:error, reason}, event, name, reason)
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp server_collect_process_handlers(:async, event, [handler | t], handlers, name) do
|
||||
server_collect_process_handlers(:async, event, t, [handler | handlers], name)
|
||||
end
|
||||
|
||||
defp server_collect_process_handlers(mode, event, [handler | t], handlers, name)
|
||||
when mode in [:sync, :ack] do
|
||||
handler(ref: ref, id: id) = handler
|
||||
|
||||
receive do
|
||||
{^ref, :ok} ->
|
||||
server_collect_process_handlers(mode, event, t, [handler | handlers], name)
|
||||
|
||||
{_from, tag, {:delete_handler, ^id, args}} ->
|
||||
do_terminate(handler, args, :remove, name, :normal)
|
||||
reply(tag, :ok)
|
||||
server_collect_process_handlers(mode, event, t, handlers, name)
|
||||
|
||||
{:DOWN, ^ref, _, _, reason} ->
|
||||
do_terminate(handler, {:stop, reason}, :DOWN, name, :shutdown)
|
||||
server_collect_process_handlers(mode, event, t, handlers, name)
|
||||
end
|
||||
end
|
||||
|
||||
defp server_collect_process_handlers(_mode, _event, [], handlers, _name) do
|
||||
handlers
|
||||
end
|
||||
|
||||
defp server_call(module, query, handlers, name) do
|
||||
case :lists.keyfind(module, handler(:id) + 1, handlers) do
|
||||
false ->
|
||||
{false, {:error, :not_found}, handlers}
|
||||
|
||||
handler ->
|
||||
case server_call_update(handler, query, name, handlers) do
|
||||
{{hib, handler}, reply} ->
|
||||
{hib, reply, :lists.keyreplace(module, handler(:id) + 1, handlers, handler)}
|
||||
|
||||
{:error, reply} ->
|
||||
{false, reply, :lists.keydelete(module, handler(:id) + 1, handlers)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp server_call_update(handler, query, name, _handlers) do
|
||||
handler(module: module, state: state) = handler
|
||||
|
||||
case do_handler(module, :handle_call, [query, state]) do
|
||||
{:ok, res} ->
|
||||
case res do
|
||||
{:ok, reply, state} ->
|
||||
{{false, handler(handler, state: state)}, reply}
|
||||
|
||||
{:ok, reply, state, :hibernate} ->
|
||||
{{true, handler(handler, state: state)}, reply}
|
||||
|
||||
{:remove_handler, reply} ->
|
||||
do_terminate(handler, :remove_handler, query, name, :normal)
|
||||
{:error, reply}
|
||||
|
||||
other ->
|
||||
reason = {:bad_return_value, other}
|
||||
do_terminate(handler, {:error, reason}, query, name, reason)
|
||||
{:error, {:error, reason}}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
do_terminate(handler, {:error, reason}, query, name, reason)
|
||||
{:error, {:error, reason}}
|
||||
end
|
||||
end
|
||||
|
||||
defp server_get_modules(handlers) do
|
||||
for(handler(module: module) <- handlers, do: module)
|
||||
|> :ordsets.from_list()
|
||||
|> :ordsets.to_list()
|
||||
end
|
||||
|
||||
defp server_which_handlers(handlers) do
|
||||
for handler(id: id) <- handlers, do: id
|
||||
end
|
||||
|
||||
defp server_terminate(reason, _parent, handlers, name) do
|
||||
_ =
|
||||
for handler <- handlers do
|
||||
do_terminate(handler, :stop, :stop, name, :shutdown)
|
||||
end
|
||||
|
||||
exit(reason)
|
||||
end
|
||||
|
||||
defp reply({from, ref}, msg) do
|
||||
send(from, {ref, msg})
|
||||
end
|
||||
|
||||
defp handle_down(ref, reason, handlers, name) do
|
||||
case :lists.keyfind(ref, handler(:ref) + 1, handlers) do
|
||||
false ->
|
||||
:error
|
||||
|
||||
handler ->
|
||||
do_terminate(handler, {:stop, reason}, :DOWN, name, :shutdown)
|
||||
{:ok, :lists.keydelete(ref, handler(:ref) + 1, handlers)}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_add_handler(module, handler, arg, handlers, succ) do
|
||||
case :lists.keyfind(handler(handler, :id), handler(:id) + 1, handlers) do
|
||||
false ->
|
||||
case do_handler(module, :init, [arg]) do
|
||||
{:ok, res} ->
|
||||
case res do
|
||||
{:ok, state} ->
|
||||
{false, succ, [handler(handler, state: state) | handlers]}
|
||||
|
||||
{:ok, state, :hibernate} ->
|
||||
{true, succ, [handler(handler, state: state) | handlers]}
|
||||
|
||||
{:error, _} = error ->
|
||||
{false, error, handlers}
|
||||
|
||||
other ->
|
||||
{false, {:error, {:bad_return_value, other}}, handlers}
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
{false, error, handlers}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{false, {:error, :already_present}, handlers}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_take_handler(module, args, handlers, name, last_in, reason) do
|
||||
case :lists.keytake(module, handler(:id) + 1, handlers) do
|
||||
{:value, handler, handlers} ->
|
||||
{do_terminate(handler, args, last_in, name, reason), handlers}
|
||||
|
||||
false ->
|
||||
{{:error, :not_found}, handlers}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_terminate(handler, arg, last_in, name, reason) do
|
||||
handler(module: module, state: state) = handler
|
||||
|
||||
res =
|
||||
case do_handler(module, :terminate, [arg, state]) do
|
||||
{:ok, res} -> res
|
||||
{:error, _} = error -> error
|
||||
end
|
||||
|
||||
report_terminate(handler, reason, state, last_in, name)
|
||||
res
|
||||
end
|
||||
|
||||
defp do_handler(mod, fun, args) do
|
||||
try do
|
||||
apply(mod, fun, args)
|
||||
catch
|
||||
:throw, val -> {:ok, val}
|
||||
:error, val -> {:error, {val, __STACKTRACE__}}
|
||||
:exit, val -> {:error, val}
|
||||
else
|
||||
res -> {:ok, res}
|
||||
end
|
||||
end
|
||||
|
||||
defp report_terminate(handler, reason, state, last_in, name) do
|
||||
report_error(handler, reason, state, last_in, name)
|
||||
|
||||
if ref = handler(handler, :ref) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
end
|
||||
|
||||
if pid = handler(handler, :pid) do
|
||||
send(pid, {:gen_event_EXIT, handler(handler, :id), reason})
|
||||
end
|
||||
end
|
||||
|
||||
defp report_error(_handler, :normal, _, _, _), do: :ok
|
||||
defp report_error(_handler, :shutdown, _, _, _), do: :ok
|
||||
defp report_error(_handler, {:swapped, _, _}, _, _, _), do: :ok
|
||||
|
||||
defp report_error(handler, reason, state, last_in, name) do
|
||||
reason =
|
||||
case reason do
|
||||
{:undef, [{m, f, a, _} | _] = mfas} ->
|
||||
cond do
|
||||
:code.is_loaded(m) == false ->
|
||||
{:"module could not be loaded", mfas}
|
||||
|
||||
function_exported?(m, f, length(a)) ->
|
||||
reason
|
||||
|
||||
true ->
|
||||
{:"function not exported", mfas}
|
||||
end
|
||||
|
||||
_ ->
|
||||
reason
|
||||
end
|
||||
|
||||
formatted = report_status(handler, state)
|
||||
|
||||
:error_logger.error_msg(
|
||||
'** gen_event handler ~p crashed.~n' ++
|
||||
'** Was installed in ~p~n' ++
|
||||
'** Last event was: ~p~n' ++ '** When handler state == ~p~n' ++ '** Reason == ~p~n',
|
||||
[handler(handler, :id), name, last_in, formatted, reason]
|
||||
)
|
||||
end
|
||||
|
||||
defp report_status(handler(module: module), state) do
|
||||
if function_exported?(module, :format_status, 2) do
|
||||
try do
|
||||
module.format_status(:terminate, [Process.get(), state])
|
||||
catch
|
||||
_, _ -> state
|
||||
end
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,171 +0,0 @@
|
||||
defmodule GenEvent.Stream do
|
||||
@moduledoc false
|
||||
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
|
||||
# We do this to trick Dialyzer to not complain about non-local returns.
|
||||
case :erlang.phash2(1, 1) do
|
||||
0 -> exit({:bad_event, event})
|
||||
1 -> :remove_handler
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(msg, _state) do
|
||||
# We do this to trick Dialyzer to not complain about non-local returns.
|
||||
reason = {:bad_call, msg}
|
||||
|
||||
case :erlang.phash2(1, 1) do
|
||||
0 -> exit(reason)
|
||||
1 -> {:remove_handler, reason}
|
||||
end
|
||||
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
|
||||
|
||||
def slice(_stream) 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
|
||||
_ = :gen_event.delete_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} ->
|
||||
Process.demonitor(mon_ref, [:flush])
|
||||
:ok
|
||||
|
||||
{: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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,309 +0,0 @@
|
||||
defmodule HashDict do
|
||||
@moduledoc """
|
||||
Tuple-based HashDict implementation.
|
||||
|
||||
This module is deprecated. Use the `Map` module instead.
|
||||
"""
|
||||
|
||||
@moduledoc deprecated: "Use Map instead"
|
||||
|
||||
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}
|
||||
|
||||
message = "Use maps and the Map module instead"
|
||||
|
||||
@doc """
|
||||
Creates a new empty dict.
|
||||
"""
|
||||
@spec new :: Dict.t()
|
||||
@deprecated message
|
||||
def new do
|
||||
%HashDict{}
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
def update(%HashDict{root: root, size: size}, key, default, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> default end, fun, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def fetch(%HashDict{root: root}, key) do
|
||||
do_fetch(root, key, key_hash(key))
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def delete(dict, key) do
|
||||
case dict_delete(dict, key) do
|
||||
{dict, _value} -> dict
|
||||
:error -> dict
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def pop(dict, key, default \\ nil) do
|
||||
case dict_delete(dict, key) do
|
||||
{dict, value} -> {value, dict}
|
||||
:error -> {default, dict}
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def size(%HashDict{size: size}) do
|
||||
size
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated message
|
||||
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, default, fun, hash) do
|
||||
index = key_mask(hash)
|
||||
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{put_elem(node, index, [key | default.()]), 1}
|
||||
|
||||
[^key | value] ->
|
||||
{put_elem(node, index, [key | fun.(value)]), 0}
|
||||
|
||||
[k | v] ->
|
||||
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | default.()])
|
||||
{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, default, 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
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = HashDict
|
||||
module.reduce(dict, acc, fun)
|
||||
end
|
||||
|
||||
def member?(dict, {key, value}) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = HashDict
|
||||
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
|
||||
end
|
||||
|
||||
def member?(_dict, _) do
|
||||
{:ok, false}
|
||||
end
|
||||
|
||||
def count(dict) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = HashDict
|
||||
{:ok, module.size(dict)}
|
||||
end
|
||||
|
||||
def slice(_dict) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: HashDict do
|
||||
def into(original) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = HashDict
|
||||
|
||||
collector_fun = fn
|
||||
dict, {:cont, {key, value}} -> module.put(dict, key, value)
|
||||
dict, :done -> dict
|
||||
_, :halt -> :ok
|
||||
end
|
||||
|
||||
{original, collector_fun}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: HashDict do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(dict, opts) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = HashDict
|
||||
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
|
||||
end
|
||||
end
|
||||
@@ -1,319 +0,0 @@
|
||||
defmodule HashSet do
|
||||
@moduledoc """
|
||||
Tuple-based HashSet implementation.
|
||||
|
||||
This module is deprecated. Use the `MapSet` module instead.
|
||||
"""
|
||||
|
||||
@moduledoc deprecated: "Use MapSet instead"
|
||||
|
||||
@node_bitmap 0b111
|
||||
@node_shift 3
|
||||
@node_size 8
|
||||
@node_template :erlang.make_tuple(@node_size, [])
|
||||
|
||||
message = "Use the MapSet module instead"
|
||||
|
||||
@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}
|
||||
|
||||
@deprecated message
|
||||
@spec new :: Set.t()
|
||||
def new do
|
||||
%HashSet{}
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
def union(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
set_fold(set2, set1, fn v, acc -> put(acc, v) end)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
def difference(%HashSet{} = set1, %HashSet{} = set2) do
|
||||
set_fold(set2, set1, fn v, acc -> delete(acc, v) end)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def to_list(set) do
|
||||
set_fold(set, [], &[&1 | &2]) |> :lists.reverse()
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def equal?(%HashSet{size: size1} = set1, %HashSet{size: size2} = set2) do
|
||||
case size1 do
|
||||
^size2 -> subset?(set1, set2)
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
def member?(%HashSet{root: root}, term) do
|
||||
do_member?(root, term, key_hash(term))
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
|
||||
@deprecated message
|
||||
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
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = HashSet
|
||||
module.reduce(set, acc, fun)
|
||||
end
|
||||
|
||||
def member?(set, term) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = HashSet
|
||||
{:ok, module.member?(set, term)}
|
||||
end
|
||||
|
||||
def count(set) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = HashSet
|
||||
{:ok, module.size(set)}
|
||||
end
|
||||
|
||||
def slice(_set) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: HashSet do
|
||||
def into(original) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = HashSet
|
||||
|
||||
collector_fun = fn
|
||||
set, {:cont, term} -> module.put(set, term)
|
||||
set, :done -> set
|
||||
_, :halt -> :ok
|
||||
end
|
||||
|
||||
{original, collector_fun}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: HashSet do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(set, opts) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = HashSet
|
||||
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
|
||||
end
|
||||
end
|
||||
@@ -1,644 +0,0 @@
|
||||
import Kernel, except: [inspect: 1]
|
||||
import Inspect.Algebra
|
||||
|
||||
alias Code.Identifier
|
||||
|
||||
defprotocol Inspect do
|
||||
@moduledoc """
|
||||
The `Inspect` protocol converts an Elixir data structure into an
|
||||
algebra document.
|
||||
|
||||
This is typically done when you want to customize how your own
|
||||
structs are inspected in logs and the terminal.
|
||||
|
||||
This documentation refers to implementing the `Inspect` protocol
|
||||
for your own data structures. To learn more about using inspect,
|
||||
see `Kernel.inspect/2` and `IO.inspect/2`.
|
||||
|
||||
## Inspect representation
|
||||
|
||||
There are typically three choices of inspect representation. In order
|
||||
to understand them, let's imagine we have the following `User` struct:
|
||||
|
||||
defmodule User do
|
||||
defstruct [:id, :name, :address]
|
||||
end
|
||||
|
||||
Our choices are:
|
||||
|
||||
1. Print the struct using Elixir's struct syntax, for example:
|
||||
`%User{address: "Earth", id: 13, name: "Jane"}`. This is the
|
||||
default representation and best choice if all struct fields
|
||||
are public.
|
||||
|
||||
2. Print using the `#User<...>` notation, for example: `#User<id: 13, name: "Jane", ...>`.
|
||||
This notation does not emit valid Elixir code and is typically
|
||||
used when the struct has private fields (for example, you may want
|
||||
to hide the field `:address` to redact person identifiable information).
|
||||
|
||||
3. Print the struct using the expression syntax, for example:
|
||||
`User.new(13, "Jane", "Earth")`. This assumes there is a `User.new/3`
|
||||
function. This option is mostly used as an alternative to option 2
|
||||
for representing custom data structures, such as `MapSet`, `Date.Range`,
|
||||
and others.
|
||||
|
||||
You can implement the Inspect protocol for your own structs while
|
||||
adhering to the conventions above. Option 1 is the default representation
|
||||
and you can quickly achieve option 2 by deriving the `Inspect` protocol.
|
||||
For option 3, you need your custom implementation.
|
||||
|
||||
## Deriving
|
||||
|
||||
The `Inspect` protocol can be derived to customize the order of fields
|
||||
(the default is alphabetical) and hide certain fields from structs,
|
||||
so they don't show up in logs, inspects and similar. The latter is
|
||||
especially useful for fields containing private information.
|
||||
|
||||
The supported options are:
|
||||
|
||||
* `:only` - only include the given fields when inspecting.
|
||||
|
||||
* `:except` - remove the given fields when inspecting.
|
||||
|
||||
* `:optional` - (since v1.14.0) do not include a field if it
|
||||
matches its default value. This can be used to simplify the
|
||||
struct representation at the cost of hiding information.
|
||||
|
||||
Whenever `:only` or `:except` are used to restrict fields,
|
||||
the struct will be printed using the `#User<...>` notation,
|
||||
as the struct can no longer be copy and pasted as valid Elixir
|
||||
code. Let's see an example:
|
||||
|
||||
defmodule User do
|
||||
@derive {Inspect, only: [:id, :name]}
|
||||
defstruct [:id, :name, :address]
|
||||
end
|
||||
|
||||
inspect(%User{id: 1, name: "Jane", address: "Earth"})
|
||||
#=> #User<id: 1, name: "Jane", ...>
|
||||
|
||||
If you use only the `:optional` option, the struct will still be
|
||||
printed as `%User{...}`.
|
||||
|
||||
## Custom implementation
|
||||
|
||||
You can also define your custom protocol implementation by
|
||||
defining the `inspect/2` function. The function receives the
|
||||
entity to be inspected followed by the inspecting options,
|
||||
represented by the struct `Inspect.Opts`. Building of the
|
||||
algebra document is done with `Inspect.Algebra`.
|
||||
|
||||
Many times, inspecting a structure can be implemented in function
|
||||
of existing entities. For example, here is `MapSet`'s `inspect/2`
|
||||
implementation:
|
||||
|
||||
defimpl Inspect, for: MapSet do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(map_set, opts) do
|
||||
concat(["MapSet.new(", Inspect.List.inspect(MapSet.to_list(map_set), opts), ")"])
|
||||
end
|
||||
end
|
||||
|
||||
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from
|
||||
`Inspect.Algebra` and it concatenates algebra documents together.
|
||||
In the example above it is concatenating the string `"MapSet.new("`,
|
||||
the document returned by `Inspect.Algebra.to_doc/2`, and the final
|
||||
string `")"`. Therefore, the MapSet with the numbers 1, 2, and 3
|
||||
will be printed as:
|
||||
|
||||
iex> MapSet.new([1, 2, 3], fn x -> x * 2 end)
|
||||
MapSet.new([2, 4, 6])
|
||||
|
||||
In other words, `MapSet`'s inspect representation returns an expression
|
||||
that, when evaluated, builds the `MapSet` itself.
|
||||
|
||||
### Error handling
|
||||
|
||||
In case there is an error while your structure is being inspected,
|
||||
Elixir will raise an `ArgumentError` error and will automatically fall back
|
||||
to a raw representation for printing the structure.
|
||||
|
||||
You can however access the underlying error by invoking the `Inspect`
|
||||
implementation directly. For example, to test `Inspect.MapSet` above,
|
||||
you can invoke it as:
|
||||
|
||||
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
|
||||
|
||||
"""
|
||||
|
||||
# Handle structs in Any
|
||||
@fallback_to_any true
|
||||
|
||||
@doc """
|
||||
Converts `term` into an algebra document.
|
||||
|
||||
This function shouldn't be invoked directly, unless when implementing
|
||||
a custom `inspect_fun` to be given to `Inspect.Opts`. Everywhere else,
|
||||
`Inspect.Algebra.to_doc/2` should be preferred as it handles structs
|
||||
and exceptions.
|
||||
"""
|
||||
@spec inspect(t, Inspect.Opts.t()) :: Inspect.Algebra.t()
|
||||
def inspect(term, opts)
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Atom do
|
||||
require Macro
|
||||
|
||||
def inspect(atom, opts) do
|
||||
color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
|
||||
end
|
||||
|
||||
defp color_key(atom) when is_boolean(atom), do: :boolean
|
||||
defp color_key(nil), do: nil
|
||||
defp color_key(_), do: :atom
|
||||
end
|
||||
|
||||
defimpl Inspect, for: BitString do
|
||||
def inspect(term, opts) when is_binary(term) do
|
||||
%Inspect.Opts{binaries: bins, base: base, printable_limit: printable_limit} = opts
|
||||
|
||||
if base == :decimal and
|
||||
(bins == :as_strings or (bins == :infer and String.printable?(term, printable_limit))) do
|
||||
inspected =
|
||||
case Identifier.escape(term, ?", printable_limit) do
|
||||
{escaped, ""} -> [?", escaped, ?"]
|
||||
{escaped, _} -> [?", escaped, ?", " <> ..."]
|
||||
end
|
||||
|
||||
color(IO.iodata_to_binary(inspected), :string, opts)
|
||||
else
|
||||
inspect_bitstring(term, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def inspect(term, opts) do
|
||||
inspect_bitstring(term, opts)
|
||||
end
|
||||
|
||||
defp inspect_bitstring("", opts) do
|
||||
color("<<>>", :binary, opts)
|
||||
end
|
||||
|
||||
defp inspect_bitstring(bitstring, opts) do
|
||||
left = color("<<", :binary, opts)
|
||||
right = color(">>", :binary, opts)
|
||||
inner = each_bit(bitstring, opts.limit, opts)
|
||||
group(concat(concat(left, nest(inner, 2)), right))
|
||||
end
|
||||
|
||||
defp each_bit(_, 0, _) do
|
||||
"..."
|
||||
end
|
||||
|
||||
defp each_bit(<<>>, _counter, _opts) do
|
||||
:doc_nil
|
||||
end
|
||||
|
||||
defp each_bit(<<h::8>>, _counter, opts) do
|
||||
Inspect.Integer.inspect(h, opts)
|
||||
end
|
||||
|
||||
defp each_bit(<<h, t::bitstring>>, counter, opts) do
|
||||
flex_glue(
|
||||
concat(Inspect.Integer.inspect(h, opts), ","),
|
||||
each_bit(t, decrement(counter), opts)
|
||||
)
|
||||
end
|
||||
|
||||
defp each_bit(bitstring, _counter, opts) do
|
||||
size = bit_size(bitstring)
|
||||
<<h::size(size)>> = bitstring
|
||||
concat(Inspect.Integer.inspect(h, opts), "::size(" <> Integer.to_string(size) <> ")")
|
||||
end
|
||||
|
||||
@compile {:inline, decrement: 1}
|
||||
defp decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
end
|
||||
|
||||
defimpl Inspect, for: List do
|
||||
def inspect([], opts) do
|
||||
color("[]", :list, opts)
|
||||
end
|
||||
|
||||
# TODO: Remove :char_list and :as_char_lists handling on v2.0
|
||||
def inspect(term, opts) do
|
||||
%Inspect.Opts{
|
||||
charlists: lists,
|
||||
char_lists: lists_deprecated,
|
||||
printable_limit: printable_limit
|
||||
} = opts
|
||||
|
||||
lists =
|
||||
if lists == :infer and lists_deprecated != :infer do
|
||||
case lists_deprecated do
|
||||
:as_char_lists ->
|
||||
IO.warn(
|
||||
"the :char_lists inspect option and its :as_char_lists " <>
|
||||
"value are deprecated, use the :charlists option and its " <>
|
||||
":as_charlists value instead"
|
||||
)
|
||||
|
||||
:as_charlists
|
||||
|
||||
_ ->
|
||||
IO.warn("the :char_lists inspect option is deprecated, use :charlists instead")
|
||||
lists_deprecated
|
||||
end
|
||||
else
|
||||
lists
|
||||
end
|
||||
|
||||
open = color("[", :list, opts)
|
||||
sep = color(",", :list, opts)
|
||||
close = color("]", :list, opts)
|
||||
|
||||
cond do
|
||||
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
|
||||
inspected =
|
||||
case Identifier.escape(IO.chardata_to_string(term), ?', printable_limit) do
|
||||
{escaped, ""} -> [?', escaped, ?']
|
||||
{escaped, _} -> [?', escaped, ?', " ++ ..."]
|
||||
end
|
||||
|
||||
color(IO.iodata_to_binary(inspected), :charlist, opts)
|
||||
|
||||
keyword?(term) ->
|
||||
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
|
||||
|
||||
true ->
|
||||
container_doc(open, term, close, opts, &to_doc/2, separator: sep)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def keyword({key, value}, opts) do
|
||||
key = color(Macro.inspect_atom(:key, key), :atom, opts)
|
||||
concat(key, concat(" ", to_doc(value, opts)))
|
||||
end
|
||||
|
||||
@doc false
|
||||
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
case Atom.to_charlist(key) do
|
||||
'Elixir.' ++ _ -> false
|
||||
_ -> keyword?(rest)
|
||||
end
|
||||
end
|
||||
|
||||
def keyword?([]), do: true
|
||||
def keyword?(_other), do: false
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Tuple do
|
||||
def inspect(tuple, opts) do
|
||||
open = color("{", :tuple, opts)
|
||||
sep = color(",", :tuple, opts)
|
||||
close = color("}", :tuple, opts)
|
||||
container_opts = [separator: sep, break: :flex]
|
||||
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Map do
|
||||
def inspect(map, opts) do
|
||||
list =
|
||||
if Keyword.get(opts.custom_options, :sort_maps) do
|
||||
map |> Map.to_list() |> :lists.sort()
|
||||
else
|
||||
Map.to_list(map)
|
||||
end
|
||||
|
||||
fun =
|
||||
if Inspect.List.keyword?(list) do
|
||||
&Inspect.List.keyword/2
|
||||
else
|
||||
sep = color(" => ", :map, opts)
|
||||
&to_assoc(&1, &2, sep)
|
||||
end
|
||||
|
||||
map_container_doc(list, "", opts, fun)
|
||||
end
|
||||
|
||||
def inspect(map, name, infos, opts) do
|
||||
fun = fn %{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts) end
|
||||
map_container_doc(infos, name, opts, fun)
|
||||
end
|
||||
|
||||
defp to_assoc({key, value}, opts, sep) do
|
||||
concat(concat(to_doc(key, opts), sep), to_doc(value, opts))
|
||||
end
|
||||
|
||||
defp map_container_doc(list, name, opts, fun) do
|
||||
open = color("%" <> name <> "{", :map, opts)
|
||||
sep = color(",", :map, opts)
|
||||
close = color("}", :map, opts)
|
||||
container_doc(open, list, close, opts, fun, separator: sep, break: :strict)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Integer do
|
||||
def inspect(term, %Inspect.Opts{base: base} = opts) do
|
||||
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
|
||||
color(inspected, :number, opts)
|
||||
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::binary>>, base) do
|
||||
"-" <> prepend_prefix(value, base)
|
||||
end
|
||||
|
||||
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(float, opts) do
|
||||
abs = abs(float)
|
||||
|
||||
formatted =
|
||||
if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do
|
||||
[Integer.to_string(trunc(float)), ?., ?0]
|
||||
else
|
||||
:io_lib_format.fwrite_g(float)
|
||||
end
|
||||
|
||||
color(IO.iodata_to_binary(formatted), :number, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Regex do
|
||||
def inspect(regex = %{opts: regex_opts}, opts) when is_list(regex_opts) do
|
||||
concat([
|
||||
"Regex.compile!(",
|
||||
Inspect.BitString.inspect(regex.source, opts),
|
||||
", ",
|
||||
Inspect.List.inspect(regex_opts, opts),
|
||||
")"
|
||||
])
|
||||
end
|
||||
|
||||
def inspect(regex, opts) do
|
||||
{escaped, _} =
|
||||
regex.source
|
||||
|> normalize(<<>>)
|
||||
|> Identifier.escape(?/, :infinity, &escape_map/1)
|
||||
|
||||
source = IO.iodata_to_binary(['~r/', escaped, ?/, regex.opts])
|
||||
color(source, :regex, opts)
|
||||
end
|
||||
|
||||
defp normalize(<<?\\, ?\\, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?\\, ?\\>>)
|
||||
defp normalize(<<?\\, ?/, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?/>>)
|
||||
defp normalize(<<char, rest::binary>>, acc), do: normalize(rest, <<acc::binary, char>>)
|
||||
defp normalize(<<>>, acc), do: acc
|
||||
|
||||
defp escape_map(?\a), do: '\\a'
|
||||
defp escape_map(?\f), do: '\\f'
|
||||
defp escape_map(?\n), do: '\\n'
|
||||
defp escape_map(?\r), do: '\\r'
|
||||
defp escape_map(?\t), do: '\\t'
|
||||
defp escape_map(?\v), do: '\\v'
|
||||
defp escape_map(_), do: false
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Function do
|
||||
def inspect(function, _opts) do
|
||||
fun_info = Function.info(function)
|
||||
mod = fun_info[:module]
|
||||
name = fun_info[:name]
|
||||
|
||||
cond do
|
||||
not is_atom(mod) ->
|
||||
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]}>"
|
||||
|
||||
fun_info[:type] == :external and fun_info[:env] == [] ->
|
||||
inspected_as_atom = Macro.inspect_atom(:literal, mod)
|
||||
inspected_as_function = Macro.inspect_atom(:remote_call, name)
|
||||
"&#{inspected_as_atom}.#{inspected_as_function}/#{fun_info[:arity]}"
|
||||
|
||||
match?('elixir_compiler_' ++ _, Atom.to_charlist(mod)) ->
|
||||
if function_exported?(mod, :__RELATIVE__, 0) do
|
||||
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
|
||||
else
|
||||
default_inspect(mod, fun_info)
|
||||
end
|
||||
|
||||
true ->
|
||||
default_inspect(mod, fun_info)
|
||||
end
|
||||
end
|
||||
|
||||
defp default_inspect(mod, fun_info) do
|
||||
inspected_as_atom = Macro.inspect_atom(:literal, mod)
|
||||
extracted_name = extract_name(fun_info[:name])
|
||||
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in #{inspected_as_atom}#{extracted_name}>"
|
||||
end
|
||||
|
||||
defp extract_name([]) do
|
||||
""
|
||||
end
|
||||
|
||||
defp extract_name(name) do
|
||||
case Identifier.extract_anonymous_fun_parent(name) do
|
||||
{name, arity} ->
|
||||
"." <> Macro.inspect_atom(:remote_call, name) <> "/" <> arity
|
||||
|
||||
:error ->
|
||||
"." <> Macro.inspect_atom(:remote_call, 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: Inspect.Error do
|
||||
@impl true
|
||||
def inspect(%{stacktrace: stacktrace} = inspect_error, _opts) do
|
||||
message = Exception.message(inspect_error)
|
||||
format_output(message, stacktrace)
|
||||
end
|
||||
|
||||
defp format_output(message, [_ | _] = stacktrace) do
|
||||
stacktrace = Exception.format_stacktrace(stacktrace)
|
||||
|
||||
"""
|
||||
#Inspect.Error<
|
||||
#{Inspect.Error.pad(message, 2)}
|
||||
|
||||
Stacktrace:
|
||||
|
||||
#{stacktrace}
|
||||
>\
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_output(message, []) do
|
||||
"""
|
||||
#Inspect.Error<
|
||||
#{Inspect.Error.pad(message, 2)}
|
||||
>\
|
||||
"""
|
||||
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
|
||||
defmacro __deriving__(module, struct, options) do
|
||||
fields = Map.keys(struct) -- [:__exception__, :__struct__]
|
||||
only = Keyword.get(options, :only, fields)
|
||||
except = Keyword.get(options, :except, [])
|
||||
optional = Keyword.get(options, :optional, [])
|
||||
|
||||
:ok = validate_option(:only, only, fields, module)
|
||||
:ok = validate_option(:except, except, fields, module)
|
||||
:ok = validate_option(:optional, optional, fields, module)
|
||||
|
||||
inspect_module =
|
||||
if fields == only and except == [] do
|
||||
Inspect.Map
|
||||
else
|
||||
Inspect.Any
|
||||
end
|
||||
|
||||
filtered_fields =
|
||||
fields
|
||||
|> Enum.reject(&(&1 in except))
|
||||
|> Enum.filter(&(&1 in only))
|
||||
|
||||
optional? =
|
||||
if optional == [] do
|
||||
false
|
||||
else
|
||||
optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)}
|
||||
|
||||
quote do
|
||||
case unquote(Macro.escape(optional_map)) do
|
||||
%{^var!(field) => var!(default)} ->
|
||||
var!(default) == Map.get(var!(struct), var!(field))
|
||||
|
||||
%{} ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
defimpl Inspect, for: unquote(module) do
|
||||
def inspect(var!(struct), var!(opts)) do
|
||||
var!(infos) =
|
||||
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
|
||||
var!(field) in unquote(filtered_fields) and not unquote(optional?),
|
||||
do: var!(info)
|
||||
|
||||
var!(name) = Macro.inspect_atom(:literal, unquote(module))
|
||||
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_option(option, option_list, fields, module) do
|
||||
case option_list -- fields do
|
||||
[] ->
|
||||
:ok
|
||||
|
||||
unknown_fields ->
|
||||
raise ArgumentError,
|
||||
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
|
||||
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
|
||||
end
|
||||
end
|
||||
|
||||
def inspect(%module{} = struct, opts) do
|
||||
try do
|
||||
{module.__struct__(), module.__info__(:struct)}
|
||||
rescue
|
||||
_ -> Inspect.Map.inspect(struct, opts)
|
||||
else
|
||||
{dunder, fields} ->
|
||||
if Map.keys(dunder) == Map.keys(struct) do
|
||||
infos =
|
||||
for %{field: field} = info <- fields,
|
||||
field not in [:__struct__, :__exception__],
|
||||
do: info
|
||||
|
||||
Inspect.Map.inspect(struct, Macro.inspect_atom(:literal, module), infos, opts)
|
||||
else
|
||||
Inspect.Map.inspect(struct, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def inspect(map, name, infos, opts) do
|
||||
open = color("#" <> name <> "<", :map, opts)
|
||||
sep = color(",", :map, opts)
|
||||
close = color(">", :map, opts)
|
||||
|
||||
fun = fn
|
||||
%{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts)
|
||||
:..., _opts -> "..."
|
||||
end
|
||||
|
||||
container_doc(open, infos ++ [:...], close, opts, fun, separator: sep, break: :strict)
|
||||
end
|
||||
end
|
||||
|
||||
require Protocol
|
||||
|
||||
Protocol.derive(
|
||||
Inspect,
|
||||
Macro.Env,
|
||||
only: [
|
||||
:module,
|
||||
:file,
|
||||
:line,
|
||||
:function,
|
||||
:context,
|
||||
:aliases,
|
||||
:requires,
|
||||
:functions,
|
||||
:macros,
|
||||
:macro_aliases,
|
||||
:context_modules,
|
||||
:lexical_tracker
|
||||
]
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,489 +0,0 @@
|
||||
defmodule Integer do
|
||||
@moduledoc """
|
||||
Functions for working with integers.
|
||||
|
||||
Some functions that work on integers are found in `Kernel`:
|
||||
|
||||
* `Kernel.abs/1`
|
||||
* `Kernel.div/2`
|
||||
* `Kernel.max/2`
|
||||
* `Kernel.min/2`
|
||||
* `Kernel.rem/2`
|
||||
|
||||
"""
|
||||
|
||||
import Bitwise
|
||||
|
||||
@doc """
|
||||
Determines if `integer` is odd.
|
||||
|
||||
Returns `true` if the given `integer` is an odd number,
|
||||
otherwise it returns `false`.
|
||||
|
||||
Allowed in guard clauses.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.is_odd(5)
|
||||
true
|
||||
|
||||
iex> Integer.is_odd(6)
|
||||
false
|
||||
|
||||
iex> Integer.is_odd(-5)
|
||||
true
|
||||
|
||||
iex> Integer.is_odd(0)
|
||||
false
|
||||
|
||||
"""
|
||||
defguard is_odd(integer) when is_integer(integer) and (integer &&& 1) == 1
|
||||
|
||||
@doc """
|
||||
Determines if an `integer` is even.
|
||||
|
||||
Returns `true` if the given `integer` is an even number,
|
||||
otherwise it returns `false`.
|
||||
|
||||
Allowed in guard clauses.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.is_even(10)
|
||||
true
|
||||
|
||||
iex> Integer.is_even(5)
|
||||
false
|
||||
|
||||
iex> Integer.is_even(-10)
|
||||
true
|
||||
|
||||
iex> Integer.is_even(0)
|
||||
true
|
||||
|
||||
"""
|
||||
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
|
||||
|
||||
@doc """
|
||||
Computes `base` raised to power of `exponent`.
|
||||
|
||||
Both `base` and `exponent` must be integers.
|
||||
The exponent must be zero or positive.
|
||||
|
||||
See `Float.pow/2` for exponentiation of negative
|
||||
exponents as well as floats.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.pow(2, 0)
|
||||
1
|
||||
iex> Integer.pow(2, 1)
|
||||
2
|
||||
iex> Integer.pow(2, 10)
|
||||
1024
|
||||
iex> Integer.pow(2, 11)
|
||||
2048
|
||||
iex> Integer.pow(2, 64)
|
||||
0x10000000000000000
|
||||
|
||||
iex> Integer.pow(3, 4)
|
||||
81
|
||||
iex> Integer.pow(4, 3)
|
||||
64
|
||||
|
||||
iex> Integer.pow(-2, 3)
|
||||
-8
|
||||
iex> Integer.pow(-2, 4)
|
||||
16
|
||||
|
||||
iex> Integer.pow(2, -2)
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec pow(integer, non_neg_integer) :: integer
|
||||
def pow(base, exponent) when is_integer(base) and is_integer(exponent) do
|
||||
if exponent < 0, do: :erlang.error(:badarith, [base, exponent])
|
||||
base ** exponent
|
||||
end
|
||||
|
||||
@doc """
|
||||
Computes the modulo remainder of an integer division.
|
||||
|
||||
This function performs a [floored division](`floor_div/2`), which means that
|
||||
the result will always have the sign of the `divisor`.
|
||||
|
||||
Raises an `ArithmeticError` exception if one of the arguments is not an
|
||||
integer, or when the `divisor` is `0`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.mod(5, 2)
|
||||
1
|
||||
iex> Integer.mod(6, -4)
|
||||
-2
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec mod(integer, neg_integer | pos_integer) :: integer
|
||||
def mod(dividend, divisor) do
|
||||
remainder = rem(dividend, divisor)
|
||||
|
||||
if remainder * divisor < 0 do
|
||||
remainder + divisor
|
||||
else
|
||||
remainder
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a floored integer division.
|
||||
|
||||
Raises an `ArithmeticError` exception if one of the arguments is not an
|
||||
integer, or when the `divisor` is `0`.
|
||||
|
||||
This function performs a *floored* integer division, which means that
|
||||
the result will always be rounded towards negative infinity.
|
||||
|
||||
If you want to perform truncated integer division (rounding towards zero),
|
||||
use `Kernel.div/2` instead.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.floor_div(5, 2)
|
||||
2
|
||||
iex> Integer.floor_div(6, -4)
|
||||
-2
|
||||
iex> Integer.floor_div(-99, 2)
|
||||
-50
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec floor_div(integer, neg_integer | pos_integer) :: integer
|
||||
def floor_div(dividend, divisor) do
|
||||
if dividend * divisor < 0 and rem(dividend, divisor) != 0 do
|
||||
div(dividend, divisor) - 1
|
||||
else
|
||||
div(dividend, divisor)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the ordered digits for the given `integer`.
|
||||
|
||||
An optional `base` value may be provided representing the radix for the returned
|
||||
digits. This one must be an integer >= 2.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.digits(123)
|
||||
[1, 2, 3]
|
||||
|
||||
iex> Integer.digits(170, 2)
|
||||
[1, 0, 1, 0, 1, 0, 1, 0]
|
||||
|
||||
iex> Integer.digits(-170, 2)
|
||||
[-1, 0, -1, 0, -1, 0, -1, 0]
|
||||
|
||||
"""
|
||||
@spec digits(integer, pos_integer) :: [integer, ...]
|
||||
def digits(integer, base \\ 10)
|
||||
when is_integer(integer) and is_integer(base) and base >= 2 do
|
||||
do_digits(integer, base, [])
|
||||
end
|
||||
|
||||
defp do_digits(integer, base, acc) when abs(integer) < base, do: [integer | acc]
|
||||
|
||||
defp do_digits(integer, base, acc),
|
||||
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
|
||||
|
||||
@doc """
|
||||
Returns the integer represented by the ordered `digits`.
|
||||
|
||||
An optional `base` value may be provided representing the radix for the `digits`.
|
||||
Base has to be an integer greater than or equal to `2`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.undigits([1, 2, 3])
|
||||
123
|
||||
|
||||
iex> Integer.undigits([1, 4], 16)
|
||||
20
|
||||
|
||||
iex> Integer.undigits([])
|
||||
0
|
||||
|
||||
"""
|
||||
@spec undigits([integer], pos_integer) :: integer
|
||||
def undigits(digits, base \\ 10) when is_list(digits) and is_integer(base) and base >= 2 do
|
||||
do_undigits(digits, base, 0)
|
||||
end
|
||||
|
||||
defp do_undigits([], _base, acc), do: acc
|
||||
|
||||
defp do_undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
|
||||
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
|
||||
|
||||
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
|
||||
do: do_undigits(tail, base, acc * base + digit)
|
||||
|
||||
@doc """
|
||||
Parses a text representation of an integer.
|
||||
|
||||
An optional `base` to the corresponding integer can be provided.
|
||||
If `base` is not given, 10 will be used.
|
||||
|
||||
If successful, returns a tuple in the form of `{integer, remainder_of_binary}`.
|
||||
Otherwise `:error`.
|
||||
|
||||
Raises an error if `base` is less than 2 or more than 36.
|
||||
|
||||
If you want to convert a string-formatted integer directly to an integer,
|
||||
`String.to_integer/1` or `String.to_integer/2` can be used instead.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.parse("34")
|
||||
{34, ""}
|
||||
|
||||
iex> Integer.parse("34.5")
|
||||
{34, ".5"}
|
||||
|
||||
iex> Integer.parse("three")
|
||||
:error
|
||||
|
||||
iex> Integer.parse("34", 10)
|
||||
{34, ""}
|
||||
|
||||
iex> Integer.parse("f4", 16)
|
||||
{244, ""}
|
||||
|
||||
iex> Integer.parse("Awww++", 36)
|
||||
{509216, "++"}
|
||||
|
||||
iex> Integer.parse("fab", 10)
|
||||
:error
|
||||
|
||||
iex> Integer.parse("a2", 38)
|
||||
** (ArgumentError) invalid base 38
|
||||
|
||||
"""
|
||||
@spec parse(binary, 2..36) :: {integer, remainder_of_binary :: binary} | :error
|
||||
def parse(binary, base \\ 10)
|
||||
|
||||
def parse(_binary, base) when base not in 2..36 do
|
||||
raise ArgumentError, "invalid base #{inspect(base)}"
|
||||
end
|
||||
|
||||
def parse(binary, base) when is_binary(binary) do
|
||||
case count_digits(binary, base) do
|
||||
0 ->
|
||||
:error
|
||||
|
||||
count ->
|
||||
{digits, rem} = :erlang.split_binary(binary, count)
|
||||
{:erlang.binary_to_integer(digits, base), rem}
|
||||
end
|
||||
end
|
||||
|
||||
defp count_digits(<<sign, rest::bits>>, base) when sign in '+-' do
|
||||
case count_digits_nosign(rest, base, 1) do
|
||||
1 -> 0
|
||||
count -> count
|
||||
end
|
||||
end
|
||||
|
||||
defp count_digits(<<rest::bits>>, base) do
|
||||
count_digits_nosign(rest, base, 0)
|
||||
end
|
||||
|
||||
digits = [{?0..?9, -?0}, {?A..?Z, 10 - ?A}, {?a..?z, 10 - ?a}]
|
||||
|
||||
for {chars, diff} <- digits,
|
||||
char <- chars do
|
||||
digit = char + diff
|
||||
|
||||
defp count_digits_nosign(<<unquote(char), rest::bits>>, base, count)
|
||||
when base > unquote(digit) do
|
||||
count_digits_nosign(rest, base, count + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp count_digits_nosign(<<_::bits>>, _, count), do: count
|
||||
|
||||
@doc """
|
||||
Returns a binary which corresponds to the text representation
|
||||
of `integer` in the given `base`.
|
||||
|
||||
`base` can be an integer between 2 and 36. If no `base` is given,
|
||||
it defaults to `10`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.to_string(123)
|
||||
"123"
|
||||
|
||||
iex> Integer.to_string(+456)
|
||||
"456"
|
||||
|
||||
iex> Integer.to_string(-789)
|
||||
"-789"
|
||||
|
||||
iex> Integer.to_string(0123)
|
||||
"123"
|
||||
|
||||
iex> Integer.to_string(100, 16)
|
||||
"64"
|
||||
|
||||
iex> Integer.to_string(-100, 16)
|
||||
"-64"
|
||||
|
||||
iex> Integer.to_string(882_681_651, 36)
|
||||
"ELIXIR"
|
||||
|
||||
"""
|
||||
@spec to_string(integer, 2..36) :: String.t()
|
||||
def to_string(integer, base \\ 10) do
|
||||
:erlang.integer_to_binary(integer, base)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a charlist which corresponds to the text representation
|
||||
of `integer` in the given `base`.
|
||||
|
||||
`base` can be an integer between 2 and 36. If no `base` is given,
|
||||
it defaults to `10`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.to_charlist(123)
|
||||
'123'
|
||||
|
||||
iex> Integer.to_charlist(+456)
|
||||
'456'
|
||||
|
||||
iex> Integer.to_charlist(-789)
|
||||
'-789'
|
||||
|
||||
iex> Integer.to_charlist(0123)
|
||||
'123'
|
||||
|
||||
iex> Integer.to_charlist(100, 16)
|
||||
'64'
|
||||
|
||||
iex> Integer.to_charlist(-100, 16)
|
||||
'-64'
|
||||
|
||||
iex> Integer.to_charlist(882_681_651, 36)
|
||||
'ELIXIR'
|
||||
|
||||
"""
|
||||
@spec to_charlist(integer, 2..36) :: charlist
|
||||
def to_charlist(integer, base \\ 10) do
|
||||
:erlang.integer_to_list(integer, base)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the greatest common divisor of the two given integers.
|
||||
|
||||
The greatest common divisor (GCD) of `integer1` and `integer2` is the largest positive
|
||||
integer that divides both `integer1` and `integer2` without leaving a remainder.
|
||||
|
||||
By convention, `gcd(0, 0)` returns `0`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.gcd(2, 3)
|
||||
1
|
||||
|
||||
iex> Integer.gcd(8, 12)
|
||||
4
|
||||
|
||||
iex> Integer.gcd(8, -12)
|
||||
4
|
||||
|
||||
iex> Integer.gcd(10, 0)
|
||||
10
|
||||
|
||||
iex> Integer.gcd(7, 7)
|
||||
7
|
||||
|
||||
iex> Integer.gcd(0, 0)
|
||||
0
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec gcd(integer, integer) :: non_neg_integer
|
||||
def gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
|
||||
gcd_positive(abs(integer1), abs(integer2))
|
||||
end
|
||||
|
||||
defp gcd_positive(0, integer2), do: integer2
|
||||
defp gcd_positive(integer1, 0), do: integer1
|
||||
defp gcd_positive(integer1, integer2), do: gcd_positive(integer2, rem(integer1, integer2))
|
||||
|
||||
@doc """
|
||||
Returns the extended greatest common divisor of the two given integers.
|
||||
|
||||
This function uses the extended Euclidean algorithm to return a three-element tuple with the `gcd`
|
||||
and the coefficients `m` and `n` of Bézout's identity such that:
|
||||
|
||||
gcd(a, b) = m*a + n*b
|
||||
|
||||
By convention, `extended_gcd(0, 0)` returns `{0, 0, 0}`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.extended_gcd(240, 46)
|
||||
{2, -9, 47}
|
||||
iex> Integer.extended_gcd(46, 240)
|
||||
{2, 47, -9}
|
||||
iex> Integer.extended_gcd(-46, 240)
|
||||
{2, -47, -9}
|
||||
iex> Integer.extended_gcd(-46, -240)
|
||||
{2, -47, 9}
|
||||
|
||||
iex> Integer.extended_gcd(14, 21)
|
||||
{7, -1, 1}
|
||||
|
||||
iex> Integer.extended_gcd(10, 0)
|
||||
{10, 1, 0}
|
||||
iex> Integer.extended_gcd(0, 10)
|
||||
{10, 0, 1}
|
||||
iex> Integer.extended_gcd(0, 0)
|
||||
{0, 0, 0}
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
|
||||
def extended_gcd(0, 0), do: {0, 0, 0}
|
||||
def extended_gcd(0, b), do: {b, 0, 1}
|
||||
def extended_gcd(a, 0), do: {a, 1, 0}
|
||||
|
||||
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
|
||||
extended_gcd(integer2, integer1, 0, 1, 1, 0)
|
||||
end
|
||||
|
||||
defp extended_gcd(r1, r0, s1, s0, t1, t0) do
|
||||
div = div(r0, r1)
|
||||
|
||||
case r0 - div * r1 do
|
||||
0 when r1 > 0 -> {r1, s1, t1}
|
||||
0 when r1 < 0 -> {-r1, -s1, -t1}
|
||||
r2 -> extended_gcd(r2, r1, s0 - div * s1, s1, t0 - div * t1, t1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Integer.to_charlist/1 instead"
|
||||
def to_char_list(integer), do: Integer.to_charlist(integer)
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Integer.to_charlist/2 instead"
|
||||
def to_char_list(integer, base), do: Integer.to_charlist(integer, base)
|
||||
end
|
||||
@@ -1,768 +0,0 @@
|
||||
defmodule IO do
|
||||
@moduledoc ~S"""
|
||||
Functions handling input/output (IO).
|
||||
|
||||
Many functions in this module expect an IO device as an 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 chardata. In case another type is given,
|
||||
functions will convert those types to string via the `String.Chars` protocol
|
||||
(as shown in typespecs). For more information on chardata, see the
|
||||
"IO data" section below.
|
||||
|
||||
## 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. In addition,
|
||||
Elixir provides two shortcuts:
|
||||
|
||||
* `:stdio` - a shortcut for `:standard_io`, which maps to
|
||||
the current `Process.group_leader/0` in Erlang
|
||||
|
||||
* `:stderr` - a shortcut for the named process `:standard_error`
|
||||
provided in Erlang
|
||||
|
||||
IO devices maintain their position, which means subsequent calls to any
|
||||
reading or writing functions will start from the place where the device
|
||||
was last accessed. The position of files can be changed using the
|
||||
`:file.position/2` function.
|
||||
|
||||
## IO data
|
||||
|
||||
IO data is a data type that can be used as a more efficient alternative to binaries
|
||||
in certain situations.
|
||||
|
||||
A term of type **IO data** is a binary or a list containing bytes (integers within the `0..255` range)
|
||||
or nested IO data. The type is recursive. Let's see an example of one of
|
||||
the possible IO data representing the binary `"hello"`:
|
||||
|
||||
[?h, "el", ["l", [?o]]]
|
||||
|
||||
The built-in `t:iodata/0` type is defined in terms of `t:iolist/0`. An IO list is
|
||||
the same as IO data but it doesn't allow for a binary at the top level (but binaries
|
||||
are still allowed in the list itself).
|
||||
|
||||
### Use cases for IO data
|
||||
|
||||
IO data exists because often you need to do many append operations
|
||||
on smaller chunks of binaries in order to create a bigger binary. However, in
|
||||
Erlang and Elixir concatenating binaries will copy the concatenated binaries
|
||||
into a new binary.
|
||||
|
||||
def email(username, domain) do
|
||||
username <> "@" <> domain
|
||||
end
|
||||
|
||||
In this function, creating the email address will copy the `username` and `domain`
|
||||
binaries. Now imagine you want to use the resulting email inside another binary:
|
||||
|
||||
def welcome_message(name, username, domain) do
|
||||
"Welcome #{name}, your email is: #{email(username, domain)}"
|
||||
end
|
||||
|
||||
IO.puts(welcome_message("Meg", "meg", "example.com"))
|
||||
#=> "Welcome Meg, your email is: meg@example.com"
|
||||
|
||||
Every time you concatenate binaries or use interpolation (`#{}`) you are making
|
||||
copies of those binaries. However, in many cases you don't need the complete
|
||||
binary while you create it, but only at the end to print it out or send it
|
||||
somewhere. In such cases, you can construct the binary by creating IO data:
|
||||
|
||||
def email(username, domain) do
|
||||
[username, ?@, domain]
|
||||
end
|
||||
|
||||
def welcome_message(name, username, domain) do
|
||||
["Welcome ", name, ", your email is: ", email(username, domain)]
|
||||
end
|
||||
|
||||
IO.puts(welcome_message("Meg", "meg", "example.com"))
|
||||
#=> "Welcome Meg, your email is: meg@example.com"
|
||||
|
||||
Building IO data is cheaper than concatenating binaries. Concatenating multiple
|
||||
pieces of IO data just means putting them together inside a list since IO data
|
||||
can be arbitrarily nested, and that's a cheap and efficient operation. Most of
|
||||
the IO-based APIs, such as `:gen_tcp` and `IO`, receive IO data and write it
|
||||
to the socket directly without converting it to binary.
|
||||
|
||||
One drawback of IO data is that you can't do things like pattern match on the
|
||||
first part of a piece of IO data like you can with a binary, because you usually
|
||||
don't know the shape of the IO data. In those cases, you may need to convert it
|
||||
to a binary by calling `iodata_to_binary/1`, which is reasonably efficient
|
||||
since it's implemented natively in C. Other functionality, like computing the
|
||||
length of IO data, can be computed directly on the iodata by calling `iodata_length/1`.
|
||||
|
||||
### Chardata
|
||||
|
||||
Erlang and Elixir also have the idea of `t:chardata/0`. Chardata is very
|
||||
similar to IO data: the only difference is that integers in IO data represent
|
||||
bytes while integers in chardata represent Unicode code points. Bytes
|
||||
(`t:byte/0`) are integers within the `0..255` range, while Unicode code points
|
||||
(`t:char/0`) are integers within the `0..0x10FFFF` range. The `IO` module provides
|
||||
the `chardata_to_string/1` function for chardata as the "counter-part" of the
|
||||
`iodata_to_binary/1` function for IO data.
|
||||
|
||||
If you try to use `iodata_to_binary/1` on chardata, it will result in an
|
||||
argument error. For example, let's try to put a code point that is not
|
||||
representable with one byte, like `?π`, inside IO data:
|
||||
|
||||
IO.iodata_to_binary(["The symbol for pi is: ", ?π])
|
||||
#=> ** (ArgumentError) argument error
|
||||
|
||||
If we use chardata instead, it will work as expected:
|
||||
|
||||
iex> IO.chardata_to_string(["The symbol for pi is: ", ?π])
|
||||
"The symbol for pi is: π"
|
||||
|
||||
"""
|
||||
|
||||
@type device :: atom | pid
|
||||
@type nodata :: {:error, term} | :eof
|
||||
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
|
||||
|
||||
defguardp is_device(term) when is_atom(term) or is_pid(term)
|
||||
defguardp is_iodata(data) when is_list(data) or is_binary(data)
|
||||
|
||||
@doc """
|
||||
Reads from the IO `device`.
|
||||
|
||||
The `device` is iterated by the given number of characters, line by line if
|
||||
`:line` is given, or until `:eof`.
|
||||
|
||||
It returns:
|
||||
|
||||
* `data` - the output characters
|
||||
|
||||
* `:eof` - end of file was encountered
|
||||
|
||||
* `{:error, reason}` - other (rare) error condition;
|
||||
for instance, `{:error, :estale}` if reading from an
|
||||
NFS volume
|
||||
|
||||
"""
|
||||
@spec read(device, :eof | :line | non_neg_integer) :: chardata | nodata
|
||||
def read(device \\ :stdio, line_or_chars)
|
||||
|
||||
# TODO: Deprecate me on v1.17
|
||||
def read(device, :all) do
|
||||
with :eof <- read(device, :eof) do
|
||||
with [_ | _] = opts <- :io.getopts(device),
|
||||
false <- Keyword.get(opts, :binary, true) do
|
||||
''
|
||||
else
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def read(device, :eof) do
|
||||
getn(device, '', :eof)
|
||||
end
|
||||
|
||||
def read(device, :line) do
|
||||
:io.get_line(map_dev(device), '')
|
||||
end
|
||||
|
||||
def read(device, count) when is_integer(count) and count >= 0 do
|
||||
:io.get_chars(map_dev(device), '', count)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads from the IO `device`. The operation is Unicode unsafe.
|
||||
|
||||
The `device` is iterated as specified by the `line_or_chars` argument:
|
||||
|
||||
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
|
||||
iterated by that number of bytes.
|
||||
|
||||
* if `line_or_chars` is `:line`, the device is iterated line by line.
|
||||
|
||||
* if `line_or_chars` is `:eof`, the device is iterated until `:eof`. `line_or_chars`
|
||||
can only be `:eof` since Elixir 1.13.0. `:eof` replaces the deprecated `:all`,
|
||||
with the difference that `:all` returns `""` on end of file, while `:eof` returns
|
||||
`:eof` itself.
|
||||
|
||||
It returns:
|
||||
|
||||
* `data` - the output bytes
|
||||
|
||||
* `:eof` - end of file was encountered
|
||||
|
||||
* `{:error, reason}` - other (rare) error condition;
|
||||
for instance, `{:error, :estale}` if reading from an
|
||||
NFS volume
|
||||
|
||||
Note: do not use this function on IO devices in Unicode mode
|
||||
as it will return the wrong result.
|
||||
"""
|
||||
@spec binread(device, :eof | :line | non_neg_integer) :: iodata | nodata
|
||||
def binread(device \\ :stdio, line_or_chars)
|
||||
|
||||
# TODO: Deprecate me on v1.17
|
||||
def binread(device, :all) do
|
||||
with :eof <- binread(device, :eof), do: ""
|
||||
end
|
||||
|
||||
def binread(device, :eof) do
|
||||
binread_eof(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 is_integer(count) and count >= 0 do
|
||||
case :file.read(map_dev(device), count) do
|
||||
{:ok, data} -> data
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@read_all_size 4096
|
||||
defp binread_eof(mapped_dev, acc) do
|
||||
case :file.read(mapped_dev, @read_all_size) do
|
||||
{:ok, data} -> binread_eof(mapped_dev, acc <> data)
|
||||
:eof -> if acc == "", do: :eof, else: acc
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes `chardata` to the given `device`.
|
||||
|
||||
By default, the `device` is the standard output.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.write("sample")
|
||||
#=> sample
|
||||
|
||||
IO.write(:stderr, "error")
|
||||
#=> error
|
||||
|
||||
"""
|
||||
@spec write(device, chardata | String.Chars.t()) :: :ok
|
||||
def write(device \\ :stdio, chardata) do
|
||||
:io.put_chars(map_dev(device), to_chardata(chardata))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes `iodata` to the given `device`.
|
||||
|
||||
This operation is meant to be used with "raw" devices
|
||||
that are started without an encoding. The given `iodata`
|
||||
is written as is to the device, without conversion. For
|
||||
more information on IO data, see the "IO data" section in
|
||||
the module documentation.
|
||||
|
||||
Use `write/2` for devices with encoding.
|
||||
|
||||
Important: do **not** use this function on IO devices in
|
||||
Unicode mode as it will write the wrong data. In particular,
|
||||
the standard IO device is set to Unicode by default, so writing
|
||||
to stdio with this function will likely result in the wrong data
|
||||
being sent down the wire.
|
||||
"""
|
||||
@spec binwrite(device, iodata) :: :ok | {:error, term}
|
||||
def binwrite(device \\ :stdio, iodata) when is_iodata(iodata) do
|
||||
:file.write(map_dev(device), iodata)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes `item` to the given `device`, similar to `write/2`,
|
||||
but adds a newline at the end.
|
||||
|
||||
By default, the `device` is the standard output. It returns `:ok`
|
||||
if it succeeds.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.puts("Hello World!")
|
||||
#=> Hello World!
|
||||
|
||||
IO.puts(:stderr, "error")
|
||||
#=> error
|
||||
|
||||
"""
|
||||
@spec puts(device, chardata | String.Chars.t()) :: :ok
|
||||
def puts(device \\ :stdio, item) when is_device(device) do
|
||||
:io.put_chars(map_dev(device), [to_chardata(item), ?\n])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes a `message` to stderr, along with the given `stacktrace_info`.
|
||||
|
||||
The `stacktrace_info` must be one of:
|
||||
|
||||
* a `__STACKTRACE__`, where all entries in the stacktrace will be
|
||||
included in the error message
|
||||
|
||||
* a `Macro.Env` structure (since v1.14.0), where a single stacktrace
|
||||
entry from the compilation environment will be used
|
||||
|
||||
* a keyword list with at least the `:file` option representing
|
||||
a single stacktrace entry (since v1.14.0). The `:line`, `:module`,
|
||||
`:function` options are also supported
|
||||
|
||||
This function also notifies the compiler a warning was printed
|
||||
(in case --warnings-as-errors was enabled). It returns `:ok`
|
||||
if it succeeds.
|
||||
|
||||
## Examples
|
||||
|
||||
stacktrace = [{MyApp, :main, 1, [file: 'my_app.ex', line: 4]}]
|
||||
IO.warn("variable bar is unused", stacktrace)
|
||||
#=> warning: variable bar is unused
|
||||
#=> my_app.ex:4: MyApp.main/1
|
||||
|
||||
"""
|
||||
@spec warn(chardata | String.Chars.t(), Exception.stacktrace() | keyword() | Macro.Env.t()) ::
|
||||
:ok
|
||||
def warn(message, stacktrace_info)
|
||||
|
||||
def warn(message, []) do
|
||||
message = [to_chardata(message), ?\n]
|
||||
:elixir_errors.log_and_print_warning(0, nil, message, message)
|
||||
end
|
||||
|
||||
def warn(message, %Macro.Env{} = env) do
|
||||
warn(message, Macro.Env.stacktrace(env))
|
||||
end
|
||||
|
||||
def warn(message, [{_, _} | _] = keyword) do
|
||||
if file = keyword[:file] do
|
||||
warn(
|
||||
message,
|
||||
%{
|
||||
__ENV__
|
||||
| module: keyword[:module],
|
||||
function: keyword[:function],
|
||||
line: keyword[:line],
|
||||
file: file
|
||||
}
|
||||
)
|
||||
else
|
||||
warn(message, [])
|
||||
end
|
||||
end
|
||||
|
||||
def warn(message, [{_, _, _, opts} | _] = stacktrace) do
|
||||
message = to_chardata(message)
|
||||
formatted_trace = Enum.map_join(stacktrace, "\n ", &Exception.format_stacktrace_entry(&1))
|
||||
line = opts[:line]
|
||||
file = opts[:file]
|
||||
|
||||
:elixir_errors.log_and_print_warning(
|
||||
line || 0,
|
||||
file && List.to_string(file),
|
||||
message,
|
||||
[message, ?\n, " ", formatted_trace, ?\n]
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def warn_once(key, message, stacktrace_drop_levels) do
|
||||
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
|
||||
stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels)
|
||||
|
||||
if :elixir_config.warn(key, stacktrace) do
|
||||
warn(message, stacktrace)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes a `message` to stderr, along with the current stacktrace.
|
||||
|
||||
It returns `:ok` if it succeeds.
|
||||
|
||||
Do not call this function at the tail of another function. Due to tail
|
||||
call optimization, a stacktrace entry would not be added and the
|
||||
stacktrace would be incorrectly trimmed. Therefore make sure at least
|
||||
one expression (or an atom such as `:ok`) follows the `IO.warn/1` call.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.warn("variable bar is unused")
|
||||
#=> warning: variable bar is unused
|
||||
#=> (iex) evaluator.ex:108: IEx.Evaluator.eval/4
|
||||
|
||||
"""
|
||||
@spec warn(chardata | String.Chars.t()) :: :ok
|
||||
def warn(message) do
|
||||
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
|
||||
warn(message, Enum.drop(stacktrace, 2))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inspects and writes the given `item` to the device.
|
||||
|
||||
It's important to note that it returns the given `item` unchanged.
|
||||
This makes it possible to "spy" on values by inserting an
|
||||
`IO.inspect/2` call almost anywhere in your code, for example,
|
||||
in the middle of a pipeline.
|
||||
|
||||
It enables pretty printing by default with width of
|
||||
80 characters. The width can be changed by explicitly
|
||||
passing the `:width` option.
|
||||
|
||||
The output can be decorated with a label, by providing the `:label`
|
||||
option to easily distinguish it from other `IO.inspect/2` calls.
|
||||
The label will be printed before the inspected `item`.
|
||||
|
||||
See `Inspect.Opts` for a full list of remaining formatting options.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.inspect(<<0, 1, 2>>, width: 40)
|
||||
|
||||
Prints:
|
||||
|
||||
<<0, 1, 2>>
|
||||
|
||||
We can use the `:label` option to decorate the output:
|
||||
|
||||
IO.inspect(1..100, label: "a wonderful range")
|
||||
|
||||
Prints:
|
||||
|
||||
a wonderful range: 1..100
|
||||
|
||||
The `:label` option is especially useful with pipelines:
|
||||
|
||||
[1, 2, 3]
|
||||
|> IO.inspect(label: "before")
|
||||
|> Enum.map(&(&1 * 2))
|
||||
|> IO.inspect(label: "after")
|
||||
|> Enum.sum()
|
||||
|
||||
Prints:
|
||||
|
||||
before: [1, 2, 3]
|
||||
after: [2, 4, 6]
|
||||
|
||||
"""
|
||||
@spec inspect(item, keyword) :: item when item: var
|
||||
def inspect(item, opts \\ []) do
|
||||
inspect(:stdio, item, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inspects `item` according to the given options using the IO `device`.
|
||||
|
||||
See `inspect/2` for a full list of options.
|
||||
"""
|
||||
@spec inspect(device, item, keyword) :: item when item: var
|
||||
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
|
||||
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
|
||||
opts = Inspect.Opts.new(opts)
|
||||
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
|
||||
chardata = Inspect.Algebra.format(doc, opts.width)
|
||||
puts(device, [label, chardata])
|
||||
item
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from IO device `:stdio`.
|
||||
|
||||
If `:stdio` is a Unicode device, `count` implies
|
||||
the number of Unicode code points to be retrieved.
|
||||
Otherwise, `count` is the number of raw bytes to be retrieved.
|
||||
|
||||
See `IO.getn/3` for a description of return values.
|
||||
"""
|
||||
@spec getn(
|
||||
device | chardata | String.Chars.t(),
|
||||
pos_integer | :eof | chardata | String.Chars.t()
|
||||
) ::
|
||||
chardata | nodata
|
||||
def getn(prompt, count \\ 1)
|
||||
|
||||
def getn(prompt, :eof) do
|
||||
getn(:stdio, prompt, :eof)
|
||||
end
|
||||
|
||||
def getn(prompt, count) when is_integer(count) and count > 0 do
|
||||
getn(:stdio, prompt, count)
|
||||
end
|
||||
|
||||
def getn(device, prompt) when not is_integer(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 code points 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(device, chardata | String.Chars.t(), pos_integer | :eof) :: chardata | nodata
|
||||
def getn(device, prompt, :eof) do
|
||||
getn_eof(map_dev(device), to_chardata(prompt), [])
|
||||
end
|
||||
|
||||
def getn(device, prompt, count) when is_integer(count) and count > 0 do
|
||||
:io.get_chars(map_dev(device), to_chardata(prompt), count)
|
||||
end
|
||||
|
||||
defp getn_eof(device, prompt, acc) do
|
||||
case :io.get_line(device, prompt) do
|
||||
line when is_binary(line) or is_list(line) -> getn_eof(device, '', [line | acc])
|
||||
:eof -> wrap_eof(:lists.reverse(acc))
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp wrap_eof([h | _] = acc) when is_binary(h), do: IO.iodata_to_binary(acc)
|
||||
defp wrap_eof([h | _] = acc) when is_list(h), do: :lists.flatten(acc)
|
||||
defp wrap_eof([]), do: :eof
|
||||
|
||||
@doc ~S"""
|
||||
Reads a line from the IO `device`.
|
||||
|
||||
It returns:
|
||||
|
||||
* `data` - the characters in the line terminated
|
||||
by a line-feed (LF) or end of file (EOF)
|
||||
|
||||
* `:eof` - end of file was encountered
|
||||
|
||||
* `{:error, reason}` - other (rare) error condition;
|
||||
for instance, `{:error, :estale}` if reading from an
|
||||
NFS volume
|
||||
|
||||
## Examples
|
||||
|
||||
To display "What is your name?" as a prompt and await user input:
|
||||
|
||||
IO.gets("What is your name?\n")
|
||||
|
||||
"""
|
||||
@spec gets(device, chardata | String.Chars.t()) :: chardata | nodata
|
||||
def gets(device \\ :stdio, prompt) do
|
||||
:io.get_line(map_dev(device), to_chardata(prompt))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a line-based `IO.Stream` on `:stdio`.
|
||||
|
||||
This is equivalent to:
|
||||
|
||||
IO.stream(:stdio, :line)
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def stream, do: stream(:stdio, :line)
|
||||
|
||||
@doc """
|
||||
Converts the IO `device` into an `IO.Stream`.
|
||||
|
||||
An `IO.Stream` implements both `Enumerable` and
|
||||
`Collectable`, allowing it to be used for both read
|
||||
and write.
|
||||
|
||||
The `device` is iterated by the given number of characters or line by line if
|
||||
`:line` is given.
|
||||
|
||||
This reads from 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.
|
||||
|
||||
`stream/0` has been introduced in Elixir v1.12.0,
|
||||
while `stream/2` has been available since v1.0.0.
|
||||
|
||||
## 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 \\ :stdio, line_or_codepoints)
|
||||
when line_or_codepoints == :line
|
||||
when is_integer(line_or_codepoints) and line_or_codepoints > 0 do
|
||||
IO.Stream.__build__(map_dev(device), false, line_or_codepoints)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a raw, line-based `IO.Stream` on `:stdio`. The operation is Unicode unsafe.
|
||||
|
||||
This is equivalent to:
|
||||
|
||||
IO.binstream(:stdio, :line)
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def binstream, do: binstream(:stdio, :line)
|
||||
|
||||
@doc """
|
||||
Converts the IO `device` into an `IO.Stream`. The operation is Unicode unsafe.
|
||||
|
||||
An `IO.Stream` implements both `Enumerable` and
|
||||
`Collectable`, allowing it to be used for both read
|
||||
and write.
|
||||
|
||||
The `device` is iterated by the given number of bytes or line by line if
|
||||
`:line` is given. This reads from 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.
|
||||
|
||||
`binstream/0` has been introduced in Elixir v1.12.0,
|
||||
while `binstream/2` has been available since v1.0.0.
|
||||
"""
|
||||
@spec binstream(device, :line | pos_integer) :: Enumerable.t()
|
||||
def binstream(device \\ :stdio, line_or_bytes)
|
||||
when line_or_bytes == :line
|
||||
when is_integer(line_or_bytes) and line_or_bytes > 0 do
|
||||
IO.Stream.__build__(map_dev(device), true, line_or_bytes)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts chardata into a string.
|
||||
|
||||
For more information about chardata, see the ["Chardata"](#module-chardata)
|
||||
section in the module documentation.
|
||||
|
||||
In case the conversion fails, it raises an `UnicodeConversionError`.
|
||||
If a string is given, it returns the string itself.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.chardata_to_string([0x00E6, 0x00DF])
|
||||
"æß"
|
||||
|
||||
iex> IO.chardata_to_string([0x0061, "bc"])
|
||||
"abc"
|
||||
|
||||
iex> IO.chardata_to_string("string")
|
||||
"string"
|
||||
|
||||
"""
|
||||
@spec chardata_to_string(chardata) :: String.t()
|
||||
def chardata_to_string(chardata)
|
||||
|
||||
def chardata_to_string(string) when is_binary(string) do
|
||||
string
|
||||
end
|
||||
|
||||
def chardata_to_string(list) when is_list(list) do
|
||||
List.to_string(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts IO data into a binary
|
||||
|
||||
The operation is Unicode unsafe.
|
||||
|
||||
Note that this function treats integers in the given IO data as
|
||||
raw bytes and does not perform any kind of encoding conversion.
|
||||
If you want to convert from a charlist to a UTF-8-encoded string,
|
||||
use `chardata_to_string/1` instead. For more information about
|
||||
IO data and chardata, see the ["IO data"](#module-io-data) section in the
|
||||
module documentation.
|
||||
|
||||
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(iodata) do
|
||||
:erlang.iolist_to_binary(iodata)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the size of an IO data.
|
||||
|
||||
For more information about IO data, see the ["IO data"](#module-io-data)
|
||||
section in the module documentation.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.iodata_length([1, 2 | <<3, 4>>])
|
||||
4
|
||||
|
||||
"""
|
||||
@spec iodata_length(iodata) :: non_neg_integer
|
||||
def iodata_length(iodata) do
|
||||
:erlang.iolist_size(iodata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def each_stream(device, line_or_codepoints) do
|
||||
case read(device, line_or_codepoints) do
|
||||
:eof ->
|
||||
{:halt, device}
|
||||
|
||||
{:error, reason} ->
|
||||
raise IO.StreamError, reason: reason
|
||||
|
||||
data ->
|
||||
{[data], device}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def each_binstream(device, line_or_chars) do
|
||||
case binread(device, line_or_chars) 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,336 +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
|
||||
@spec unquote(name)() :: String.t()
|
||||
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.
|
||||
|
||||
[ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||
are characters embedded in text used to control formatting, color, and
|
||||
other output options on video text terminals.
|
||||
|
||||
ANSI escapes are typically enabled on all Unix terminals. They are also
|
||||
available on Windows consoles from Windows 10, although it must be
|
||||
explicitly enabled for the current user in the registry by running the
|
||||
following command:
|
||||
|
||||
reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1
|
||||
|
||||
After running the command above, you must restart your current console.
|
||||
|
||||
## Examples
|
||||
|
||||
Because the ANSI escape sequences are embedded in text, the normal usage of
|
||||
these functions is to concatenate their output with text.
|
||||
|
||||
formatted_text = IO.ANSI.blue_background() <> "Example" <> IO.ANSI.reset()
|
||||
IO.puts(formatted_text)
|
||||
|
||||
A higher level and more convenient API is also available via `IO.ANSI.format/1`,
|
||||
where you use atoms to represent each ANSI escape sequence and by default
|
||||
checks if ANSI is enabled:
|
||||
|
||||
IO.puts(IO.ANSI.format([:blue_background, "Example"]))
|
||||
|
||||
In case ANSI is disabled, the ANSI escape sequences are simply discarded.
|
||||
"""
|
||||
|
||||
import IO.ANSI.Sequence
|
||||
|
||||
@type ansicode :: atom
|
||||
@type 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 """
|
||||
Syntax colors to be used by `Inspect`.
|
||||
|
||||
Those colors are used throughout Elixir's standard library,
|
||||
such as `dbg/2` and `IEx`.
|
||||
|
||||
The colors can be changed by setting the `:ansi_syntax_colors`
|
||||
in the `:elixir` application configuration. Configuration for
|
||||
most built-in data types are supported: `:atom`, `:binary`,
|
||||
`:boolean`, `:charlist`, `:list`, `:map`, `:nil`, `:number`,
|
||||
`:string`, and `:tuple`. The default is:
|
||||
|
||||
[
|
||||
atom: :cyan
|
||||
boolean: :magenta,
|
||||
charlist: :yellow,
|
||||
nil: :magenta,
|
||||
number: :yellow,
|
||||
string: :green
|
||||
]
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec syntax_colors :: Keyword.t(ansidata)
|
||||
def syntax_colors do
|
||||
Application.fetch_env!(:elixir, :ansi_syntax_colors)
|
||||
end
|
||||
|
||||
@doc "Sets foreground color."
|
||||
@spec color(0..255) :: String.t()
|
||||
def color(code) when code in 0..255, do: "\e[38;5;#{code}m"
|
||||
|
||||
@doc ~S"""
|
||||
Sets the foreground color from individual RGB values.
|
||||
|
||||
Valid values for each color are in the range 0 to 5.
|
||||
"""
|
||||
@spec color(0..5, 0..5, 0..5) :: String.t()
|
||||
def color(r, g, b) when r in 0..5 and g in 0..5 and b in 0..5 do
|
||||
color(16 + 36 * r + 6 * g + b)
|
||||
end
|
||||
|
||||
@doc "Sets background color."
|
||||
@spec color_background(0..255) :: String.t()
|
||||
def color_background(code) when code in 0..255, do: "\e[48;5;#{code}m"
|
||||
|
||||
@doc ~S"""
|
||||
Sets the background color from individual RGB values.
|
||||
|
||||
Valid values for each color are in the range 0 to 5.
|
||||
"""
|
||||
@spec color_background(0..5, 0..5, 0..5) :: String.t()
|
||||
def color_background(r, g, b) when r in 0..5 and g in 0..5 and b in 0..5 do
|
||||
color_background(16 + 36 * r + 6 * g + b)
|
||||
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)
|
||||
|
||||
@doc "Image: positive. Normal foreground and background."
|
||||
defsequence(:inverse_off, 27)
|
||||
|
||||
@doc "Image: positive. Normal foreground and background."
|
||||
defsequence(:reverse_off, 27)
|
||||
|
||||
colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
|
||||
|
||||
for {color, code} <- Enum.with_index(colors) do
|
||||
@doc "Sets foreground color to #{color}."
|
||||
defsequence(color, code + 30)
|
||||
|
||||
@doc "Sets foreground color to light #{color}."
|
||||
defsequence(:"light_#{color}", code + 90)
|
||||
|
||||
@doc "Sets background color to #{color}."
|
||||
defsequence(:"#{color}_background", code + 40)
|
||||
|
||||
@doc "Sets background color to light #{color}."
|
||||
defsequence(:"light_#{color}_background", code + 100)
|
||||
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 "Sends cursor home."
|
||||
defsequence(:home, "", "H")
|
||||
|
||||
@doc """
|
||||
Sends cursor to the absolute position specified by `line` and `column`.
|
||||
|
||||
Line `0` and column `0` would mean the top left corner.
|
||||
"""
|
||||
@spec cursor(non_neg_integer, non_neg_integer) :: String.t()
|
||||
def cursor(line, column)
|
||||
when is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
|
||||
"\e[#{line};#{column}H"
|
||||
end
|
||||
|
||||
@doc "Sends cursor `lines` up."
|
||||
@spec cursor_up(pos_integer) :: String.t()
|
||||
def cursor_up(lines \\ 1) when is_integer(lines) and lines >= 1, do: "\e[#{lines}A"
|
||||
|
||||
@doc "Sends cursor `lines` down."
|
||||
@spec cursor_down(pos_integer) :: String.t()
|
||||
def cursor_down(lines \\ 1) when is_integer(lines) and lines >= 1, do: "\e[#{lines}B"
|
||||
|
||||
@doc "Sends cursor `columns` to the right."
|
||||
@spec cursor_right(pos_integer) :: String.t()
|
||||
def cursor_right(columns \\ 1) when is_integer(columns) and columns >= 1, do: "\e[#{columns}C"
|
||||
|
||||
@doc "Sends cursor `columns` to the left."
|
||||
@spec cursor_left(pos_integer) :: String.t()
|
||||
def cursor_left(columns \\ 1) when is_integer(columns) and columns >= 1, do: "\e[#{columns}D"
|
||||
|
||||
@doc "Clears screen."
|
||||
defsequence(:clear, "2", "J")
|
||||
|
||||
@doc "Clears line."
|
||||
defsequence(:clear_line, "2", "K")
|
||||
|
||||
defp format_sequence(other) do
|
||||
raise ArgumentError, "invalid ANSI sequence specification: #{inspect(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/0` 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 be 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"]
|
||||
|
||||
"""
|
||||
@spec format(ansidata, boolean) :: IO.chardata()
|
||||
def format(ansidata, emit? \\ enabled?()) when is_boolean(emit?) do
|
||||
do_format(ansidata, [], [], 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 be 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]
|
||||
|
||||
"""
|
||||
@spec format_fragment(ansidata, boolean) :: IO.chardata()
|
||||
def format_fragment(ansidata, emit? \\ enabled?()) when is_boolean(emit?) do
|
||||
do_format(ansidata, [], [], 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,964 +0,0 @@
|
||||
defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@bullet_text_unicode "• "
|
||||
@bullet_text_ascii "* "
|
||||
@bullets [?*, ?-, ?+]
|
||||
@spaces [" ", "\n", "\t"]
|
||||
|
||||
@doc """
|
||||
The default options used by this module.
|
||||
|
||||
The supported keys are:
|
||||
|
||||
* `:enabled` - toggles coloring on and off (true)
|
||||
* `:doc_bold` - bold text (bright)
|
||||
* `:doc_code` - code blocks (cyan)
|
||||
* `:doc_headings` - h1, h2, h3, h4, h5, h6 headings (yellow)
|
||||
* `:doc_metadata` - documentation metadata keys (yellow)
|
||||
* `:doc_quote` - leading quote character `> ` (light black)
|
||||
* `:doc_inline_code` - inline code (cyan)
|
||||
* `:doc_table_heading` - the style for table headings
|
||||
* `:doc_title` - top level heading (reverse, yellow)
|
||||
* `: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.
|
||||
"""
|
||||
@spec default_options() :: keyword
|
||||
def default_options do
|
||||
[
|
||||
enabled: true,
|
||||
doc_bold: [:bright],
|
||||
doc_code: [:cyan],
|
||||
doc_headings: [:yellow],
|
||||
doc_metadata: [:yellow],
|
||||
doc_quote: [:light_black],
|
||||
doc_inline_code: [:cyan],
|
||||
doc_table_heading: [:reverse],
|
||||
doc_title: [:reverse, :yellow],
|
||||
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.
|
||||
"""
|
||||
@spec print_headings([String.t()], keyword) :: :ok
|
||||
def print_headings(headings, options \\ []) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
newline_after_block(options)
|
||||
width = options[:width]
|
||||
|
||||
for heading <- headings do
|
||||
padding = div(width + String.length(heading), 2)
|
||||
heading = String.pad_leading(heading, padding)
|
||||
heading = if options[:enabled], do: String.pad_trailing(heading, width), else: heading
|
||||
write(:doc_title, heading, options)
|
||||
end
|
||||
|
||||
newline_after_block(options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints documentation metadata (only `delegate_to`, `deprecated`, `guard`, and `since` for now).
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
@spec print_metadata(map, keyword) :: :ok
|
||||
def print_metadata(metadata, options \\ []) when is_map(metadata) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
print_each_metadata(metadata, options) && IO.write("\n")
|
||||
end
|
||||
|
||||
@metadata_filter [:deprecated, :guard, :since]
|
||||
|
||||
defp print_each_metadata(metadata, options) do
|
||||
metadata
|
||||
|> Enum.sort()
|
||||
|> Enum.reduce(false, fn
|
||||
{key, value}, _printed when is_binary(value) and key in @metadata_filter ->
|
||||
label = metadata_label(key, options)
|
||||
indent = String.duplicate(" ", length_without_escape(label, 0) + 1)
|
||||
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true, "")
|
||||
|
||||
{key, value}, _printed when is_boolean(value) and key in @metadata_filter ->
|
||||
IO.puts([metadata_label(key, options), ?\s, to_string(value)])
|
||||
|
||||
{:delegate_to, {m, f, a}}, _printed ->
|
||||
label = metadata_label(:delegate_to, options)
|
||||
IO.puts([label, ?\s, Exception.format_mfa(m, f, a)])
|
||||
|
||||
_metadata, printed ->
|
||||
printed
|
||||
end)
|
||||
end
|
||||
|
||||
defp metadata_label(key, options) do
|
||||
"#{color(:doc_metadata, options)}#{key}:#{maybe_reset(options)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the documentation body `doc` according to `format`.
|
||||
|
||||
It takes a set of `options` defined in `default_options/0`.
|
||||
"""
|
||||
@spec print(term(), String.t(), keyword) :: :ok
|
||||
def print(doc, format, options \\ [])
|
||||
|
||||
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
|
||||
print_markdown(doc, options)
|
||||
end
|
||||
|
||||
def print(doc, "application/erlang+html", options) when is_list(options) do
|
||||
print_erlang_html(doc, options)
|
||||
end
|
||||
|
||||
def print(_doc, format, options) when is_binary(format) and is_list(options) do
|
||||
IO.puts("\nUnknown documentation format #{inspect(format)}\n")
|
||||
end
|
||||
|
||||
## Erlang+html
|
||||
|
||||
def print_erlang_html(doc, options) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
IO.write(traverse_erlang_html(doc, "", options))
|
||||
end
|
||||
|
||||
defp traverse_erlang_html(text, _indent, _options) when is_binary(text) do
|
||||
text
|
||||
end
|
||||
|
||||
defp traverse_erlang_html(nodes, indent, options) when is_list(nodes) do
|
||||
for node <- nodes do
|
||||
traverse_erlang_html(node, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:div, [class: class] ++ _, entries}, indent, options) do
|
||||
prefix = indent <> quote_prefix(options)
|
||||
|
||||
content =
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing()
|
||||
|
||||
[
|
||||
prefix,
|
||||
class |> to_string() |> String.upcase(),
|
||||
"\n#{prefix}\n#{prefix}" | String.replace(content, "\n", "\n#{prefix}")
|
||||
]
|
||||
|> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:p, _, entries}, indent, options) do
|
||||
[indent | handle_erlang_html_text(entries, indent, options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h1, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(1, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h2, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(2, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h3, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(3, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h4, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(4, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h5, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(5, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h6, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(6, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:br, _, []}, _indent, _options) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:i, _, entries}, indent, options) do
|
||||
inline_text("_", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:em, _, entries}, indent, options) do
|
||||
inline_text("*", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({tag, _, entries}, indent, options) when tag in [:strong, :b] do
|
||||
inline_text("**", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:code, _, entries}, indent, options) do
|
||||
inline_text("`", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:pre, _, [{:code, _, entries}]}, indent, options) do
|
||||
string =
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
["#{indent} ", String.replace(string, "\n", "\n#{indent} ")] |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:a, attributes, entries}, indent, options) do
|
||||
if href = attributes[:href] do
|
||||
[traverse_erlang_html(entries, indent, options), ?\s, ?(, href, ?)]
|
||||
else
|
||||
traverse_erlang_html(entries, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dl, _, entries}, indent, options) do
|
||||
traverse_erlang_html(entries, indent, options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dt, _, entries}, indent, options) do
|
||||
[
|
||||
"#{indent} ",
|
||||
bullet_text(options) | handle_erlang_html_text(entries, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dd, _, entries}, indent, options) do
|
||||
["#{indent} " | handle_erlang_html_text(entries, indent <> " ", options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:ul, attributes, entries}, indent, options) do
|
||||
if attributes[:class] == "types" do
|
||||
types =
|
||||
for {:li, _, lines} <- entries,
|
||||
line <- lines,
|
||||
do: ["#{indent} ", traverse_erlang_html(line, indent <> " ", options), ?\n]
|
||||
|
||||
if types != [] do
|
||||
["#{indent}Typespecs:\n\n", types, ?\n]
|
||||
else
|
||||
[]
|
||||
end
|
||||
else
|
||||
for {:li, _, lines} <- entries do
|
||||
[
|
||||
"#{indent} ",
|
||||
bullet_text(options) | handle_erlang_html_text(lines, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:ol, _, entries}, indent, options) do
|
||||
for {{:li, _, lines}, i} <- Enum.with_index(entries, 1) do
|
||||
[
|
||||
"#{indent} ",
|
||||
Integer.to_string(i),
|
||||
". " | handle_erlang_html_text(lines, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({tag, _, entries}, indent, options) do
|
||||
[
|
||||
indent <> "<#{tag}>\n",
|
||||
traverse_erlang_html(entries, indent <> " ", options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing(),
|
||||
"\n" <> indent <> "</#{tag}>"
|
||||
]
|
||||
|> newline_cons()
|
||||
end
|
||||
|
||||
defp newline_cons(text) do
|
||||
[text | "\n\n"]
|
||||
end
|
||||
|
||||
defp handle_erlang_html_text(entries, indent, options) do
|
||||
if Enum.all?(entries, &inline_html?/1) do
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.split(@spaces)
|
||||
|> wrap_text(options[:width], indent, true, "", [])
|
||||
|> tl()
|
||||
|> newline_cons()
|
||||
else
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_leading()
|
||||
end
|
||||
end
|
||||
|
||||
defp inline_html?(binary) when is_binary(binary), do: true
|
||||
defp inline_html?({tag, _, _}) when tag in [:a, :code, :em, :i, :strong, :b, :br], do: true
|
||||
defp inline_html?(_), do: false
|
||||
|
||||
## Markdown
|
||||
|
||||
def print_markdown(doc, options) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
|
||||
doc
|
||||
|> String.split(["\r\n", "\n"], trim: false)
|
||||
|> Enum.map(&String.trim_trailing/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_heading(heading, rest, text, indent, options)
|
||||
end
|
||||
|
||||
defp process(["## " <> _ = heading | rest], text, indent, options) do
|
||||
write_heading(heading, rest, text, indent, options)
|
||||
end
|
||||
|
||||
defp process(["### " <> _ = heading | rest], text, indent, options) do
|
||||
write_heading(heading, rest, text, indent, options)
|
||||
end
|
||||
|
||||
defp process(["#### " <> _ = heading | rest], text, indent, options) do
|
||||
write_heading(heading, rest, text, indent, options)
|
||||
end
|
||||
|
||||
defp process(["##### " <> _ = heading | rest], text, indent, options) do
|
||||
write_heading(heading, rest, text, indent, options)
|
||||
end
|
||||
|
||||
defp process(["###### " <> _ = heading | rest], text, indent, options) do
|
||||
write_heading(heading, rest, text, indent, options)
|
||||
end
|
||||
|
||||
defp process([">" <> line | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
process_quote(rest, [line], indent, 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(["```" <> _line | rest], text, indent, options) do
|
||||
process_fenced_code_block(rest, text, indent, options, _delimiter = "```")
|
||||
end
|
||||
|
||||
defp process(["~~~" <> _line | rest], text, indent, options) do
|
||||
process_fenced_code_block(rest, text, indent, options, _delimiter = "~~~")
|
||||
end
|
||||
|
||||
defp process(all = [line | rest], text, indent, options) do
|
||||
{stripped, count} = strip_spaces(line, 0, :infinity)
|
||||
|
||||
cond do
|
||||
link_label?(stripped, count) ->
|
||||
write_text([line], indent, options, true)
|
||||
process(rest, text, indent, options)
|
||||
|
||||
table_line?(stripped) and rest != [] and table_line?(hd(rest)) ->
|
||||
write_text(text, indent, options)
|
||||
process_table(all, indent, options)
|
||||
|
||||
true ->
|
||||
process_rest(stripped, rest, count, text, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
### Headings
|
||||
|
||||
defp write_heading(heading, rest, text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
write(:doc_headings, heading, options)
|
||||
newline_after_block(options)
|
||||
process(rest, [], "", options)
|
||||
end
|
||||
|
||||
### Quotes
|
||||
|
||||
defp process_quote([], lines, indent, options) do
|
||||
write_quote(lines, indent, options, false)
|
||||
end
|
||||
|
||||
defp process_quote([">", ">" <> line | rest], lines, indent, options) do
|
||||
write_quote(lines, indent, options, true)
|
||||
write_empty_quote_line(options)
|
||||
process_quote(rest, [line], indent, options)
|
||||
end
|
||||
|
||||
defp process_quote([">" <> line | rest], lines, indent, options) do
|
||||
process_quote(rest, [line | lines], indent, options)
|
||||
end
|
||||
|
||||
defp process_quote(rest, lines, indent, options) do
|
||||
write_quote(lines, indent, options, false)
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
defp write_quote(lines, indent, options, no_wrap) do
|
||||
lines
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.reverse()
|
||||
|> write_lines(
|
||||
indent,
|
||||
options,
|
||||
no_wrap,
|
||||
quote_prefix(options)
|
||||
)
|
||||
end
|
||||
|
||||
defp write_empty_quote_line(options) do
|
||||
options
|
||||
|> quote_prefix()
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
### Lists
|
||||
|
||||
defp process_rest(stripped, rest, count, text, indent, options) do
|
||||
case stripped do
|
||||
<<bullet, ?\s, item::binary>> when bullet in @bullets ->
|
||||
write_text(text, indent, options)
|
||||
process_list(bullet_text(options), 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
|
||||
|
||||
defp process_list(entry, line, rest, count, indent, options) do
|
||||
# The first list always win some extra padding
|
||||
entry = if indent == "", do: " " <> entry, else: entry
|
||||
new_indent = indent <> String.duplicate(" ", String.length(entry))
|
||||
|
||||
{contents, rest, done} =
|
||||
process_list_next(rest, count, byte_size(new_indent) - byte_size(indent), [])
|
||||
|
||||
process(contents, [indent <> entry <> line, :no_wrap], new_indent, options)
|
||||
|
||||
if done, do: newline_after_block(options)
|
||||
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
|
||||
write_lines(lines, indent, options, no_wrap, "")
|
||||
end
|
||||
|
||||
defp write_lines(lines, indent, options, no_wrap, prefix) do
|
||||
lines
|
||||
|> Enum.join(" ")
|
||||
|> format_text(options)
|
||||
|> String.split(@spaces)
|
||||
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
|
||||
|
||||
unless no_wrap, do: newline_after_block(options)
|
||||
end
|
||||
|
||||
defp format_text(text, options) do
|
||||
text
|
||||
|> handle_links()
|
||||
|> handle_inline(options)
|
||||
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 process_fenced_code_block(rest, text, indent, options, delimiter) do
|
||||
write_text(text, indent, options)
|
||||
process_fenced_code(rest, [], indent, options, delimiter)
|
||||
end
|
||||
|
||||
defp process_fenced_code([], code, indent, options, _delimiter) do
|
||||
write_code(code, indent, options)
|
||||
end
|
||||
|
||||
defp process_fenced_code([line | rest], code, indent, options, delimiter) do
|
||||
if line === delimiter do
|
||||
process_code(rest, code, indent, options)
|
||||
else
|
||||
process_fenced_code(rest, [line | code], indent, options, delimiter)
|
||||
end
|
||||
end
|
||||
|
||||
defp write_code(code, indent, options) do
|
||||
write(:doc_code, "#{indent} #{Enum.join(Enum.reverse(code), "\n#{indent} ")}", options)
|
||||
newline_after_block(options)
|
||||
end
|
||||
|
||||
### Tables
|
||||
|
||||
defp process_table(lines, indent, options) do
|
||||
{table, rest} = Enum.split_while(lines, &table_line?/1)
|
||||
table_lines(table, options)
|
||||
newline_after_block(options)
|
||||
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
|
||||
if table_header?(line) do
|
||||
for _ <- line, do: 0
|
||||
else
|
||||
for {_col, length} <- line, do: length
|
||||
end
|
||||
end
|
||||
|
||||
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.trim(" ")
|
||||
|> String.trim("|")
|
||||
|> String.split(~r{(?<!\\)\|})
|
||||
|> Enum.map(&render_column(&1, options))
|
||||
end
|
||||
|
||||
defp render_column(col, options) do
|
||||
col =
|
||||
col
|
||||
|> String.trim()
|
||||
|> String.replace("\\\|", "|")
|
||||
|> handle_links
|
||||
|> handle_inline(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
|
||||
alignments = Enum.map(second, &column_alignment/1)
|
||||
options = Keyword.put_new(options, :alignments, alignments)
|
||||
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 column_alignment({line, _}) do
|
||||
cond do
|
||||
String.starts_with?(line, ":") and String.ends_with?(line, ":") -> :center
|
||||
String.ends_with?(line, ":") -> :right
|
||||
true -> :left
|
||||
end
|
||||
end
|
||||
|
||||
defp table_header?(line) do
|
||||
Enum.all?(line, fn {col, _} -> table_header_column?(col) end)
|
||||
end
|
||||
|
||||
defp table_header_column?(":" <> rest), do: table_header_contents?(rest)
|
||||
defp table_header_column?(col), do: table_header_contents?(col)
|
||||
|
||||
defp table_header_contents?("-" <> rest), do: table_header_contents?(rest)
|
||||
defp table_header_contents?(":"), do: true
|
||||
defp table_header_contents?(""), do: true
|
||||
defp table_header_contents?(_), do: false
|
||||
|
||||
defp draw_table_row(cols_and_widths, options, heading \\ false) do
|
||||
default_alignments = List.duplicate(:left, length(cols_and_widths))
|
||||
alignments = Keyword.get(options, :alignments, default_alignments)
|
||||
|
||||
columns =
|
||||
cols_and_widths
|
||||
|> Enum.zip(alignments)
|
||||
|> Enum.map_join(" | ", &generate_table_cell/1)
|
||||
|
||||
if heading do
|
||||
write(:doc_table_heading, columns, options)
|
||||
else
|
||||
IO.puts(columns)
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_table_cell({{{col, length}, width}, :center}) do
|
||||
ansi_diff = byte_size(col) - length
|
||||
width = width + ansi_diff
|
||||
|
||||
col
|
||||
|> String.pad_leading(div(width, 2) - div(length, 2) + length)
|
||||
|> String.pad_trailing(width + 1 - rem(width, 2))
|
||||
end
|
||||
|
||||
defp generate_table_cell({{{col, length}, width}, :right}) do
|
||||
ansi_diff = byte_size(col) - length
|
||||
String.pad_leading(col, width + ansi_diff)
|
||||
end
|
||||
|
||||
defp generate_table_cell({{{col, length}, width}, :left}) do
|
||||
ansi_diff = byte_size(col) - length
|
||||
String.pad_trailing(col, width + ansi_diff)
|
||||
end
|
||||
|
||||
defp table_line?(line) do
|
||||
line =~ ~r/[:\ -]\|[:\ -]/
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp link_label?("[" <> rest, count) when count <= 3, do: link_label?(rest)
|
||||
defp link_label?(_, _), do: false
|
||||
|
||||
defp link_label?("]: " <> _), do: true
|
||||
defp link_label?("]" <> _), do: false
|
||||
defp link_label?(""), do: false
|
||||
defp link_label?(<<_>> <> rest), do: link_label?(rest)
|
||||
|
||||
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, maybe_reset(options)])
|
||||
end
|
||||
|
||||
defp write_with_wrap([], _available, _indent, _first, _prefix) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_with_wrap(words, available, indent, first, prefix) do
|
||||
words
|
||||
|> wrap_text(available, indent, first, prefix, [])
|
||||
|> tl()
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
defp wrap_text([], _available, _indent, _first, _prefix, wrapped_lines) do
|
||||
Enum.reverse(wrapped_lines)
|
||||
end
|
||||
|
||||
defp wrap_text(words, available, indent, first, prefix, wrapped_lines) do
|
||||
prefix_length = length_without_escape(prefix, 0)
|
||||
{words, rest} = take_words(words, available - prefix_length, [])
|
||||
line = [if(first, do: "", else: indent), prefix, Enum.join(words, " ")]
|
||||
|
||||
wrap_text(rest, available, indent, false, prefix, [line, ?\n | wrapped_lines])
|
||||
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, count) do
|
||||
length_without_escape(rest, count)
|
||||
end
|
||||
|
||||
defp length_without_escape(<<?\e, ?[, _, ?m>> <> rest, 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
|
||||
# Regular expression adapted from https://tools.ietf.org/html/rfc3986#appendix-B
|
||||
Regex.replace(~r{[a-z][a-z0-9\+\-\.]*://\S*}i, text, &String.replace(&1, "_", "\\_"))
|
||||
end
|
||||
|
||||
defp remove_square_brackets_in_link(text) do
|
||||
Regex.replace(~r{\[([^\]]*?)\]\((.*?)\)}, text, "\\1 (\\2)")
|
||||
end
|
||||
|
||||
# We have four entries: **, __, *, _ and `.
|
||||
#
|
||||
# The first four behave the same while the last one is simpler
|
||||
# when it comes to delimiters as it ignores spaces and escape
|
||||
# characters. But, since the first two has two characters,
|
||||
# we need to handle 3 cases:
|
||||
#
|
||||
# 1. __ and **
|
||||
# 2. _ and *
|
||||
# 3. `
|
||||
#
|
||||
# Where the first two should have the same code but match differently.
|
||||
@single [?_, ?*]
|
||||
|
||||
# Characters that can mark the beginning or the end of a word.
|
||||
# Only support the most common ones at this moment.
|
||||
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
|
||||
|
||||
### Inline start
|
||||
|
||||
defp handle_inline(<<mark, mark, rest::binary>>, options) when mark in @single do
|
||||
handle_inline(rest, [mark | mark], [<<mark, mark>>], [], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, rest::binary>>, options) when mark in @single do
|
||||
handle_inline(rest, mark, [<<mark>>], [], options)
|
||||
end
|
||||
|
||||
defp handle_inline(rest, options) do
|
||||
handle_inline(rest, nil, [], [], options)
|
||||
end
|
||||
|
||||
### Inline delimiters
|
||||
|
||||
defp handle_inline(<<delimiter, mark, mark, rest::binary>>, nil, buffer, acc, options)
|
||||
when rest != "" and delimiter in @delimiters and mark in @single do
|
||||
acc = [delimiter, Enum.reverse(buffer) | acc]
|
||||
handle_inline(rest, [mark | mark], [<<mark, mark>>], acc, options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<delimiter, mark, rest::binary>>, nil, buffer, acc, options)
|
||||
when rest != "" and delimiter in @delimiters and mark in @single do
|
||||
handle_inline(rest, mark, [<<mark>>], [delimiter, Enum.reverse(buffer) | acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<?`, rest::binary>>, nil, buffer, acc, options)
|
||||
when rest != "" do
|
||||
handle_inline(rest, ?`, ["`"], [Enum.reverse(buffer) | acc], options)
|
||||
end
|
||||
|
||||
### Clauses for handling escape
|
||||
|
||||
defp handle_inline(<<?\\, ?\\, mark, mark, rest::binary>>, nil, buffer, acc, options)
|
||||
when rest != "" and mark in @single do
|
||||
acc = [?\\, Enum.reverse(buffer) | acc]
|
||||
handle_inline(rest, [mark | mark], [<<mark, mark>>], 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
|
||||
|
||||
defp handle_inline(<<?\\, ?\\, rest::binary>>, limit, buffer, acc, options) do
|
||||
handle_inline(rest, limit, [?\\ | buffer], acc, options)
|
||||
end
|
||||
|
||||
# An escape is not valid inside `
|
||||
defp handle_inline(<<?\\, mark, rest::binary>>, limit, buffer, acc, options) when limit != ?` do
|
||||
handle_inline(rest, limit, [mark | buffer], acc, options)
|
||||
end
|
||||
|
||||
### Inline end
|
||||
|
||||
defp handle_inline(<<mark, mark, delimiter, rest::binary>>, [mark | mark], buffer, acc, options)
|
||||
when delimiter in @delimiters and mark in @single do
|
||||
inline_buffer = inline_buffer(buffer, options)
|
||||
handle_inline(<<delimiter, rest::binary>>, nil, [], [inline_buffer | acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, delimiter, rest::binary>>, mark, buffer, acc, options)
|
||||
when delimiter in @delimiters and mark in @single do
|
||||
inline_buffer = inline_buffer(buffer, options)
|
||||
handle_inline(<<delimiter, rest::binary>>, nil, [], [inline_buffer | acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, mark, rest::binary>>, [mark | mark], buffer, acc, options)
|
||||
when rest == "" and mark in @single do
|
||||
handle_inline(<<>>, nil, [], [inline_buffer(buffer, options) | acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<mark, rest::binary>>, mark, buffer, acc, options)
|
||||
when rest == "" and mark in @single do
|
||||
handle_inline(<<>>, nil, [], [inline_buffer(buffer, options) | acc], options)
|
||||
end
|
||||
|
||||
defp handle_inline(<<?`, rest::binary>>, ?`, buffer, acc, options) do
|
||||
handle_inline(rest, nil, [], [inline_buffer(buffer, options) | acc], options)
|
||||
end
|
||||
|
||||
### Catch all
|
||||
|
||||
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
|
||||
[mark | t] = Enum.reverse(buffer)
|
||||
inline_text(mark, t, options)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{maybe_reset(options)}"
|
||||
|
||||
defp heading(text, n, options) do
|
||||
[color(:doc_headings, options), String.duplicate("#", n), " ", text, maybe_reset(options)]
|
||||
end
|
||||
|
||||
defp inline_text(mark, text, options) do
|
||||
if options[:enabled] do
|
||||
[[color_for(mark, options) | text] | IO.ANSI.reset()]
|
||||
else
|
||||
[[mark | text] | mark]
|
||||
end
|
||||
end
|
||||
|
||||
defp color_for(mark, colors) do
|
||||
case mark do
|
||||
"__" -> color(:doc_bold, colors)
|
||||
"**" -> color(:doc_bold, colors)
|
||||
"_" -> color(:doc_underline, colors)
|
||||
"*" -> color(:doc_underline, colors)
|
||||
"`" -> color(:doc_inline_code, colors)
|
||||
end
|
||||
end
|
||||
|
||||
defp bullet_text(options) do
|
||||
if options[:enabled], do: @bullet_text_unicode, else: @bullet_text_ascii
|
||||
end
|
||||
|
||||
defp color(style, colors) do
|
||||
IO.ANSI.format_fragment(colors[style], colors[:enabled])
|
||||
end
|
||||
|
||||
defp newline_after_block(options) do
|
||||
IO.puts(maybe_reset(options))
|
||||
end
|
||||
|
||||
defp maybe_reset(options) do
|
||||
if options[:enabled], do: IO.ANSI.reset(), else: ""
|
||||
end
|
||||
end
|
||||
@@ -1,82 +0,0 @@
|
||||
defmodule IO.StreamError do
|
||||
defexception [:reason, :message]
|
||||
|
||||
@impl true
|
||||
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 an `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 number of bytes
|
||||
|
||||
It is worth noting that an IO stream has side effects and every time you go
|
||||
over the stream you may get different results.
|
||||
|
||||
"""
|
||||
|
||||
defstruct device: nil, raw: true, line_or_bytes: :line
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
device: IO.device(),
|
||||
raw: boolean(),
|
||||
line_or_bytes: :line | non_neg_integer()
|
||||
}
|
||||
|
||||
@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 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
|
||||
|
||||
def slice(_stream) do
|
||||
{:error, __MODULE__}
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user