From b3bde6350d23cced8e2277420b895fd17fb66625 Mon Sep 17 00:00:00 2001 From: sabine Date: Sat, 17 Feb 2024 23:56:12 +0100 Subject: [PATCH] hierarchy: category > task > recipe - URL we serve this under is docs/cookbook/[task slug]/[recipe slug] - multiple recipes per task - recipe page displays links to other recipes for the same task --- CONTRIBUTING.md | 7 ++ data/cookbook/cookbook_categories.yml | 61 ++++++++++ .../get-today-date/00-stdlib.md | 5 - .../00-mirage-crypto-rng.md | 35 ------ .../00-mirage-crypto-rng.md | 34 ++++++ .../00-mirage-crypto-rng.md | 42 +++++++ src/global/url.ml | 2 +- src/ocamlorg_data/data.ml | 13 +- src/ocamlorg_data/data.mli | 17 ++- src/ocamlorg_frontend/pages/cookbook.eml | 53 +++++---- .../pages/cookbook_recipe.eml | 33 +++++- src/ocamlorg_web/lib/handler.ml | 17 ++- src/ocamlorg_web/lib/router.ml | 4 +- tool/ood-gen/lib/cookbook.ml | 112 ++++++++++++++---- 14 files changed, 330 insertions(+), 105 deletions(-) create mode 100644 data/cookbook/cookbook_categories.yml rename data/cookbook/{ => date-and-time}/get-today-date/00-stdlib.md (90%) delete mode 100644 data/cookbook/generate-random-numbers/00-mirage-crypto-rng.md create mode 100644 data/cookbook/generate-random-values/generate-random-numbers/00-mirage-crypto-rng.md create mode 100644 data/cookbook/generate-random-values/generate-random-strings-and-arrays/00-mirage-crypto-rng.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 478755c127..96a0101546 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,7 @@ We've provided a list of community-driven content below. When adding content to - [Success Stories](#content-success-story) - [Academic and Industrial Users](#content-user) - [OCaml Books](#content-book) +- [OCaml Cookbook Recipes](#content-cookbook) - [Community Meetups](#content-meetup) - [Upcoming Events](#content-upcoming_event) - [The OCaml Changelog](#content-changelog) @@ -112,6 +113,12 @@ You can add a new industrial user by creating a new Markdown file in [data/acade You can add a new OCaml book by creating a new Markdown file in [data/books/](data/books/). For instance: [ocaml-from-the-very-beginning.md](data/books/ocaml-from-the-very-beginning.md). +### Add a Recipe to the OCaml Cookbook + +The OCaml cookbook is a place where OCaml developers share how to solve practical-minded tasks in OCaml using packages from the OCaml ecosystem. To contribute a recipe, you need to choose a category for the recipe (see [cookbook_categories.yml](https://)) and write a markdown file with YAML header (for an example, see []). + +Every cookbook recipe consists of one or more files with explanations. In the markdown body of the document, you can provide further context or links to the packages used. But: keep this short, the point is to get people back to building things, not keep them here reading. + ### Add A meetup > Contribute to the [Community Meetups](https://ocaml.org/community). diff --git a/data/cookbook/cookbook_categories.yml b/data/cookbook/cookbook_categories.yml new file mode 100644 index 0000000000..dc404f6661 --- /dev/null +++ b/data/cookbook/cookbook_categories.yml @@ -0,0 +1,61 @@ +cookbook_categories: +- title: Command Line + folder: command-line + tasks: [] +- title: Compression + folder: compression + tasks: [] +- title: Concurrency + folder: concurrency + tasks: [] +- title: Cryptography + folder: cryptography + tasks: [] +- title: Database + folder: database + tasks: [] +- title: Date and Time + folder: date-and-time + tasks: + - title: Get Today's Date + folder: get-today-date +- title: Debugging + folder: debugging + tasks: [] +- title: Encoding + folder: encoding + tasks: [] +- title: File System + folder: file-system + tasks: [] +- title: Garbage Collector + folder: garbage-collector + tasks: [] +- title: Generate Random Values + folder: generate-random-values + tasks: + - title: Generate random numbers + folder: generate-random-numbers + - title: Generate random strings and arrays + folder: generate-random-strings-and-arrays +- title: Hash Tables and Maps + folder: hash-tables-and-maps + tasks: [] +- title: Mathematics + folder: mathematics + tasks: [] +- title: Networking + folder: networking + tasks: [] +- title: Operating System + folder: operating-system + tasks: [] +- title: Sorting + folder: sorting + tasks: [] +- title: Text Processing + folder: text-processing + tasks: [] +- title: Web Programming + folder: web-programming + tasks: [] diff --git a/data/cookbook/get-today-date/00-stdlib.md b/data/cookbook/date-and-time/get-today-date/00-stdlib.md similarity index 90% rename from data/cookbook/get-today-date/00-stdlib.md rename to data/cookbook/date-and-time/get-today-date/00-stdlib.md index cc8113e41b..f3e341b255 100644 --- a/data/cookbook/get-today-date/00-stdlib.md +++ b/data/cookbook/date-and-time/get-today-date/00-stdlib.md @@ -1,7 +1,4 @@ --- -title: Get Current Date (Stdlib) -problem: "You need to find the year, month, and day values for today's date." -category: "Date and Time" packages: [] sections: - filename: main.ml @@ -23,8 +20,6 @@ sections: year month day;; --- -## Discussion - - **Understanding `Unix.localtime` and `Unix.time`:** The `Unix.localtime` function converts a timestamp obtained from `Unix.time` (which returns the current time since the Unix epoch) into a local time, represented by a `tm` structure. This structure includes fields like `tm_year`, `tm_mon`, and `tm_mday` for year, month, and day, respectively. - **Month and Year Adjustments:** In OCaml's `Unix` module, the month is zero-indexed (0 for January, 11 for December), and the year is the number of years since 1900. Don't forget to adjust these values to get a human-readable date. - **Alternative Libraries:** For more complex date-time operations, consider using external libraries like `calendar` or `timedesc`, which offer more functionalities like time zone handling and date arithmetic. diff --git a/data/cookbook/generate-random-numbers/00-mirage-crypto-rng.md b/data/cookbook/generate-random-numbers/00-mirage-crypto-rng.md deleted file mode 100644 index ae52e1338f..0000000000 --- a/data/cookbook/generate-random-numbers/00-mirage-crypto-rng.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Generate random numbers (mirage-crypto-rng) -problem: "You need to find the year, month, and day values for today's date." -category: "Generate Random Values" -packages: ["mirage-crypto-rng"] -sections: -- filename: main.ml - language: ocaml - code_blocks: - - explanation: "" - code: | - open Mirage_crypto_rng - - explanation: | - Initialize the RNG with the default entropy source. - code: | - Mirage_crypto_rng_unix.initialize (); - - explanation: Generate a random byte array of length 8. - code: | - let random_bytes = Mirage_crypto_rng.generate 8 in - - explanation: Convert the random bytes to a hex string (for display purposes). - code: | - let hex_string = Cstruct.to_string random_bytes - |> Hex.of_string - |> Hex.show in - Printf.printf "Random Bytes (Hex): %s\n" hex_string; - - explanation: | - Example: Converting the first 4 bytes to an int (assuming little endian). - code: | - let random_int = - let open Cstruct.LE in - get_uint32 random_bytes 0 |> Int32.to_int - in - Printf.printf "Random Int: %d\n" random_int - ---- diff --git a/data/cookbook/generate-random-values/generate-random-numbers/00-mirage-crypto-rng.md b/data/cookbook/generate-random-values/generate-random-numbers/00-mirage-crypto-rng.md new file mode 100644 index 0000000000..81e20ba030 --- /dev/null +++ b/data/cookbook/generate-random-values/generate-random-numbers/00-mirage-crypto-rng.md @@ -0,0 +1,34 @@ +--- +packages: +- name: "mirage-crypto-rng" + version: "0.11.2" +- name: "randomconv" + version: "0.1.3" +sections: +- filename: main.ml + language: ocaml + code_blocks: + - explanation: | + Initialize the RNG with the default entropy source. + code: | + let () = Mirage_crypto_rng_unix.initialize + (module Mirage_crypto_rng.Fortuna) + - explanation: | + Generate a `Cstruct.t` with random data. + code: | + let cstruct n = Mirage_crypto_rng.generate n + - explanation: | + Use the `Randomconv` module from the `randomconv` package to convert to various integer types. + code: | + let int8 () = Randomconv.int8 cstruct + let int16 () = Randomconv.int16 cstruct + let int32 () = Randomconv.int32 cstruct + let int64 () = Randomconv.int64 cstruct + - explanation: + Generate a random `int` or `float` less than or equal to `max`. + code: | + let int ?max () = + Randomconv.int ?bound:max cstruct + let float ?max () = + Randomconv.float ?bound:max cstruct +--- diff --git a/data/cookbook/generate-random-values/generate-random-strings-and-arrays/00-mirage-crypto-rng.md b/data/cookbook/generate-random-values/generate-random-strings-and-arrays/00-mirage-crypto-rng.md new file mode 100644 index 0000000000..dd734b1fad --- /dev/null +++ b/data/cookbook/generate-random-values/generate-random-strings-and-arrays/00-mirage-crypto-rng.md @@ -0,0 +1,42 @@ +--- +packages: +- name: "mirage-crypto-rng" + version: "0.11.2" +- name: "randomconv" + version: "0.1.3" +sections: +- filename: main.ml + language: ocaml + code_blocks: + - explanation: | + Initialize the RNG with the default entropy source. + code: | + let () = Mirage_crypto_rng_unix.initialize + (module Mirage_crypto_rng.Fortuna) + - explanation: | + Generate a `Cstruct.t` with random data. + code: | + let cstruct n = Mirage_crypto_rng.generate n + - explanation: | + To get a random `char`, convert a random `int8`: + code: | + let char () = Char.chr (int8 ()) + - explanation: | + Use `Cstruct`'s conversion methods to obtain random byte arrays, bigarrays or strings. + code: | + let bytes n = cstruct n |> Cstruct.to_bytes + let bigarray n = cstruct n |> Cstruct.to_bigarray + let string n = cstruct n |> Cstruct.to_string + - explanation: | + You can also create alphanumeric random characters: + code: | + let alphanum () = + Char.chr (48 + Randomconv.int ~bound:74 cstruct) + - explanation: | + Wrap your random value generator into a sequence to generate as many random values as you want. + code: | + let seq gen = + Seq.unfold (fun () -> Some (gen (), ())) () + let list n gen = + seq gen |> Seq.take n |> List.of_seq +--- diff --git a/src/global/url.ml b/src/global/url.ml index fd3bea8845..3e3a1acea2 100644 --- a/src/global/url.ml +++ b/src/global/url.ml @@ -84,7 +84,7 @@ let exercises = "/exercises" let outreachy = "/outreachy" let logos = "/logo" let cookbook = "/docs/cookbook" -let cookbook_recipe recipe = "/docs/cookbook/" ^ recipe +let cookbook_recipe ~task_slug slug = "/docs/cookbook/" ^ task_slug ^ "/" ^ slug let github_opam_file package_name package_version = Printf.sprintf diff --git a/src/ocamlorg_data/data.ml b/src/ocamlorg_data/data.ml index 31dd3a4e5f..3984b6a489 100644 --- a/src/ocamlorg_data/data.ml +++ b/src/ocamlorg_data/data.ml @@ -217,5 +217,16 @@ end module Cookbook = struct include Cookbook - let get_by_slug slug = List.find_opt (fun x -> String.equal slug x.slug) all + let get_tasks_by_category ~category_slug = + tasks + |> List.filter (fun (x : task) -> + String.equal category_slug x.category.slug) + + let get_by_task ~task_slug = + all |> List.filter (fun (x : t) -> String.equal task_slug x.task.slug) + + let get_by_slug ~task_slug slug = + List.find_opt + (fun x -> String.equal slug x.slug && String.equal task_slug x.task.slug) + all end diff --git a/src/ocamlorg_data/data.mli b/src/ocamlorg_data/data.mli index 06d2ff32e6..ef8feec623 100644 --- a/src/ocamlorg_data/data.mli +++ b/src/ocamlorg_data/data.mli @@ -518,7 +518,10 @@ module Governance : sig end module Cookbook : sig + type category = { title : string; slug : string } + type task = { title : string; slug : string; category : category } type code_block_with_explanation = { code : string; explanation : string } + type package = { name : string; version : string } type section = { filename : string; @@ -529,15 +532,17 @@ module Cookbook : sig type t = { slug : string; - group_id : string; - title : string; - problem : string; - category : string; - packages : string list; + filepath : string; + task : task; + packages : package list; sections : section list; body_html : string; } + val categories : category list + val tasks : task list val all : t list - val get_by_slug : string -> t option + val get_tasks_by_category : category_slug:string -> task list + val get_by_task : task_slug:string -> t list + val get_by_slug : task_slug:string -> string -> t option end diff --git a/src/ocamlorg_frontend/pages/cookbook.eml b/src/ocamlorg_frontend/pages/cookbook.eml index 5008401a14..3fdcc000b3 100644 --- a/src/ocamlorg_frontend/pages/cookbook.eml +++ b/src/ocamlorg_frontend/pages/cookbook.eml @@ -1,4 +1,4 @@ -let render (recipes: Data.Cookbook.t list) = +let render (categories: Data.Cookbook.category list) = Learn_layout.single_column_layout ~title:"OCaml Cookbook" ~description:"A collection of recipes to get things done in OCaml." @@ -14,31 +14,44 @@ Learn_layout.single_column_layout common tasks using packages from the OCaml ecosystem.

