improvement

This commit is contained in:
Ole-Morten Duesund 2025-06-06 18:55:54 +02:00
commit c0ccb8ca5e
6 changed files with 516 additions and 63 deletions

View file

@ -15,6 +15,9 @@ COPY Gemfile* ./
# Install Ruby gems # Install Ruby gems
RUN bundle install --without development RUN bundle install --without development
# Copy responses file first
COPY responses.yml ./
# Copy application code # Copy application code
COPY . . COPY . .

View file

@ -3,6 +3,7 @@ source 'https://rubygems.org'
gem 'sinatra', '~> 3.0' gem 'sinatra', '~> 3.0'
gem 'puma', '~> 6.0' gem 'puma', '~> 6.0'
gem 'json', '~> 2.6' gem 'json', '~> 2.6'
gem 'yaml', '~> 0.2'
group :development do group :development do
gem 'rerun' gem 'rerun'

View file

@ -38,6 +38,7 @@ GEM
rack-protection (= 3.2.0) rack-protection (= 3.2.0)
tilt (~> 2.0) tilt (~> 2.0)
tilt (2.6.0) tilt (2.6.0)
yaml (0.4.0)
PLATFORMS PLATFORMS
aarch64-linux-gnu aarch64-linux-gnu
@ -57,6 +58,7 @@ DEPENDENCIES
puma (~> 6.0) puma (~> 6.0)
rerun rerun
sinatra (~> 3.0) sinatra (~> 3.0)
yaml (~> 0.2)
BUNDLED WITH BUNDLED WITH
2.6.7 2.6.7

117
README.md
View file

