Skip to content

Commit

Permalink
Embed the nginx extension's Lua code in its assets.
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra committed Jan 18, 2024
1 parent c58ca9e commit 78021b9
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .busted
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
return {
default = {
ROOT = { './busted' }
}
}
14 changes: 13 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ jobs:
)
sudo apt-get install "${apt_packages[@]}"
- name: Instlal Lua
if: startsWith(runner.os, 'Linux')
uses: leafo/gh-actions-lua@v9

- name: Install LuaRocks
if: startsWith(runner.os, 'Linux')
uses: leafo/gh-actions-luarocks@v4

- name: Install Lua dependencies
if: startsWith(runner.os, 'Linux')
run: luarocks install busted

- name: Build the development environment
run: ./bin/build-ci
shell: bash
Expand All @@ -122,7 +134,7 @@ jobs:

- name: Run the tests
if: ${{ ! startsWith(runner.os, 'Linux') }}
run: BETTY_TEST_SKIP_SHELLCHECK=true BETTY_TEST_SKIP_FLAKE8=true BETTY_TEST_SKIP_MYPY=true BETTY_TEST_SKIP_STYLELINT=true BETTY_TEST_SKIP_ESLINT=true BETTY_TEST_SKIP_CYPRESS=true BETTY_TEST_SKIP_PYINSTALLER=true ./bin/test
run: BETTY_TEST_SKIP_SHELLCHECK=true BETTY_TEST_SKIP_FLAKE8=true BETTY_TEST_SKIP_MYPY=true BETTY_TEST_SKIP_STYLELINT=true BETTY_TEST_SKIP_ESLINT=true BETTY_TEST_SKIP_BUSTED=true BETTY_TEST_SKIP_CYPRESS=true BETTY_TEST_SKIP_PYINSTALLER=true ./bin/test
shell: bash