-
-
-
Recipe
-
Packages Used
-
Category
-
-% recipes |> List.iter (fun (recipe : Data.Cookbook.t) -> -
- - <%s recipe.title %> - -
-% (if List.length recipe.packages = 0 then - - +
+ <% categories |> List.iter (fun (category: Data.Cookbook.category) -> %> +
+
+

<%s category.title %>

+ <% let tasks = Data.Cookbook.get_tasks_by_category ~category_slug:category.slug in %> +% if tasks = [] then ( +

There's nothing in this category yet, maybe you want to + contribute a recipe! +

% ); -% recipe.packages |> List.iter (fun (package: string) -> - <%s package %> -%); +
+% if List.length tasks > 0 then ( +
+
+
Task
+
Packages Used
-
- <%s recipe.category %> +
+ <% tasks |> List.iter (fun (task : Data.Cookbook.task) -> %> + <% let recipe = List.nth (Data.Cookbook.get_by_task ~task_slug:task.slug) 0 in %> +
+ + <%s recipe.task.title %> + +
+ <%s if List.length recipe.packages = 0 then "-" else "" %> + <% recipe.packages |> List.iter (fun (package: Data.Cookbook.package) -> %> + <%s package.name %>.<%s package.version %> + <% ); %> +
+
+ <% ); %>
% );
+ <% ); %>
diff --git a/src/ocamlorg_frontend/pages/cookbook_recipe.eml b/src/ocamlorg_frontend/pages/cookbook_recipe.eml index 0365cc0751..69a9e1f289 100644 --- a/src/ocamlorg_frontend/pages/cookbook_recipe.eml +++ b/src/ocamlorg_frontend/pages/cookbook_recipe.eml @@ -1,4 +1,4 @@ -let render (recipe: Data.Cookbook.t) = +let render (recipe: Data.Cookbook.t) (other_recipes_for_this_task: Data.Cookbook.t list) = Learn_layout.single_column_layout ~title:"OCaml Cookbook" ~description:"A collection of recipes to get things done in OCaml." @@ -8,9 +8,8 @@ Learn_layout.single_column_layout
-

<%s recipe.title %>

-

Problem

-

<%s recipe.problem %>

+

<%s recipe.task.title %>

+

Files

% recipe.sections |> List.iter (fun (section: Data.Cookbook.section) ->
@@ -23,7 +22,33 @@ Learn_layout.single_column_layout % );
% ); +% if String.length recipe.body_html > 0 then ( +

Discussion

<%s! recipe.body_html %> +% ); +

Recipe not working? Want to Upgrade a Package Version of this Recipe?

+ Open an issue + or contribute to this recipe! +% if List.length other_recipes_for_this_task > 0 then ( +

Other Recipes for this Task

+ +% );
diff --git a/src/ocamlorg_web/lib/handler.ml b/src/ocamlorg_web/lib/handler.ml index 153b90dfe7..b17462c9a7 100644 --- a/src/ocamlorg_web/lib/handler.ml +++ b/src/ocamlorg_web/lib/handler.ml @@ -448,15 +448,20 @@ let exercises req = Dream.html (Ocamlorg_frontend.exercises ?difficulty_level filtered_exercises) let cookbook _req = - let recipes = Data.Cookbook.all in - Dream.html (Ocamlorg_frontend.cookbook recipes) + let categories = Data.Cookbook.categories in + Dream.html (Ocamlorg_frontend.cookbook categories) let cookbook_recipe req = - let slug = Dream.param req "id" in - let? recipe = - List.find_opt (fun x -> x.Data.Cookbook.slug = slug) Data.Cookbook.all + let task_slug = Dream.param req "task_slug" in + let slug = Dream.param req "slug" in + let? recipe = Data.Cookbook.get_by_slug ~task_slug slug in + let other_recipes_for_this_task = + Data.Cookbook.all + |> List.filter (fun (c : Data.Cookbook.t) -> + c.task.slug = recipe.task.slug && c.slug <> recipe.slug) in - Dream.html (Ocamlorg_frontend.cookbook_recipe recipe) + Dream.html + (Ocamlorg_frontend.cookbook_recipe recipe other_recipes_for_this_task) let outreachy _req = Dream.html (Ocamlorg_frontend.outreachy Data.Outreachy.all) diff --git a/src/ocamlorg_web/lib/router.ml b/src/ocamlorg_web/lib/router.ml index 38c8d9e3d9..41254e75aa 100644 --- a/src/ocamlorg_web/lib/router.ml +++ b/src/ocamlorg_web/lib/router.ml @@ -31,7 +31,9 @@ let page_routes t = Dream.get Url.learn_guides Handler.learn_guides; Dream.get Url.platform Handler.platform; Dream.get Url.cookbook Handler.cookbook; - Dream.get (Url.cookbook_recipe ":id") Handler.cookbook_recipe; + Dream.get + (Url.cookbook_recipe ~task_slug:":task_slug" ":slug") + Handler.cookbook_recipe; Dream.get Url.community Handler.community; Dream.get Url.changelog Handler.changelog; Dream.get (Url.changelog_entry ":id") Handler.changelog_entry; diff --git a/tool/ood-gen/lib/cookbook.ml b/tool/ood-gen/lib/cookbook.ml index a2b989ff9d..e6fc7dbd84 100644 --- a/tool/ood-gen/lib/cookbook.ml +++ b/tool/ood-gen/lib/cookbook.ml @@ -1,3 +1,18 @@ +type task_metadata = { title : string; folder : string } [@@deriving of_yaml] + +type category_metadata = { + title : string; + folder : string; + tasks : task_metadata list; +} +[@@deriving of_yaml] + +type category = { title : string; slug : string } +[@@deriving show { with_path = false }] + +type task = { title : string; slug : string; category : category } +[@@deriving show { with_path = false }] + type code_block_with_explanation = { code : string; explanation : string } [@@deriving of_yaml, show { with_path = false }] @@ -8,13 +23,10 @@ type metadata_section = { } [@@deriving of_yaml] -type metadata = { - title : string; - problem : string; - category : string; - packages : string list; - sections : metadata_section list; -} +type package = { name : string; version : string } +[@@deriving of_yaml, show { with_path = false }] + +type metadata = { packages : package list; sections : metadata_section list } [@@deriving of_yaml] type section = { @@ -26,27 +38,33 @@ type section = { [@@deriving show { with_path = false }] type t = { - group_id : string; + filepath : string; slug : string; - title : string; - problem : string; - category : string; - packages : string list; + task : task; + packages : package list; sections : section list; body_html : string; } [@@deriving stable_record ~version:metadata - ~remove:[ slug; group_id; body_html ] + ~remove:[ slug; filepath; task; body_html ] ~modify:[ sections ], show { with_path = false }] -let decode (fpath, (head, body)) = - (* TODO: use body and put that somewhere *) - let group_id = Filename.basename (Filename.dirname fpath) in +let decode (tasks : task list) (fpath, (head, body)) = + let ( let* ) = Result.bind in let name = Filename.basename (Filename.remove_extension fpath) in - let id = String.sub name 3 (String.length name - 3) in - let slug = group_id ^ "-" ^ id in + let category_slug = List.nth (String.split_on_char '/' fpath) 1 in + let task_slug = List.nth (String.split_on_char '/' fpath) 2 in + let* task = + try Ok (tasks |> List.find (fun (c : task) -> c.slug = task_slug)) + with Not_found -> + Error + (`Msg + (fpath ^ ": failed to find task '" ^ task_slug ^ "' in category " + ^ category_slug ^ " in cookbook_categories.yml")) + in + let slug = String.sub name 3 (String.length name - 3) in let metadata = metadata_of_yaml head in let render_markdown str = @@ -80,16 +98,54 @@ let decode (fpath, (head, body)) = Result.map (fun metadata -> - of_metadata ~slug ~group_id ~body_html ~modify_sections metadata) + of_metadata ~slug ~filepath:fpath ~task ~body_html ~modify_sections + metadata) metadata +let all_categories_and_tasks () = + let categories = + Utils.yaml_sequence_file category_metadata_of_yaml + "cookbook/cookbook_categories.yml" + in + let tasks = ref [] in + let categories = + categories + |> List.map (fun (c : category_metadata) : category -> + let category = { slug = c.folder; title = c.title } in + let category_tasks = + c.tasks + |> List.map (fun (t : task_metadata) : task -> + { title = t.title; slug = t.folder; category }) + in + tasks := category_tasks @ !tasks; + category) + |> List.rev + in + (categories, !tasks) + let all () = - Utils.map_files decode "cookbook/*/*.md" + let _, tasks = all_categories_and_tasks () in + Utils.map_files (decode tasks) "cookbook/*/*/*.md" |> List.sort (fun a b -> String.compare b.slug a.slug) + |> List.rev let template () = + let categories, tasks = all_categories_and_tasks () in Format.asprintf {| +type category = + { title : string + ; slug : string + } +type task = + { title : string + ; slug : string + ; category : category + } +type package = + { name : string + ; version : string + } type code_block_with_explanation = { code : string ; explanation : string @@ -102,16 +158,20 @@ type section = } type t = { slug: string - ; group_id: string - ; title : string - ; problem : string - ; category : string - ; packages : string list + ; filepath: string + ; task : task + ; packages : package list ; sections : section list ; body_html : string } - + +let categories = %a +let tasks = %a let all = %a |} + (Fmt.brackets (Fmt.list pp_category ~sep:Fmt.semi)) + categories + (Fmt.brackets (Fmt.list pp_task ~sep:Fmt.semi)) + tasks (Fmt.brackets (Fmt.list pp ~sep:Fmt.semi)) (all ())