@ -1,51 +1,104 @@
# No as a Service (NaaS) # No as a Service (NaaS) - Multilingual Edition
A simple Sinatra-based web service that provides creative "no" responses in both HTML and JSON formats. A Sinatra-based web service that provides creative "no" responses in multiple languages, with full support for all Nordic languages and automatic language detection.
## Features ## Features
- **Multilingual Support**: 8 languages including all Nordic languages
- **Automatic Language Detection**: Via Accept-Language headers and URL parameters
- **Dual Format Support**: Serves both HTML (for browsers) and JSON (for APIs) - **Dual Format Support**: Serves both HTML (for browsers) and JSON (for APIs)
- **Random Responses**: Returns a random "no" response from a curated list - **Random Responses**: Returns a random "no" response from curated lists per language
- **Modern Styling**: Beautiful glassmorphism design for the HTML interface - **Modern Styling**: Beautiful glassmorphism design with language indicators
- **Health Checks**: Built-in health check endpoint for monitoring - **Health Checks**: Built-in health check endpoint for monitoring
- **Containerized**: Ready for deployment with Podman/Docker - **Containerized**: Ready for deployment with Podman/Docker
## Supported Languages
- 🇬🇧 **English** (`en`) - Default
- 🇳🇴 **Norwegian** (`no`) - Norsk
- 🇸🇪 **Swedish** (`sv`) - Svenska
- 🇩🇰 **Danish** (`da`) - Dansk
- 🇮🇸 **Icelandic** (`is`) - Íslenska
- 🇫🇮 **Finnish** (`fi`) - Suomi
- 🇫🇴 **Faroese** (`fo`) - Føroyskt
- 🏔️ **Northern Sami** (`smi`) - Sámegiella
## API Endpoints ## API Endpoints
- `GET /` - Returns a random "no" (HTML by default, JSON with Accept header) - `GET /` - Returns a random "no" (HTML by default, JSON with Accept header)
- `GET /api/no` - Explicit JSON endpoint with additional metadata - `GET /api/no` - Explicit JSON endpoint with language detection
- `GET /api/no/:lang` - Get response in specific language (e.g., `/api/no/no`)
- `GET /languages` - List all available languages
- `GET /health` - Health check endpoint - `GET /health` - Health check endpoint
- `GET /*` - Catch-all that returns "no" for any other path - `GET /*` - Catch-all that returns "no" for any other path
## Content Negotiation ## Language Detection
The root endpoint (`/`) supports content negotiation: The service automatically detects language preference through:
- Browser requests get HTML
- Requests with `Accept: application/json` get JSON 1. **URL Parameter**: `?lang=no` (highest priority)
2. **Accept-Language Header**: Browser language preferences
3. **Default Fallback**: English (`en`)
Language family mapping is supported:
- `nb`, `nn``no` (Norwegian variants)
- `sv-*``sv` (Swedish variants)
- `da-*``da` (Danish variants)
- etc.
## Usage Examples ## Usage Examples
### HTML (Browser) ### HTML (Browser)
``` ```bash
# Default language (auto-detected)
curl http://localhost:4567/ curl http://localhost:4567/
# Specific language via parameter
curl http://localhost:4567/?lang=no
# Language via Accept-Language header
curl -H "Accept-Language: sv-SE,sv;q=0.9" http://localhost:4567/
``` ```
### JSON ### JSON API
```bash ```bash
# Using Accept header # Auto-detected language
curl -H "Accept: application/json" http://localhost:4567/ curl -H "Accept: application/json" http://localhost:4567/
# Using explicit API endpoint # Specific language endpoint
curl http://localhost:4567/api/no curl http://localhost:4567/api/no/is
# With language parameter
curl http://localhost:4567/api/no?lang=fi
# List available languages
curl http://localhost:4567/languages
``` ```
### Sample JSON Response ### Sample JSON Responses
**Basic Response:**
```json ```json
{ {
"answer": "Absolutely not", "answer": "Absolutt ikke",
"language": "no",
"language_name": "Norsk (Norwegian)",
"timestamp": "2025-06-06T12:00:00Z", "timestamp": "2025-06-06T12:00:00Z",
"service": "No as a Service", "service": "No as a Service",
"version": "1.0.0" "version": "2.0.0"
}
```
**Languages List:**
```json
{
"languages": [
{"code": "en", "name": "English"},
{"code": "no", "name": "Norsk (Norwegian)"},
{"code": "sv", "name": "Svenska (Swedish)"}
],
"default": "en",
"timestamp": "2025-06-06T12:00:00Z"
} }
``` ```
@ -101,6 +154,7 @@ The service includes a health check endpoint at `/health` that returns:
```json ```json
{ {
"status": "healthy", "status": "healthy",
"languages_loaded": 8,
"timestamp": "2025-06-06T12:00:00Z" "timestamp": "2025-06-06T12:00:00Z"
} }
``` ```
@ -110,11 +164,38 @@ The service includes a health check endpoint at `/health` that returns:
``` ```
. .
├── app.rb # Main Sinatra application ├── app.rb # Main Sinatra application
├── responses.yml # Multilingual response data
├── Gemfile # Ruby dependencies ├── Gemfile # Ruby dependencies
├── Containerfile # Container build instructions ├── Containerfile # Container build instructions
└── README.md # This file └── README.md # This file
``` ```
## Adding New Languages
To add a new language, edit `responses.yml`:
```yaml
xx: # Language code
name: "Language Name"
responses:
- "Response 1"
- "Response 2"
# ... more responses
```
Then optionally add language family mapping in `app.rb` if needed.
## Nordic Language Features
Each Nordic language includes culturally appropriate expressions:
- **Norwegian**: "Når kua flyger" (When cows fly)
- **Swedish**: "När grisar flyger" (When pigs fly)
- **Danish**: "Når svin flyver" (When pigs fly)
- **Icelandic**: "Þegar svín fljúga" (When pigs fly)
- **Finnish**: "Kun lehmät lentää" (When cows fly)
- **Faroese**: "Tá svín fljúgva" (When pigs fly)
- **Northern Sami**: "Go vuonji liddjo" (When reindeer fly)
## License ## License
This project is released under the MIT License. Feel free to use it for your negative response needs! This project is released under the MIT License. Perfect for all your multilingual negative response needs!. Feel free to use it for your negative response needs!

266
app.rb
View file

