diff --git a/actions/sim7-url-shortener.yaml b/actions/sim7-url-shortener.yaml new file mode 100644 index 00000000..f28dca9f --- /dev/null +++ b/actions/sim7-url-shortener.yaml @@ -0,0 +1,15 @@ +name: local/url-shortener +description: > + Build a URL shortener service in Python using Flask. Include endpoints + to shorten a URL (POST /shorten) and redirect via short code (GET /). + Use an in-memory dictionary for storage. Generate short codes using + base62 encoding of an auto-incrementing counter. Include input validation + for URLs, a simple HTML landing page, and requirements.txt. +strategy_actor: anthropic/claude-sonnet-4-20250514 +execution_actor: anthropic/claude-sonnet-4-20250514 +definition_of_done: > + An app.py has Flask routes for POST /shorten and GET /. + Short codes are generated via base62 encoding. + URL validation rejects malformed inputs. + An index.html landing page allows users to paste and shorten URLs. + In-memory dict stores mappings. A requirements.txt lists dependencies. diff --git a/simulations/sim7/app.py b/simulations/sim7/app.py new file mode 100644 index 00000000..69bf047d --- /dev/null +++ b/simulations/sim7/app.py @@ -0,0 +1,155 @@ +from flask import Flask, request, redirect, render_template, jsonify, url_for +from urllib.parse import urlparse +import re +import threading + +app = Flask(__name__) + +# In-memory storage +url_storage = {} +counter = 0 +counter_lock = threading.Lock() + +# Base62 alphabet +BASE62_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +def base62_encode(number): + """Convert a number to base62 string.""" + if number == 0: + return BASE62_ALPHABET[0] + + result = "" + while number > 0: + result = BASE62_ALPHABET[number % 62] + result + number //= 62 + return result + +def base62_decode(string): + """Convert a base62 string to number.""" + number = 0 + for char in string: + number = number * 62 + BASE62_ALPHABET.index(char) + return number + +def is_valid_url(url): + """Validate URL format and structure.""" + try: + result = urlparse(url) + # Check if URL has scheme and netloc + if not all([result.scheme, result.netloc]): + return False + + # Check for valid scheme + if result.scheme not in ['http', 'https']: + return False + + # Basic domain validation + domain_pattern = re.compile( + r'^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' + ) + + return bool(domain_pattern.match(result.netloc.split(':')[0])) + + except Exception: + return False + +def generate_short_code(): + """Generate next short code using thread-safe counter.""" + global counter + with counter_lock: + counter += 1 + return base62_encode(counter) + +@app.route('/') +def index(): + """Serve the main landing page.""" + return render_template('index.html') + +@app.route('/shorten', methods=['POST']) +def shorten_url(): + """ + Shorten a URL endpoint. + Accepts JSON or form data with 'url' field. + """ + # Handle both JSON and form data + if request.is_json: + data = request.get_json() + url = data.get('url', '').strip() + else: + url = request.form.get('url', '').strip() + + # Validate URL presence + if not url: + if request.is_json: + return jsonify({'error': 'URL is required'}), 400 + return render_template('index.html', error='URL is required'), 400 + + # Validate URL format + if not is_valid_url(url): + error_msg = 'Invalid URL format. Please enter a valid HTTP or HTTPS URL.' + if request.is_json: + return jsonify({'error': error_msg}), 400 + return render_template('index.html', error=error_msg, url=url), 400 + + # Generate short code and store + short_code = generate_short_code() + url_storage[short_code] = url + + # Build shortened URL + shortened_url = url_for('redirect_url', code=short_code, _external=True) + + if request.is_json: + return jsonify({ + 'shortened_url': shortened_url, + 'short_code': short_code, + 'original_url': url + }), 200 + + return render_template('index.html', + shortened_url=shortened_url, + original_url=url, + success=True) + +@app.route('/') +def redirect_url(code): + """ + Redirect to original URL using short code. + Returns 404 if code not found. + """ + # Validate short code format (base62) + if not code or not all(c in BASE62_ALPHABET for c in code): + return render_template('error.html', + message='Invalid short code format'), 404 + + # Look up original URL + original_url = url_storage.get(code) + + if not original_url: + return render_template('error.html', + message='Short code not found'), 404 + + return redirect(original_url, code=302) + +@app.errorhandler(404) +def not_found(error): + """Handle 404 errors.""" + return render_template('error.html', + message='Page not found'), 404 + +@app.errorhandler(500) +def internal_error(error): + """Handle 500 errors.""" + return render_template('error.html', + message='Internal server error'), 500 + +# Health check endpoint +@app.route('/health') +def health_check(): + """Simple health check endpoint.""" + return jsonify({ + 'status': 'healthy', + 'urls_stored': len(url_storage) + }), 200 + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/simulations/sim7/requirements.txt b/simulations/sim7/requirements.txt new file mode 100644 index 00000000..2f841bdc --- /dev/null +++ b/simulations/sim7/requirements.txt @@ -0,0 +1 @@ +Flask>=2.0.0 \ No newline at end of file diff --git a/simulations/sim7/templates/error.html b/simulations/sim7/templates/error.html new file mode 100644 index 00000000..bc99aafa --- /dev/null +++ b/simulations/sim7/templates/error.html @@ -0,0 +1,58 @@ + + + + + + Error - URL Shortener + + + +
+

+

{{ message }}

+ ← Go Back Home +
+ + \ No newline at end of file diff --git a/simulations/sim7/templates/index.html b/simulations/sim7/templates/index.html new file mode 100644 index 00000000..5f1a23e5 --- /dev/null +++ b/simulations/sim7/templates/index.html @@ -0,0 +1,212 @@ + + + + + + URL Shortener + + + +
+

🔗 URL Shortener

+ + {% if error %} +
+ Error: {{ error }} +
+ {% endif %} + + {% if success and shortened_url %} +
+ Success! Your URL has been shortened. + +
+ Shortened URL:
+ {{ shortened_url }} +
+ +
+ +
+ Original URL: + {{ original_url }} +
+
+ {% endif %} + +
+
+ + +
+ + +
+ + +
+ + + + \ No newline at end of file