diff --git a/src/MyWebLog.Tests/ConvertersTests.fs b/src/MyWebLog.Tests/ConvertersTests.fs new file mode 100644 index 0000000..3699943 --- /dev/null +++ b/src/MyWebLog.Tests/ConvertersTests.fs @@ -0,0 +1,265 @@ +module ConvertersTests + +open Expecto +open MyWebLog +open MyWebLog.Converters.Json +open Newtonsoft.Json + +/// Unit tests for the CategoryIdConverter type +let categoryIdConverterTests = testList "CategoryIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(CategoryIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(CategoryId "test-cat-id", opts) + Expect.equal after "\"test-cat-id\"" "Category ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"test-cat-id\"", opts) + Expect.equal after (CategoryId "test-cat-id") "Category ID not serialized incorrectly" + } +] + +/// Unit tests for the CommentIdConverter type +let commentIdConverterTests = testList "CommentIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(CommentIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(CommentId "test-id", opts) + Expect.equal after "\"test-id\"" "Comment ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"my-test\"", opts) + Expect.equal after (CommentId "my-test") "Comment ID deserialized incorrectly" + } +] + +/// Unit tests for the CommentStatusConverter type +let commentStatusConverterTests = testList "CommentStatusConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(CommentStatusConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(Approved, opts) + Expect.equal after "\"Approved\"" "Comment status serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"Spam\"", opts) + Expect.equal after Spam "Comment status deserialized incorrectly" + } +] + +/// Unit tests for the CustomFeedIdConverter type +let customFeedIdConverterTests = testList "CustomFeedIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(CustomFeedIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(CustomFeedId "my-feed", opts) + Expect.equal after "\"my-feed\"" "Custom feed ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"feed-me\"", opts) + Expect.equal after (CustomFeedId "feed-me") "Custom feed ID deserialized incorrectly" + } +] + +/// Unit tests for the CustomFeedSourceConverter type +let customFeedSourceConverterTests = testList "CustomFeedSourceConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(CustomFeedSourceConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(Category (CategoryId "abc-123"), opts) + Expect.equal after "\"category:abc-123\"" "Custom feed source serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"tag:testing\"", opts) + Expect.equal after (Tag "testing") "Custom feed source deserialized incorrectly" + } +] + +/// Unit tests for the ExplicitRating type +let explicitRatingConverterTests = testList "ExplicitRatingConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(ExplicitRatingConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(Yes, opts) + Expect.equal after "\"yes\"" "Explicit rating serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"clean\"", opts) + Expect.equal after Clean "Explicit rating deserialized incorrectly" + } +] + +/// Unit tests for the MarkupText type +let markupTextConverterTests = testList "MarkupTextConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(MarkupTextConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(Html "

test

", opts) + Expect.equal after "\"HTML:

test

