commit c018ea3c0e8af812eb48a26a5948683fab22e498 Author: mitteneer Date: Tue Apr 2 11:48:20 2024 -0400 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbcae4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +zig-out/ +zig-cache/ +*.core +static/ +.jetzig diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..363e94a --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 miteneer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee2d386 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Zuletzt +**Zuletzt** gives you the statistics of your music listening habits. + +Inspired by Last.fm, Maloja, and Lastfmstats.com. + + +**Z**uletzt is written in **Z**ig as a means of learning the +language, reintroducing myself to programming, and combining +the functionality of the aforementioned inspirations. + +Zuletzt means "last" in German. + +Licensed under MIT. \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..d80454e --- /dev/null +++ b/build.zig @@ -0,0 +1,52 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "zuletzt", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + // Example dependency: + const iguanas_dep = b.dependency("iguanas", .{ .optimize = optimize, .target = target }); + exe.root_module.addImport("iguanas", iguanas_dep.module("iguanas")); + + // All dependencies **must** be added to imports above this line. + + try jetzig.jetzigInit(b, exe, .{}); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| run_cmd.addArgs(args); + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const lib_unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..4d80a70 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,40 @@ +.{ + .name = "zuletzt", + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + .jetzig = .{ + .url = "https://github.com/jetzig-framework/jetzig/archive/800b72eeb9b146d4ee4901e164fa691dc5332fb5.tar.gz", + .hash = "122031827c1329f8dbd9135e7463c27cc09e9fcbbebde6f2a8411ee122984afe0a4c", + }, + .iguanas = .{ + .url = "https://github.com/jetzig-framework/iguanas/archive/89c2abf29de0bc31054a9a6feac5a6a83bab0459.tar.gz", + .hash = "12202fd319a5ab4e124b00e8ddea474d07c19c4e005d77b6c29fc44860904ea01a5c", + }, + }, + .paths = .{ + // This makes *all* files, recursively, included in this package. It is generally + // better to explicitly list the files and directories instead, to insure that + // fetching from tarballs, file system paths, and version control all result + // in the same contents hash. + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..0ccc92b Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/jetzig.png b/public/jetzig.png new file mode 100644 index 0000000..314d70c Binary files /dev/null and b/public/jetzig.png differ diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..1755d47 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,10 @@ +/* Root stylesheet. Load into your Zmpl template with: + * + * + * + */ + +.message { + font-weight: bold; + font-size: 3rem; +} diff --git a/public/zmpl.png b/public/zmpl.png new file mode 100644 index 0000000..0d2f8d3 Binary files /dev/null and b/public/zmpl.png differ diff --git a/src/app/middleware/DemoMiddleware.zig b/src/app/middleware/DemoMiddleware.zig new file mode 100644 index 0000000..a6758d2 --- /dev/null +++ b/src/app/middleware/DemoMiddleware.zig @@ -0,0 +1,65 @@ +/// Demo middleware. Assign middleware by declaring `pub const middleware` in the +/// `jetzig_options` defined in your application's `src/main.zig`. +/// +/// Middleware is called before and after the request, providing full access to the active +/// request, allowing you to execute any custom code for logging, tracking, inserting response +/// headers, etc. +/// +/// This middleware is configured in the demo app's `src/main.zig`: +/// +/// ``` +/// pub const jetzig_options = struct { +/// pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")}; +/// }; +/// ``` +const std = @import("std"); +const jetzig = @import("jetzig"); + +/// Define any custom data fields you want to store here. Assigning to these fields in the `init` +/// function allows you to access them in various middleware callbacks defined below, where they +/// can also be modified. +my_custom_value: []const u8, + +const Self = @This(); + +/// Initialize middleware. +pub fn init(request: *jetzig.http.Request) !*Self { + var middleware = try request.allocator.create(Self); + middleware.my_custom_value = "initial value"; + return middleware; +} + +/// Invoked immediately after the request is received but before it has started processing. +/// Any calls to `request.render` or `request.redirect` will prevent further processing of the +/// request, including any other middleware in the chain. +pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { + try request.server.logger.DEBUG( + "[DemoMiddleware:afterRequest] my_custom_value: {s}", + .{self.my_custom_value}, + ); + self.my_custom_value = @tagName(request.method); +} + +/// Invoked immediately before the response renders to the client. +/// The response can be modified here if needed. +pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { + try request.server.logger.DEBUG( + "[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}", + .{ self.my_custom_value, @tagName(response.status_code) }, + ); +} + +/// Invoked immediately after the response has been finalized and sent to the client. +/// Response data can be accessed for logging, but any modifications will have no impact. +pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { + _ = self; + _ = response; + try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{}); +} + +/// Invoked after `afterResponse` is called. Use this function to do any clean-up. +/// Note that `request.allocator` is an arena allocator, so any allocations are automatically +/// freed before the next request starts processing. +pub fn deinit(self: *Self, request: *jetzig.http.Request) void { + request.allocator.destroy(self); +} diff --git a/src/app/views/root.zig b/src/app/views/root.zig new file mode 100644 index 0000000..0fa146c --- /dev/null +++ b/src/app/views/root.zig @@ -0,0 +1,43 @@ +const jetzig = @import("jetzig"); + +/// `src/app/views/root.zig` represents the root URL `/` +/// The `index` view function is invoked when when the HTTP verb is `GET`. +/// Other view types are invoked either by passing a resource ID value (e.g. `/1234`) or by using +/// a different HTTP verb: +/// +/// GET / => index(request, data) +/// GET /1234 => get(id, request, data) +/// POST / => post(request, data) +/// PUT /1234 => put(id, request, data) +/// PATCH /1234 => patch(id, request, data) +/// DELETE /1234 => delete(id, request, data) +pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + // The first call to `data.object()` or `data.array()` sets the root response data value. + // JSON requests return a JSON string representation of the root data value. + // Zmpl templates can access all values in the root data value. + var root = try data.object(); + + // Add a string to the root object. + try root.put("message", data.string("Welcome to Jetzig!")); + + // Request params have the same type as a `data.object()` so they can be inserted them + // directly into the response data. Fetch `http://localhost:8080/?message=hello` to set the + // param. JSON data is also accepted when the `content-type: application/json` header is + // present. + const params = try request.params(); + + if (params.get("message")) |value| { + try root.put("message_param", value); + } + + // Set arbitrary response headers as required. `content-type` is automatically assigned for + // HTML, JSON responses. + // + // Static files located in `public/` in the root of your project directory are accessible + // from the root path (e.g. `public/jetzig.png`) is available at `/jetzig.png` and the + // content type is inferred from the extension using MIME types. + try request.response.headers.append("x-example-header", "example header value"); + + // Render the response and set the response code. + return request.render(.ok); +} diff --git a/src/app/views/root/_content.zmpl b/src/app/views/root/_content.zmpl new file mode 100644 index 0000000..6d3a665 --- /dev/null +++ b/src/app/views/root/_content.zmpl @@ -0,0 +1,18 @@ +// Renders the `message` response data value. +