@ -1,81 +1,180 @@
require 'sinatra' require 'sinatra'
require 'json' require 'json'
require 'yaml'
# Configure Sinatra # Configure Sinatra
set :port, ENV['PORT'] || 4567 set :port, ENV['PORT'] || 4567
set :bind, '0.0.0.0' set :bind, '0.0.0.0'
# Array of creative "no" responses # Load responses from YAML file
NO_RESPONSES = [ RESPONSES = YAML.load_file('responses.yml')
"No", DEFAULT_LANGUAGE = 'en'
"Nope",
"Absolutely not",
"Not a chance",
"Never",
"No way",
"Negative",
"Not happening",
"Nah",
"No siree",
"Not today",
"Dream on",
"Over my dead body",
"When pigs fly",
"Not in a million years",
"Forget about it",
"No dice",
"Not on your life",
"Fat chance",
"No can do"
].freeze
# Helper method to get a random "no" # Helper method to get a random "no" response
def get_no_response def get_no_response(lang = DEFAULT_LANGUAGE)
NO_RESPONSES.sample language = RESPONSES[lang] || RESPONSES[DEFAULT_LANGUAGE]
language['responses'].sample
end
# Helper method to get language name
def get_language_name(lang)
language = RESPONSES[lang] || RESPONSES[DEFAULT_LANGUAGE]
language['name']
end
# Helper method to get available languages
def get_available_languages
RESPONSES.map { |code, data| { code: code, name: data['name'] } }
end
# Helper method to determine language from request
def determine_language(params, headers)
# Priority: URL parameter > Accept-Language header > default
return params[:lang] if params[:lang] && RESPONSES[params[:lang]]
# Parse Accept-Language header
if headers['HTTP_ACCEPT_LANGUAGE']
accepted_langs = headers['HTTP_ACCEPT_LANGUAGE']
.split(',')
.map { |lang| lang.split(';').first.downcase.strip }
# Check for exact matches first
accepted_langs.each do |lang|
return lang if RESPONSES[lang]
end
# Check for language family matches (e.g., 'nb' or 'nn' -> 'no')
accepted_langs.each do |lang|
case lang
when /^nb|nn/ # Norwegian Bokmål/Nynorsk
return 'no'
when /^sv/ # Swedish variants
return 'sv'
when /^da/ # Danish variants
return 'da'
when /^is/ # Icelandic variants
return 'is'
when /^fi/ # Finnish variants
return 'fi'
when /^fo/ # Faroese variants
return 'fo'
when /^se|smi/ # Sami variants
return 'smi'
end
end
end
DEFAULT_LANGUAGE
end end
# Root endpoint - returns HTML by default # Root endpoint - returns HTML by default
get '/' do get '/' do
no_response = get_no_response lang = determine_language(params, request.env)
no_response = get_no_response(lang)
case request.accept.first&.to_s case request.accept.first&.to_s
when /application\/json/ when /application\/json/
content_type :json content_type :json
{ answer: no_response, timestamp: Time.now.iso8601 }.to_json {
answer: no_response,
language: lang,
language_name: get_language_name(lang),
timestamp: Time.now.iso8601
}.to_json
else else
content_type :html content_type :html
erb :index, locals: { no_response: no_response } erb :index, locals: {
no_response: no_response,
language: lang,
language_name: get_language_name(lang),
available_languages: get_available_languages
}
end end
end end
# Explicit JSON endpoint # Languages endpoint - list available languages
get '/api/no' do get '/languages' do
content_type :json content_type :json
{ {
answer: get_no_response, languages: get_available_languages,
default: DEFAULT_LANGUAGE,
timestamp: Time.now.iso8601
}.to_json
end
# Explicit JSON endpoint with language support
get '/api/no' do
lang = determine_language(params, request.env)
content_type :json
{
answer: get_no_response(lang),
language: lang,
language_name: get_language_name(lang),
timestamp: Time.now.iso8601, timestamp: Time.now.iso8601,
service: "No as a Service", service: "No as a Service",
version: "1.0.0" version: "2.0.0"
}.to_json
end
# Language-specific endpoint
get '/api/no/:lang' do
lang = params[:lang]
unless RESPONSES[lang]
content_type :json
status 404
return {
error: "Language not supported",
requested: lang,
available: get_available_languages.map { |l| l[:code] },
timestamp: Time.now.iso8601
}.to_json
end
content_type :json
{
answer: get_no_response(lang),
language: lang,
language_name: get_language_name(lang),
timestamp: Time.now.iso8601,
service: "No as a Service",
version: "2.0.0"
}.to_json }.to_json
end end
# Health check endpoint # Health check endpoint
get '/health' do get '/health' do
content_type :json content_type :json
{ status: "healthy", timestamp: Time.now.iso8601 }.to_json {
status: "healthy",
languages_loaded: RESPONSES.keys.size,
timestamp: Time.now.iso8601
}.to_json
end end
# Catch-all route for any other endpoint # Catch-all route for any other endpoint
get '/*' do get '/*' do
no_response = get_no_response lang = determine_language(params, request.env)
no_response = get_no_response(lang)
case request.accept.first&.to_s case request.accept.first&.to_s
when /application\/json/ when /application\/json/
content_type :json content_type :json
{ answer: no_response, timestamp: Time.now.iso8601 }.to_json {
answer: no_response,
language: lang,
language_name: get_language_name(lang),
timestamp: Time.now.iso8601
}.to_json
else else
content_type :html content_type :html
erb :index, locals: { no_response: no_response } erb :index, locals: {
no_response: no_response,
language: lang,
language_name: get_language_name(lang),
available_languages: get_available_languages
}
end end
end end
@ -83,7 +182,7 @@ __END__
@@index @@index
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="<%= language %>">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -107,7 +206,7 @@ __END__
.container { .container {
text-align: center; text-align: center;
max-width: 600px; max-width: 700px;
padding: 2rem; padding: 2rem;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
@ -124,10 +223,19 @@ __END__
.subtitle { .subtitle {
font-size: 1.2rem; font-size: 1.2rem;
margin-bottom: 2rem; margin-bottom: 1rem;
opacity: 0.9; opacity: 0.9;
} }
.language-info {
font-size: 1rem;
margin-bottom: 2rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
display: inline-block;
}
.no-response { .no-response {
font-size: 4rem; font-size: 4rem;
font-weight: bold; font-weight: bold;
@ -138,6 +246,16 @@ __END__
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
border-radius: 15px; border-radius: 15px;
border: 2px solid rgba(255, 107, 107, 0.3); border: 2px solid rgba(255, 107, 107, 0.3);
word-break: break-word;
}
.controls {
margin: 2rem 0;
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
align-items: center;
} }
.refresh-btn { .refresh-btn {
@ -157,6 +275,21 @@ __END__
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4);
} }
.language-selector {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 0.5rem 1rem;
border-radius: 25px;
font-size: 1rem;
cursor: pointer;
}
.language-selector option {
background: #444;
color: white;
}
.api-info { .api-info {
margin-top: 2rem; margin-top: 2rem;
padding: 1rem; padding: 1rem;
@ -172,6 +305,12 @@ __END__
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
} }
.nordic-flag {
display: inline-block;
margin-left: 0.5rem;
font-size: 1.2em;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.container { .container {
margin: 1rem; margin: 1rem;
@ -185,28 +324,63 @@ __END__
.no-response { .no-response {
font-size: 2.5rem; font-size: 2.5rem;
} }
.controls {
flex-direction: column;
gap: 0.5rem;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>No as a Service</h1> <h1>No as a Service</h1>
<p class="subtitle">The definitive API for negative responses</p> <p class="subtitle">The definitive multilingual API for negative responses</p>
<div class="language-info">
Currently in: <strong><%= language_name %></strong>
<% if language == 'no' %><span class="nordic-flag">🇳🇴</span><% end %>
<% if language == 'sv' %><span class="nordic-flag">🇸🇪</span><% end %>
<% if language == 'da' %><span class="nordic-flag">🇩🇰</span><% end %>
<% if language == 'is' %><span class="nordic-flag">🇮🇸</span><% end %>
<% if language == 'fi' %><span class="nordic-flag">🇫🇮</span><% end %>
<% if language == 'fo' %><span class="nordic-flag">🇫🇴</span><% end %>
</div>
<div class="no-response"> <div class="no-response">
<%= no_response %> <%= no_response %>
</div> </div>
<button class="refresh-btn" onclick="location.reload()"> <div class="controls">
Get Another No <button class="refresh-btn" onclick="location.reload()">
</button> Get Another No
</button>
<select class="language-selector" onchange="changeLanguage(this.value)">
<% available_languages.each do |lang| %>
<option value="<%= lang[:code] %>" <%= 'selected' if lang[:code] == language %>>
<%= lang[:name] %>
</option>
<% end %>
</select>
</div>
<div class="api-info"> <div class="api-info">
<h3>API Usage</h3> <h3>API Usage</h3>
<p>JSON API: <code>GET /api/no</code></p> <p>JSON API: <code>GET /api/no</code> or <code>GET /api/no/:lang</code></p>
<p>Accept JSON: <code>curl -H "Accept: application/json" /</code></p> <p>Languages: <code>GET /languages</code></p>
<p>URL Parameter: <code>?lang=<%= language %></code></p>
<p>Accept-Language header supported</p>
<p>Health Check: <code>GET /health</code></p> <p>Health Check: <code>GET /health</code></p>
</div> </div>
</div> </div>
<script>
function changeLanguage(lang) {
const url = new URL(window.location);
url.searchParams.set('lang', lang);
window.location.href = url.toString();
}
</script>
</body> </body>
</html> </html>

192
responses.yml Normal file
View file

@ -0,0 +1,192 @@
---
en:
name: "English"
responses:
- "No"
- "Nope"
- "Absolutely not"
- "Not a chance"
- "Never"
- "No way"
- "Negative"
- "Not happening"
- "Nah"
- "No siree"
- "Not today"
- "Dream on"
- "Over my dead body"
- "When pigs fly"
- "Not in a million years"
- "Forget about it"
- "No dice"
- "Not on your life"
- "Fat chance"
- "No can do"
no:
name: "Norsk (Norwegian)"
responses:
- "Nei"
- "Neida"
- "Aldri"
- "Absolutt ikke"
- "Ikke en sjanse"
- "Glem det"
- "Ikke tale om"
- "Over mitt lik"
- "Aldri i livet"
- "Ikke i dag"
- "Bare glem det"
- "Ikke på din salighet"
- "Når kua flyger"
- "Ikke noe sjanse"
- "Ikke faen"
- "Drøm videre"
- "Negativt"
- "Ikke aktuelt"
- "Ikke på min vakt"
- "Bare drøm"
sv:
name: "Svenska (Swedish)"
responses:
- "Nej"
- "Nä"
- "Aldrig"
- "Absolut inte"
- "Inte en chans"
- "Glöm det"
- "Inte på tal"
- "Över min döda kropp"
- "Aldrig i livet"
- "Inte idag"
- "Bara glöm det"
- "När grisar flyger"
- "Inte någon chans"
- "Drömma vidare"
- "Negativt"
- "Inte aktuellt"
- "Aldrig någonsin"
- "Inte på min vakttid"
- "Bara dröm"
- "Knappast"
da:
name: "Dansk (Danish)"
responses:
- "Nej"
- "Nah"
- "Aldrig"
- "Absolut ikke"
- "Ikke en chance"
- "Glem det"
- "Ikke tale om"
- "Over mit lig"
- "Aldrig i livet"
- "Ikke i dag"
- "Bare glem det"
- "Når svin flyver"
- "Ikke nogen chance"
- "Bare drøm"
- "Negativt"
- "Ikke aktuelt"
- "Aldrig nogensinde"
- "Ikke på min vagt"
- "Glem det bare"
- "Nej tak"
is:
name: "Íslenska (Icelandic)"
responses:
- "Nei"
- "Aldrei"
- "Algjörlega ekki"
- "Engin möguleiki"
- "Gleyma því"
- "Ekki í dag"
- "Aldrei í lífinu"
- "Yfir mína lík"
- "Ekki um það að ræða"
- "Bara gleyma því"
- "Þegar svín fljúga"
- "Engin von"
- "Aldrei nogurra"
- "Bara dreyma"
- "Neikvætt"
- "Ekki í boði"
- "Aldrei aftur"
- "Ekki á mínum degi"
- "Gleyma þessu"
- "Nei takk"
fi:
name: "Suomi (Finnish)"
responses:
- "Ei"
- "Eipä"
- "Ei koskaan"
- "Ehdottomasti ei"
- "Ei mahista"
- "Unohda se"
- "Ei tule kuuloonkaan"
- "Yli minun ruumiini"
- "Ei ikinä elämässä"
- "Ei tänään"
- "Bara unohda"
- "Kun lehmät lentää"
- "Ei mitään mahdollisuuksia"
- "Haaveile vaan"
- "Negatiivinen"
- "Ei ole ajankohtaista"
- "Ei ikinä"
- "Ei minun vuorollani"
- "Ei tuloa"
- "Kiitos ei"
fo:
name: "Føroyskt (Faroese)"
responses:
- "Nei"
- "Aldri"
- "Alls ikki"
- "Onki møguleiki"
- "Gleym tað"
- "Ikki í dag"
- "Aldri í lívinum"
- "Yvir mítt lík"
- "Ikki tað"
- "Bara gleym tað"
- "Tá svín fljúgva"
- "Ongin von"
- "Aldri aftur"
- "Bara droyma"
- "Ikki møguligt"
- "Ikki aktuelt"
- "Aldri meir"
- "Ikki á mínum døgum"
- "Gleym hettar"
- "Nei takk"
smi:
name: "Sámegiella (Northern Sami)"
responses:
- "Ii"
- "Ale goassege"
- "Ale ipmašii"
- "Ii oktage vejolašvuohta"
- "Vajálduhte dan"
- "Ii odne"
- "Ale goassege eallimis"
- "Mu rupmasa ala"
- "Ii leat sáhka"
- "Dušše vajálduhte"
- "Go vuonji liddjo"
- "Ii oktage doaivva"
- "Ale fas"
- "Dušše nágot"
- "Ii vejolašlávvu"
- "Ii leat áigeguovdil"
- "Ale eanet"
- "Ii mu beaivvis"
- "Vajálduhte dán"
- "Ii giitu"