\"" "Markup text serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"Markdown: #### test\"", opts) + Expect.equal after (Markdown "#### test") "Markup text deserialized incorrectly" + } +] + +/// Unit tests for the PermalinkConverter type +let permalinkConverterTests = testList "PermalinkConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(PermalinkConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(Permalink "2022/test", opts) + Expect.equal after "\"2022/test\"" "Permalink serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"2023/unit.html\"", opts) + Expect.equal after (Permalink "2023/unit.html") "Permalink deserialized incorrectly" + } +] + +/// Unit tests for the PageIdConverter type +let pageIdConverterTests = testList "PageIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(PageIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(PageId "test-page", opts) + Expect.equal after "\"test-page\"" "Page ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"page-test\"", opts) + Expect.equal after (PageId "page-test") "Page ID deserialized incorrectly" + } +] + +/// Unit tests for the PodcastMedium type +let podcastMediumConverterTests = testList "PodcastMediumConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(PodcastMediumConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(Audiobook, opts) + Expect.equal after "\"audiobook\"" "Podcast medium serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"newsletter\"", opts) + Expect.equal after Newsletter "Podcast medium deserialized incorrectly" + } +] + +/// Unit tests for the PostIdConverter type +let postIdConverterTests = testList "PostIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(PostIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(PostId "test-post", opts) + Expect.equal after "\"test-post\"" "Post ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"post-test\"", opts) + Expect.equal after (PostId "post-test") "Post ID deserialized incorrectly" + } +] + +/// Unit tests for the TagMapIdConverter type +let tagMapIdConverterTests = testList "TagMapIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(TagMapIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(TagMapId "test-map", opts) + Expect.equal after "\"test-map\"" "Tag map ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"map-test\"", opts) + Expect.equal after (TagMapId "map-test") "Tag map ID deserialized incorrectly" + } +] + +/// Unit tests for the ThemeAssetIdConverter type +let themeAssetIdConverterTests = testList "ThemeAssetIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(ThemeAssetIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(ThemeAssetId (ThemeId "test", "unit.jpg"), opts) + Expect.equal after "\"test/unit.jpg\"" "Theme asset ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"theme/test.png\"", opts) + Expect.equal after (ThemeAssetId (ThemeId "theme", "test.png")) "Theme asset ID deserialized incorrectly" + } +] + +/// Unit tests for the ThemeIdConverter type +let themeIdConverterTests = testList "ThemeIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(ThemeIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(ThemeId "test-theme", opts) + Expect.equal after "\"test-theme\"" "Theme ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"theme-test\"", opts) + Expect.equal after (ThemeId "theme-test") "Theme ID deserialized incorrectly" + } +] + +/// Unit tests for the UploadIdConverter type +let uploadIdConverterTests = testList "UploadIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(UploadIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(UploadId "test-up", opts) + Expect.equal after "\"test-up\"" "Upload ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"up-test\"", opts) + Expect.equal after (UploadId "up-test") "Upload ID deserialized incorrectly" + } +] + +/// Unit tests for the WebLogIdConverter type +let webLogIdConverterTests = testList "WebLogIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(WebLogIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(WebLogId "test-web", opts) + Expect.equal after "\"test-web\"" "Web log ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"web-test\"", opts) + Expect.equal after (WebLogId "web-test") "Web log ID deserialized incorrectly" + } +] + +/// Unit tests for the WebLogUserIdConverter type +let webLogUserIdConverterTests = testList "WebLogUserIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(WebLogUserIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(WebLogUserId "test-user", opts) + Expect.equal after "\"test-user\"" "Web log user ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"user-test\"", opts) + Expect.equal after (WebLogUserId "user-test") "Web log user ID deserialized incorrectly" + } +] + +/// All tests for the Data.Converters file +let all = testList "Converters" [ + categoryIdConverterTests + commentIdConverterTests + commentStatusConverterTests + customFeedIdConverterTests + customFeedSourceConverterTests + explicitRatingConverterTests + markupTextConverterTests + permalinkConverterTests + pageIdConverterTests + podcastMediumConverterTests + postIdConverterTests + tagMapIdConverterTests + themeAssetIdConverterTests + themeIdConverterTests + uploadIdConverterTests + webLogIdConverterTests + webLogUserIdConverterTests +] diff --git a/src/MyWebLog.Tests/MyWebLog.Tests.fsproj b/src/MyWebLog.Tests/MyWebLog.Tests.fsproj index d415d68..b33f3fd 100644 --- a/src/MyWebLog.Tests/MyWebLog.Tests.fsproj +++ b/src/MyWebLog.Tests/MyWebLog.Tests.fsproj @@ -8,7 +8,9 @@ + + diff --git a/src/MyWebLog.Tests/Program.fs b/src/MyWebLog.Tests/Program.fs index 5755a72..7db606f 100644 --- a/src/MyWebLog.Tests/Program.fs +++ b/src/MyWebLog.Tests/Program.fs @@ -2,6 +2,7 @@ let allTests = testList "MyWebLog" [ testList "Domain" [ SupportTypesTests.all; DataTypesTests.all; ViewModelsTests.all ] + testList "Data" [ ConvertersTests.all ] ] [] diff --git a/src/MyWebLog.Tests/root-weblog.json b/src/MyWebLog.Tests/root-weblog.json new file mode 100644 index 0000000..7577c81 --- /dev/null +++ b/src/MyWebLog.Tests/root-weblog.json @@ -0,0 +1 @@ +{"WebLog":{"Id":"uSitJEuD3UyzWC9jgOHc8g","Name":"Root WebLog","Slug":"root-weblog","Subtitle":"This is the main one","DefaultPage":"posts","PostsPerPage":9,"ThemeId":"default","UrlBase":"http://localhost:8081","TimeZone":"America/Denver","Rss":{"IsFeedEnabled":true,"FeedName":"feed","ItemsInFeed":7,"IsCategoryEnabled":true,"IsTagEnabled":true,"Copyright":"CC40-NC-BY","CustomFeeds":[{"Id":"isPQ6drbDEydxohQzaiYtQ","Source":"tag:podcast","Path":"podcast-feed","Podcast":{"Title":"Root Podcast","ItemsInFeed":23,"Summary":"All things that happen in the domain root","DisplayedAuthor":"Podcaster Extraordinaire","Email":"podcaster@example.com","ImageUrl":"images/cover-art.png","AppleCategory":"Fiction","AppleSubcategory":"Drama","Explicit":"no","DefaultMediaType":"audio/mpeg","MediaBaseUrl":"https://media.example.com/root/","PodcastGuid":"10fd7f79-c719-4e1d-9da7-10405dd4fd96","FundingUrl":"https://example.com/support-us","FundingText":"Support Our Work","Medium":"newsletter"}}]},"AutoHtmx":true,"Uploads":"Database","RedirectRules":[]},"Users":[{"Id":"5EM2rimH9kONpmd2zQkiVA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Email":"root@example.com","FirstName":"Root","LastName":"Owner","PreferredName":"Admin","PasswordHash":"AQAAAAIAAYagAAAAEEnq9J9lKZoMQZaTOJHKIQo44skDdzDigzqS+o6myMop38YuHfm/vNs9b/WpYjsOxg==","AccessLevel":"Administrator","CreatedOn":"2024-01-20T21:49:03Z","LastSeenOn":"2024-01-20T22:25:03Z"},{"Id":"GPbJaSOwTkKt14ZKYyveKA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Email":"editor@example.com","FirstName":"Ed","LastName":"It-Or","PreferredName":"Edits","PasswordHash":"AQAAAAIAAYagAAAAEA8E3NwJkZO+q35FTmUT0wMNB8IpBOSVACKQcccXpaWfZJMRmZzjPEzd4j/f9h+rEA==","AccessLevel":"Editor","CreatedOn":"2024-01-20T21:58:42Z"},{"Id":"iIRNLSeY0EanxRPyqGuwVg","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Email":"author@example.com","FirstName":"Author","LastName":"Dude","PreferredName":"Mister","PasswordHash":"AQAAAAIAAYagAAAAEBYNf1sR/pjaX2dZgqlvpH/Tqpz2h/CG3rsk/wH2ReTysjpK/gxSqht7IULWXM7KHQ==","Url":"https://example.com/author","AccessLevel":"Author","CreatedOn":"2024-01-20T21:53:27Z"}],"Theme":{"Id":"default","Name":"myWebLog Default Theme","Version":"2.1.0","Templates":[{"Name":"single-post","Text":"{%- assign post = model.posts | first -%}\n