- name: Upload code coverage
Expand Down
6 changes: 4 additions & 2 deletions betty/extension/nginx/artifact.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
from pathlib import Path
from shutil import copyfile
from urllib.parse import urlparse
Expand Down Expand Up @@ -42,6 +43,7 @@ async def generate_dockerfile_file(app: App, destination_file_path: Path | None
Generate a Dockerfile to the given destination path.
"""
if destination_file_path is None:
destination_file_path = app.project.configuration.output_directory_path / 'nginx' / 'docker' / 'Dockerfile'
destination_file_path = app.project.configuration.output_directory_path / 'nginx' / 'Dockerfile'
await makedirs(destination_file_path.parent, exist_ok=True)
copyfile(Path(__file__).parent / 'assets' / 'docker' / 'Dockerfile', destination_file_path)
await asyncio.to_thread(copyfile, Path(__file__).parent / 'assets' / 'Dockerfile', destination_file_path)
await asyncio.to_thread(copyfile, Path(__file__).parent / 'assets' / 'content_negotiation.lua', destination_file_path.parent / 'content_negotiation.lua')
5 changes: 5 additions & 0 deletions betty/extension/nginx/assets/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM openresty/openresty:alpine

RUN mkdir /betty-lua
COPY content_negotiation.lua /betty-lua/content_negotiation.lua
RUN echo "lua_package_path '/betty-lua/?.lua;;';" > /etc/nginx/conf.d/default.conf
65 changes: 65 additions & 0 deletions betty/extension/nginx/assets/content_negotiation.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
local Cone = {}

function Cone.negotiate(header, available_values)
if available_values == nil or available_values == {} then
return nil
end

if header == nil or header == '' then
return available_values[1]
end

header = header:gsub('%s+', '')

acceptable_values = {}
unacceptable_values = {}
for qualified_value in header:gmatch('([^,]+)') do
value, quality = Cone.parse_qualified_value(qualified_value)
if quality == 0 then
table.insert(unacceptable_values, value)
else
table.insert(acceptable_values, { value, quality})
end
end
-- Sort the values by quality in descending order.
table.sort(acceptable_values, function(a, b) return a[2] > b[2] end)

for _, qualified_acceptable_value in ipairs(acceptable_values) do
acceptable_value = qualified_acceptable_value[1]
for _, available_value in pairs(available_values) do
if acceptable_value == available_value then
return acceptable_value
end
end
end

for _, available_value in ipairs(available_values) do
if not Cone._contains(available_value, unacceptable_values) then
return available_value
end
end

return available_values[1]
end

function Cone.parse_qualified_value(qualified_value)
if qualified_value:find(';q=') then
value, quality = qualified_value:match("(.*)%;q=(.*)")
quality = tonumber(quality)
else
value = qualified_value
quality = 1
end
return value, quality
end

function Cone._contains(needle, haystack)
for _, haystack_value in pairs(haystack) do
if haystack_value == needle then
return true
end
end
return false
end

return Cone
5 changes: 0 additions & 5 deletions betty/extension/nginx/assets/docker/Dockerfile

This file was deleted.

4 changes: 2 additions & 2 deletions betty/extension/nginx/assets/nginx.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ server {
local media_type_extensions = {}
media_type_extensions['text/html'] = 'html'
media_type_extensions['application/json'] = 'json'
local media_type = require('cone').negotiate(ngx.req.get_headers()['Accept'], available_media_types)
local media_type = require('content_negotiation').negotiate(ngx.req.get_headers()['Accept'], available_media_types)
return media_type_extensions[media_type]
}
{% else %}
Expand All @@ -57,7 +57,7 @@ server {
{% for locale_configuration in app.project.configuration.locales.values() %}
locale_aliases['{{ locale_configuration.locale }}'] = '{{ locale_configuration.alias }}'
{% endfor %}
local locale = require('cone').negotiate(ngx.req.get_headers()['Accept-Language'], available_locales)
local locale = require('content_negotiation').negotiate(ngx.req.get_headers()['Accept-Language'], available_locales)
return locale_aliases[locale]
}
{{ headers(
Expand Down
2 changes: 1 addition & 1 deletion betty/extension/nginx/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async def start(self) -> None:
self._output_directory = TemporaryDirectory()
output_directory_name = await self._output_directory.__aenter__()
nginx_configuration_file_path = Path(output_directory_name) / 'nginx.conf'
docker_directory_path = Path(output_directory_name) / 'docker'
docker_directory_path = Path(output_directory_name)
dockerfile_file_path = docker_directory_path / 'Dockerfile'

self._app.project.configuration.debug = True
Expand Down
6 changes: 3 additions & 3 deletions betty/tests/extension/nginx/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ async def test_post_render_config_multilingual_with_clean_urls(self):
local media_type_extensions = {}
media_type_extensions['text/html'] = 'html'
media_type_extensions['application/json'] = 'json'
local media_type = require('cone').negotiate(ngx.req.get_headers()['Accept'], available_media_types)
local media_type = require('content_negotiation').negotiate(ngx.req.get_headers()['Accept'], available_media_types)
return media_type_extensions[media_type]
}
index index.$media_type_extension;
Expand All @@ -170,7 +170,7 @@ async def test_post_render_config_multilingual_with_clean_urls(self):
local locale_aliases = {}
locale_aliases['en-US'] = 'en'
locale_aliases['nl-NL'] = 'nl'
local locale = require('cone').negotiate(ngx.req.get_headers()['Accept-Language'], available_locales)
local locale = require('content_negotiation').negotiate(ngx.req.get_headers()['Accept-Language'], available_locales)
return locale_aliases[locale]
}
add_header Vary Accept-Language;
Expand Down Expand Up @@ -242,7 +242,7 @@ async def test_post_render_config_with_clean_urls(self):
local media_type_extensions = {}
media_type_extensions['text/html'] = 'html'
media_type_extensions['application/json'] = 'json'
local media_type = require('cone').negotiate(ngx.req.get_headers()['Accept'], available_media_types)
local media_type = require('content_negotiation').negotiate(ngx.req.get_headers()['Accept'], available_media_types)
return media_type_extensions[media_type]
}
index index.$media_type_extension;
Expand Down
3 changes: 3 additions & 0 deletions bin/test
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ fi
if [ -z "${BETTY_TEST_SKIP_ESLINT-}" ]; then
./bin/test-eslint
fi
if [ -z "${BETTY_TEST_SKIP_BUSTED-}" ]; then
./bin/test-busted
fi
./bin/test-pytest
if [ -z "${BETTY_TEST_SKIP_CYPRESS-}" ]; then
./bin/test-cypress
Expand Down
9 changes: 9 additions & 0 deletions bin/test-busted
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash

set -Eeuo pipefail

cd "$(dirname "$0")/.."

echo 'Running Busted...'

busted "$@"
67 changes: 67 additions & 0 deletions busted/content_negotiation_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
local cone = require('../betty/extension/nginx/assets/content_negotiation')

describe('negotiate', function ()
it('nil header, with nil available, should not return', function ()
assert.is_nil(cone.negotiate(nil, nil))
end)

it('nil header, with none available, should not return', function ()
assert.is_nil(cone.negotiate(nil, {}))
end)

it('empty header, with nil available, should not return', function ()
assert.is_nil(cone.negotiate('', nil))
end)

it('empty header, with none available, should not return', function ()
assert.is_nil(cone.negotiate('', {}))
end)

it('empty header containing spaces, with none available, should not return', function ()
assert.is_nil(cone.negotiate(' ', {}))
end)

it('empty header containing tabs, with none available, should not return', function ()
assert.is_nil(cone.negotiate(' ', {}))
end)

it('empty header, with one available, should return the default available', function ()
assert.are.equal('apples', cone.negotiate('', {'apples'}))
end)

it('empty header, with multiple available, should return the default available', function ()
assert.are.equal('apples', cone.negotiate('', {'apples', 'oranges', 'bananas'}))
end)

it('header with multiple, with multiple others available, should return the default available', function ()
assert.are.equal('apples', cone.negotiate('uk,fr,la', {'apples', 'oranges', 'bananas'}))
end)

it('header with one value, with none available, should not return', function ()
assert.is_nil(cone.negotiate('apples', {}))
end)

it('header with multiple values, with none available, should not return', function ()
assert.is_nil(cone.negotiate('apples,oranges,bananas', {}))
end)

it('header with one value, with one available, should not return', function ()
assert.are.equal('apples', cone.negotiate('apples', {'apples'}))
end)

it('header with one value with a default quality, being the last available, should return the one header value', function ()
assert.are.equal('bananas', cone.negotiate('bananas', {'apples', 'oranges', 'bananas'}))
end)

it('header with one value with an explicit quality, being the last available, should return the one header value', function ()
assert.are.equal('bananas', cone.negotiate('bananas;q=0.5', {'apples', 'oranges', 'bananas'}))
end)

it('header with one value with an unacceptable quality, being the last available, should return the one header value', function ()
assert.are.equal('oranges', cone.negotiate('apples;q=0', {'apples', 'oranges', 'bananas'}))
end)

it('header with multiple values and whitespace, should return the preferred header value', function ()
assert.are.equal('bananas', cone.negotiate('bananas , oranges', {'apples', 'oranges', 'bananas'}))
end)
end)
1 change: 1 addition & 0 deletions documentation/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ These impact the ``./bin/test`` command:
* ``BETTY_TEST_SKIP_MYPY=true``: Skip mypy tests.
* ``BETTY_TEST_SKIP_STYLELINT=true``: Skip Stylelint tests.
* ``BETTY_TEST_SKIP_ESLINT=true``: Skip ESLint tests.
* ``BETTY_TEST_SKIP_BUSTED=true``: Skip the Busted test build.
* ``BETTY_TEST_SKIP_CYPRESS=true``: Skip Cypress tests.
* ``BETTY_TEST_SKIP_PYINSTALLER=true``: Skip the PyInstaller test build.

Expand Down
7 changes: 1 addition & 6 deletions documentation/usage/extension/nginx.rst
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
The nginx extension
===================
The :py:class:`betty.extension.Nginx` extension creates an `nginx <https://nginx.org>`_ configuration file and a `Docker <https://www.docker.com/>`_ ``Dockerfile`` in the output
directory. If ``clean_urls`` is enabled. You must make sure the nginx
`Lua module <https://github.com/openresty/lua-nginx-module#readme>`_ is enabled, and
`CONE <https://github.com/bartfeenstra/cone>`_'s
`cone.lua <https://raw.githubusercontent.com/bartfeenstra/cone/master/cone.lua>`_ can be found by putting it in
nginx's `lua_package_path <https://github.com/openresty/lua-nginx-module#lua_package_path>`_. This is done
automatically when using the ``Dockerfile``.
directory.

Enable this extension through Betty Desktop, or in your project's :doc:`configuration file </usage/project/configuration>` as follows:

Expand Down

0 comments on commit 78021b9

Please sign in to comment.