{.message}

+ +
+ +
+ + + + +
+ +
Visit jetzig.dev to get started. +
Join our Discord server and introduce yourself:
+
+ https://discord.gg/eufqssz7X6 +
+
diff --git a/src/app/views/root/index.zmpl b/src/app/views/root/index.zmpl new file mode 100644 index 0000000..cccb8d8 --- /dev/null +++ b/src/app/views/root/index.zmpl @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ // If present, renders the `message_param` response data value, add `?message=hello` to the + // URL to see the output: +

{.message_param}

+ + // Renders `src/app/views/root/_content.zmpl` with the same template data available: +
{^root/content}
+
+ + diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..6c5d3a0 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,96 @@ +const std = @import("std"); + +pub const jetzig = @import("jetzig"); + +pub const routes = @import("routes").routes; + +// Override default settings in `jetzig.config` here: +pub const jetzig_options = struct { + /// Middleware chain. Add any custom middleware here, or use middleware provided in + /// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`). + pub const middleware: []const type = &.{ + // htmx middleware skips layouts when `HX-Target` header is present and issues + // `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called. + jetzig.middleware.HtmxMiddleware, + // Demo middleware included with new projects. Remove once you are familiar with Jetzig's + // middleware system. + @import("app/middleware/DemoMiddleware.zig"), + }; + + // Maximum bytes to allow in request body. + // pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16); + + // Maximum filesize for `public/` content. + // pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20); + + // Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`). + // pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18); + + // Path relative to cwd() to serve public content from. Symlinks are not followed. + // pub const public_content_path = "public"; + + // HTTP buffer. Must be large enough to store all headers. This should typically not be modified. + // pub const http_buffer_size: usize = std.math.pow(usize, 2, 16); + + // Set custom fragments for rendering markdown templates. Any values will fall back to + // defaults provided by Zmd (https://github.com/bobf/zmd/blob/main/src/zmd/html.zig). + pub const markdown_fragments = struct { + pub const root = .{ + "
", + "
", + }; + pub const h1 = .{ + "

", + "

", + }; + pub const h2 = .{ + "

", + "

", + }; + pub const h3 = .{ + "

", + "

", + }; + pub const paragraph = .{ + "

", + "

", + }; + pub const code = .{ + "", + "", + }; + + pub const unordered_list = .{ + "", + }; + + pub const ordered_list = .{ + "", + }; + + pub fn block(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\
{s}
+ , .{ node.meta, node.content }); + } + + pub fn link(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\{1s} + , .{ node.href.?, node.title.? }); + } + }; +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer std.debug.assert(gpa.deinit() == .ok); + const allocator = gpa.allocator(); + + const app = try jetzig.init(allocator); + defer app.deinit(); + + try app.start(comptime jetzig.route(routes)); +}