{{ post.title }}

\n

\n {% if post.published_on -%}\n Published {{ post.published_on | date: \"dddd, MMMM d, yyyy\" }}\n at {{ post.published_on | date: \"h:mm tt\" | downcase }}\n {%- else -%}\n **DRAFT**\n {% endif %}\n by {{ model.authors | value: post.author_id }}\n {%- if is_editor or is_author and user_id == post.author_id %}\n • Edit Post\n {%- endif %}\n

\n
\n
\n
{{ post.text }}
\n {%- assign cat_count = post.category_ids | size -%}\n {% if cat_count > 0 -%}\n

\n Categorized under\n {% for cat_id in post.category_ids -%}\n {% assign cat = categories | where: \"Id\", cat_id | first %}\n \n \n {{ cat.name }}\n \n \n {% unless forloop.last %} • {% endunless %}\n {%- endfor %}\n

\n {%- endif %}\n {%- assign tag_count = post.tags | size -%}\n {% if tag_count > 0 -%}\n

\n Tagged\n {% for tag in post.tags %}\n \n {{ tag }}\n \n {% unless forloop.last %} • {% endunless %}\n {%- endfor %}\n

\n {%- endif %}\n
\n
\n \n
\n
\n"},{"Name":"single-page","Text":"

{{ page.title }}

\n
{{ page.text }}
\n"},{"Name":"layout","Text":"\n\n \n \n \n {{ page_title | strip_html }}{% if page_title %} « {% endif %}{{ web_log.name | strip_html }}\n {% page_head -%}\n
\n \n
\n
\n {% if messages %}\n
\n {% for msg in messages %}\n \n {% endfor %}\n
\n {% endif %}\n {{ content }} \n
\n
\n
\n
\n myWebLog\n
\n
\n \n\n"},{"Name":"index","Text":"{%- if is_category or is_tag %}\n

{{ page_title }}

\n {%- if subtitle %}

{{ subtitle }}

{% endif -%}\n{% endif %}\n{%- assign post_count = model.posts | size -%}\n{%- if post_count > 0 %}\n
\n {%- for post in model.posts %}\n
\n

\n \n {{ post.title }}\n \n

\n

\n Published on {{ post.published_on | date: \"MMMM d, yyyy\" }}\n at {{ post.published_on | date: \"h:mmtt\" | downcase }}\n by {{ model.authors | value: post.author_id }}\n {{ post.text }}\n {%- assign category_count = post.category_ids | size -%}\n {%- assign tag_count = post.tags | size -%}\n {% if category_count > 0 or tag_count > 0 %}\n

\n

\n {%- if category_count > 0 -%}\n Categorized under:\n {% for cat in post.category_ids -%}\n {%- assign this_cat = categories | where: \"Id\", cat | first -%}\n {{ this_cat.name }}{% unless forloop.last %}, {% endunless %}\n {%- assign cat_names = this_cat.name | concat: cat_names -%}\n {%- endfor -%}\n {%- assign cat_names = \"\" -%}\n
\n {% endif -%}\n {%- if tag_count > 0 %}\n Tagged: {{ post.tags | join: \", \" }}\n {% endif -%}\n

\n {% endif %}\n
\n
\n {% endfor %}\n
\n \n{%- else %}\n

No posts found

\n{%- endif %}\n"}]},"Assets":[{"Id":"default/style.css","UpdatedOn":"2023-07-02T20:36:28Z","Data":"Lm1lc3NhZ2VzIHsKICBtYXgtd2lkdGg6IDYwcmVtOwogIG1hcmdpbjogYXV0bzsKfQpibG9ja3F1b3RlIHsKICBib3JkZXItbGVmdDogc29saWQgNHB4IGxpZ2h0Z3JheTsKICBwYWRkaW5nLWxlZnQ6IDFyZW07Cn0KLml0ZW0tbWV0YSB7CiAgZm9udC1zaXplOiAxLjFyZW07CiAgZm9udC13ZWlnaHQ6IG5vcm1hbDs7Cn0KLml0ZW0tbWV0YTo6YmVmb3JlIHsKICBjb250ZW50OiAiwrsiOwogIHZlcnRpY2FsLWFsaWduOiB0ZXh0LXRvcDsKfQphOmxpbmssIGE6dmlzaXRlZCB7CiAgdGV4dC1kZWNvcmF0aW9uOiBub25lOwp9CmE6aG92ZXIgewogIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lOwp9Cg=="}],"Categories":[{"Id":"S5JflPsJ9EG7gA2LD4m92A","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Name":"Favorites","Slug":"favorites","Description":"Favorite posts"},{"Id":"jw6N69YtTEWVHAO33jHU-w","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Name":"Spitball","Slug":"spitball","Description":"Posts that may or may not work"},{"Id":"ScVpyu1e7UiP7bDdge3ZEw","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Name":"Moonshot","Slug":"moonshot","ParentId":"jw6N69YtTEWVHAO33jHU-w"}],"TagMappings":[{"Id":"Icm027noqE-rPHKZA98vAw","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Tag":"f#","UrlValue":"f-sharp"},{"Id":"GdryXh-S0kGsNBs2RIacGA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Tag":"ghoti","UrlValue":"fish"}],"Pages":[{"Id":"hgc_BLEZ50SoAWLuPNISvA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Title":"Page Title","Permalink":"a-cool-page.html","PublishedOn":"2024-01-20T22:14:28Z","UpdatedOn":"2024-01-20T22:14:28Z","IsInPageList":false,"Text":"

A Cool Page

\n

It really is cool!

\n","Metadata":[{"Name":"Cool","Value":"true"},{"Name":"Warm","Value":"false"}],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:14:28Z","Text":"Markdown: # A Cool Page\n\nIt really is cool!"}]},{"Id":"KouRjvSmm0Wz6TMD8xf67A","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Title":"Yet Another Page","Permalink":"this-again.html","PublishedOn":"2024-01-20T22:15:08Z","UpdatedOn":"2024-01-20T22:15:08Z","IsInPageList":true,"Text":"

Page 2

\n\n

It's a trip.","Metadata":[],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:15:08Z","Text":"HTML:

Page 2

\n\n

It's a trip."}]}],"Posts":[{"Id":"RCsCU2puYEmkpzotoi8p4g","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Status":"Published","Title":"Test Post 1","Permalink":"2024/test-post-1.html","PublishedOn":"2024-01-20T22:17:29Z","UpdatedOn":"2024-01-20T22:17:29Z","Text":"

Introduction

\n

Visit my web site or my local page for more information.

\n","CategoryIds":["ScVpyu1e7UiP7bDdge3ZEw"],"Tags":["f#","howdy","intro"],"Metadata":[],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:17:29Z","Text":"Markdown: ## Introduction\n\nVisit [my web site](https://example.com) or [my local page](/a-fine-page.html) for more information."}]},{"Id":"osxMfWGlAkyugUbJ1-xD1g","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Status":"Published","Title":"Episode 1","Permalink":"2024/episode-1.html","PublishedOn":"2024-01-20T22:24:01Z","UpdatedOn":"2024-01-20T22:24:01Z","Text":"

It's the launch of my new podcast - y'all come listen!","CategoryIds":["S5JflPsJ9EG7gA2LD4m92A"],"Tags":["general","podcast"],"Episode":{"Media":"episode-1.mp3","Length":124302,"Duration":"0:12:22","ImageUrl":"images/ep1-cover.png","Subtitle":"An introduction to this podcast","Explicit":"clean","ChapterFile":"uploads/chapters.json","TranscriptUrl":"uploads/transcript.srt","TranscriptType":"application/srt","TranscriptLang":"en","TranscriptCaptions":true,"SeasonNumber":1,"SeasonDescription":"The First Season","EpisodeNumber":1.0,"EpisodeDescription":"The first episode ever!"},"Metadata":[{"Name":"Density","Value":"Non-existent"},{"Name":"Intensity","Value":"Low"}],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:24:01Z","Text":"HTML:

It's the launch of my new podcast - y'all come listen!"}]},{"Id":"l4_Eh4aFO06SqqJjOymNzA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Status":"Published","Title":"Episode 2","Permalink":"2024/episode-2.html","PublishedOn":"2024-01-20T22:31:32Z","UpdatedOn":"2024-01-20T22:31:32Z","Text":"

m i n i m a l","CategoryIds":[],"Tags":["podcast"],"Episode":{"Media":"episode-2.mp3","Length":12873952,"Duration":"1:03:24","SeasonNumber":1,"SeasonDescription":"The First Season","EpisodeNumber":2.0,"EpisodeDescription":"A long update"},"Metadata":[],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:31:32Z","Text":"HTML:

m i n i m a l"}]},{"Id":"QweKbWQiOkqqrjEdgP9wwg","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Status":"Published","Title":"Something May Happen","Permalink":"2024/something.html","PublishedOn":"2024-01-20T22:32:59Z","UpdatedOn":"2024-01-20T22:32:59Z","Text":"

Hmm

","CategoryIds":["jw6N69YtTEWVHAO33jHU-w"],"Tags":["f#","ghoti","speculation"],"Metadata":[],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:32:59Z","Text":"HTML:

Hmm

"}]}],"Uploads":[]} \ No newline at end of file