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 (
+
+
-
- <%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 %>
+
+ <% ); %>
% );
+ <% ); %>
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
+
+% other_recipes_for_this_task |> List.iter (fun (recipe : Data.Cookbook.t) ->
+ -
+
+
+% );
+
+% );
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 ())