Skip to content

Commit

Permalink
Merge pull request #7 from foomo/contentful-v0.5.0
Browse files Browse the repository at this point in the history
Contentful v0.5.1
  • Loading branch information
cvidmar authored Aug 12, 2024
2 parents bd54130 + d5873e4 commit be3d18b
Show file tree
Hide file tree
Showing 45 changed files with 1,349 additions and 575 deletions.
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
- package-ecosystem: gomod
directory: '/'
schedule:
interval: weekly
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: checks

on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
merge_group:
workflow_dispatch:
Expand Down
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ linters:
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
#- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false]
#- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false]
- execinquery # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds [fast: false, auto-fix: false]
#- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
#- exhaustruct # Checks if all structure fields are initialized [fast: false, auto-fix: false]
- exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false]
Expand Down
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,48 @@
# gocontentful
# Gocontentful

A Contentful API code generator for Go. Features:
Gocontentful is a command line tool that generates a set of APIs for the [Go Language](https://go.dev) to interact with a [Contentful](https://www.contentful.com) CMS space.

- Creates and updates a full set of Value Objects from Contentful content model
- Supports CDA, CPA and CMA operations through a simplified, idiomatic Go API based on the model
- Caches entire spaces and handles updates automatically
- Simplifies management/resolution of references
- Adds several utility functions for RichText from/to HTML conversion, assets handling and more
Unlike the plain Contentful API for Go, the Gocontentful API is idiomatic. Go types are provided with names that mirror the content types of the Contentful space, and get/set methods are named after each field.

Full documentation available at [foomo.org](https://www.foomo.org/docs/projects/cms/gocontentful/introduction)
In addition, Gocontentful supports in-memory caching and updates of spaces. This way, the space is always accessible through fast Go function calls, even offline.

## What is Contentful

[Contentful](https://www.contentful.com/) is a content platform (often referred to as headless CMS) for [micro-content](https://www.contentful.com/r/knowledgebase/content-as-a-microservice/).

Unlike traditional CMSes, there's no pages or content trees in Contentful. The data model is built from scratch for the purpose of the consuming application, is completely flexible and can be created and hot-changed through the same Web UI that the content editors use. The model dictates which content types can reference others and the final structure is a graph.

## How applications interact with Contentful

Contentful hosts several APIs that remote applications use to create, retrieve, update and delete content. Content is any of the following:

- **Entries**, each with a content type name and a list of data fields as defined by the developer in the content model editor at Contentful
- **Assets** (images, videos, other binary files)

The Contentful APIs exist as either REST or GraphQL endpoints. Gocontentful only supports the REST APIs.

The REST APIs used to manage and retrieve content use standard HTTP verbs (GET, POST, PUT and DELETE) and a JSON payload for both the request (where needed) and the response.

## What is gocontentful

A golang API code generator that simplifies interacting with a Contentful space. The generated API:

- Supports most of the Contentful APIs to perform all read/write operation on entries and assets
- Hides the complexity of the Contentful REST/JSON APIs behind an idiomatic set of golang functions and methods
- Allows for in-memory caching of an entire Contentful space

## Why we need a Go API generator

While it's perfectly fine to call a REST service and receive data in JSON format, in Go that is not very practical. For each content type, the developer needs to maintan type definitions by hand and decode the JSON coming from the Contentful server into the value object.

In addition, calling a remote API across the Internet each time a piece of content is needed, even multiple times for a single page rendering, can have significant impact on performance.

Gocontentful generates a Go API that handles both issues above and can be regenerated every time the content model changes. The developer never needs to update the types by hand, or deal with the complexity of caching content locally. It all happens auytomatically in the generated client.

> **NOTE** - _How much code does Gocontentful generate? In a real-world production scenario where Gocontentful is in use as of 2024, a space content model with 43 content types of various field counts generates around 65,000 lines of Go code._

[//]: # (Footer)

---

Read the documentation: [Getting Started](docs/00-gettingstarted.md)
2 changes: 1 addition & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ func TestConfigFromYAML(t *testing.T) {
require.Equal(t, "abc123", config.SpaceID)
require.Equal(t, "dev", config.Environment)
require.Equal(t, "v1.0.19", config.RequireVersion)
require.Equal(t, 2, len(config.ContentTypes))
require.Len(t, config.ContentTypes, 2)
}
12 changes: 6 additions & 6 deletions config/vo.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package config

type Config struct {
SpaceID string `yaml:"spaceId,omitempty"`
Environment string `yaml:"environment,omitempty"`
ExportFile string `yaml:"exportFile,omitempty"`
ContentTypes []string `yaml:"contentTypes,omitempty"`
PathTargetPackage string `yaml:"pathTargetPackage,omitempty"`
RequireVersion string `yaml:"requireVersion,omitempty"`
SpaceID string `json:"spaceId,omitempty" yaml:"spaceId,omitempty"`
Environment string `json:"environment,omitempty" yaml:"environment,omitempty"`
ExportFile string `json:"exportFile,omitempty" yaml:"exportFile,omitempty"`
ContentTypes []string `json:"contentTypes,omitempty" yaml:"contentTypes,omitempty"`
PathTargetPackage string `json:"pathTargetPackage,omitempty" yaml:"pathTargetPackage,omitempty"`
RequireVersion string `json:"requireVersion,omitempty" yaml:"requireVersion,omitempty"`
}
49 changes: 31 additions & 18 deletions docs/gettingstarted.md → docs/00-gettingstarted.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
---
sidebar_label: Getting started
sidebar_position: 0
---

# Getting Started

Before you install Gocontentful as a command-line tool to use it in your projects, we suggest you get a taste of how it works by playing with the test API from the Gocontentful repository. This doesn't yet require you to have access to Contentful.
Expand All @@ -15,14 +10,27 @@ in your IDE.
The repository includes an offline representation of a Contentful space that can is used for testing gocontentful
without depending on an online connection and an existing Contentful space.

Create a file in the repository home directory and name it `untracked_test.go`. This ensures it's not tracked by git.
First, open a terminal and install

```bash
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
```

Then `cd` to the repository folder and make sure tests run fine on your machine

```bash
make test
```

Create a test file in the repository home directory (`api_test.go` might be a good choice).
Paste the following into the file:

```go
package main

import (
"testing"
"context"
"testing"

"github.com/foomo/gocontentful/test"
"github.com/foomo/gocontentful/test/testapi"
Expand All @@ -31,15 +39,17 @@ import (
)

func TestTheAPI(t *testing.T) {
testLogger := logrus.StandardLogger()
cc, errClient := testapi.NewOfflineContentfulClient("./test/test-space-export.json",
testLogger := logrus.StandardLogger()
testFile, err := test.GetTestFile("./test/test-space-export.json")
require.NoError(t, err)
cc, errClient := testapi.NewOfflineContentfulClient(testFile,
test.GetContenfulLogger(testLogger),
test.LogDebug,
true)
require.NoError(t, errClient)
prods, errProds := cc.GetAllProduct()
require.NoError(t, errProds)
testLogger.WithField("prods", len(prods)).Info("Loaded products")
true, false)
require.NoError(t, errClient)
prods, errProds := cc.GetAllProduct(context.TODO())
require.NoError(t, errProds)
testLogger.WithField("prods", len(prods)).Info("Loaded products")
}
```

Expand All @@ -55,7 +65,10 @@ value object defined for all content types and functions to convert from/to thos
by gocontentful all you need to do to load all the products is one single line:

```go
prods, errProds := cc.GetAllProduct()
// First get a context, this is needed for all operations
// that potentially require a network connection to Contentful
ctx := context.TODO()
prods, errProds := cc.GetAllProduct(ctx)
```

Open a terminal and from the repository home directory run the test. Your output should looks similar to this:
Expand All @@ -82,7 +95,7 @@ The last line shows that we loaded 4 products. Let's go ahead and play with the
We'll load a specific product and log its name. Add this at the end of the unit test:

```go
prod, errProd := cc.GetProductByID("6dbjWqNd9SqccegcqYq224")
prod, errProd := cc.GetProductByID(ctx, "6dbjWqNd9SqccegcqYq224")
require.NoError(t, errProd)
prodName := prod.ProductName("de")
testLogger.WithField("name", prodName).Info("Product loaded")
Expand All @@ -104,7 +117,7 @@ Let's load the product's brand:

```go
// Get the brand
brandReference := prod.Brand()
brandReference := prod.Brand(ctx)
brand := brandReference.VO.(*testapi.CfBrand)
testLogger.WithField("name", brand.CompanyName()).Info("Brand")
```
Expand All @@ -126,7 +139,7 @@ INFO[0000] Brand name="Normann Copenhage
What if we want to follow the reference the other way around and find out which entries link to this brand?

```go
parentRefs, errParents := brand.GetParents()
parentRefs, errParents := brand.GetParents(ctx)
require.NoError(t, errParents)
testLogger.WithField("parent count", len(parentRefs)).Info("Parents")
for _, parentRef := range parentRefs {
Expand Down
5 changes: 0 additions & 5 deletions docs/setup.md → docs/01-setup.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
---
sidebar_label: Setup
sidebar_position: 1
---

# Gocontentful Setup

## Installation
Expand Down
5 changes: 0 additions & 5 deletions docs/client/index.md → docs/02-client/00-index.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
---
sidebar_label: The Client At Work
sidebar_position: 0
---

# The Client At Work

This section explains how to work with the Gocontentful client to create, retrieve, update and delete entities, such as entries and assets. It also walks you through working with references to handle a graph-like structure in a Contentful space. Finally, it dives into some more specific functionalities, like converting to/from RichText and HTML.
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
---
sidebar_label: Basic client operations
sidebar_position: 1
---

# Basic client operations

Let's consider a very simple use case. You have a Contentful space where you store information
Expand Down Expand Up @@ -39,11 +34,12 @@ The generated files will be in the "people" subdirectory of your project. Your g
client from them:

```go
cc, err := people.NewContentfulClient(YOUR_SPACE_ID, people.ClientModeCDA, YOUR_API_KEY, 1000, contentfulLogger, people.LogDebug,false)
cc, err := people.NewContentfulClient(ctx context.Context, YOUR_SPACE_ID, people.ClientModeCDA, YOUR_API_KEY, 1000, contentfulLogger, people.LogDebug,false)
```

The parameters to pass to NewContentfulClient are:

- _ctx_ (context.Context)
- _spaceID_ (string)
- _clientMode_ (string) supports the constants ClientModeCDA, ClientModeCPA and ClientModeCMA. If you need to operate
on multiple APIs (e.g. one for reading and CMA for writing) you need to get two clients
Expand Down Expand Up @@ -154,7 +150,7 @@ Finally, you can get the parents (AKA referring) entries of either an entry or
an EntryReference with the _GetParents()_ method. This returns a slice of `[]EntryReference`:

```go
(vo *CfPerson) GetParents() (parents []EntryReference, err error)
(vo *CfPerson) GetParents(ctx context.Context) (parents []EntryReference, err error)
(ref *EntryReference) GetParents(cc *ContentfulClient) (parents []EntryReference, err error)
```

Expand All @@ -163,7 +159,7 @@ an EntryReference with the _GetParents()_ method. This returns a slice of `[]Ent
Another thing you might want to know is the content type of an entry with a given ID:

```go
(cc *ContentfulClient) GetContentTypeOfID(ID string) (contentType string)
(cc *ContentfulClient) GetContentTypeOfID(ctx context.Context, ID string) (contentType string)
```

### Caveats and limitations
Expand Down
26 changes: 11 additions & 15 deletions docs/client/entries.md → docs/02-client/02-entries.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
---
sidebar_label: Entries
sidebar_position: 2
---

# Working with entries

Refer to the [Getting started section](../gettingstarted) for an introduction on entry operations.
Refer to the [Getting started section](../00-gettingstarted) for an introduction on entry operations.
With your newly created client you can do things like:

```go
ctx := context.Background()
// Load all persons
persons, err := cc.GetAllPerson()
persons, err := cc.GetAllPerson(ctx)
// Load a specific person
person, err := cc.GetPersonByID(THE_PERSON_ID)
person, err := cc.GetPersonByID(ctx, THE_PERSON_ID)
// or pass a query
person, err := GetFilteredPerson(&contentful.Query{
person, err := GetFilteredPerson(ctx, &contentful.Query{
"contentType":"person",
"exists": []string{"fields.resume"}
})
Expand All @@ -25,7 +21,7 @@ name := person.Name()
// the getter functions will return that if the value is not set for locale passed to the function.
name := person.Title(people.SpaceLocaleItalian)
// Get references to the person's pets
petRefs := person.Pets()
petRefs := person.Pets(ctx)
// Deal with pets
for _, pet := range petRefs {
switch pet.ContentType {
Expand All @@ -48,13 +44,13 @@ To save the entry to Contentful you need to explicitly call one of these methods

```go
// Upsert (save) an entry
err := dog.UpsertEntry()
err := dog.UpsertEntry(ctx)
// Publish it (after it's been upserted)
err := dog.PublishEntry() // change your mind with err := dog.UnpublishEntry()
err := dog.PublishEntry(ctx) // change your mind with err := dog.UnpublishEntry()
// Or do it in one step
err := dog.UpdateEntry() // upserts and publishes
err := dog.UpdateEntry(ctx) // upserts and publishes
// And delete it
err := dog.DeleteEntry()
err := dog.DeleteEntry(ctx)
```

If you want to know the publication status of an entry as represented in Contentful's UI you
Expand Down Expand Up @@ -92,4 +88,4 @@ type GenericEntry struct {
While these seem to defeat the purpose of the idiomatic API, they are useful in cases where you need to pass-through entries from Contentful to any recipient without type switching. Each generic entry carries a reference to the Gocontentful client it was used to retrieve it, so that other operations can benefit from it.
For example, get the corresponding idiomatic entry only when needed for processing.

Gocontentful supports retrieving either all generic entries in the cache or single generic entries by ID. It also provides methods to get a localized field's value as a string or a float64, set a field's value and upsert the generic entry. Take a look at [the API reference](../api-reference.md) for the method signatures.
Gocontentful supports retrieving either all generic entries in the cache or single generic entries by ID. It also provides methods to get a localized field's value as a string or a float64, set a field's value and upsert the generic entry. Take a look at [the API reference](../04-api-reference) for the method signatures.
7 changes: 1 addition & 6 deletions docs/client/assets.md → docs/02-client/03-assets.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
---
sidebar_label: Assets
sidebar_position: 3
---

# Assets

Contentful allows upload and reference of binary assets and gocontentful fully supports them.
Assuming the dog entry references a picture in a field you can get it with:

```go
picture := dog.Picture() // you can pass a locale to this function as usual
picture := dog.Picture(ctx) // you can pass a locale to this function as usual
```

This returns a \*contenful.AssetNoLocale object handling localization for you in two ways.
Expand Down
13 changes: 7 additions & 6 deletions docs/client/richtext.md → docs/02-client/04-richtext.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
---
sidebar_label: Richtext
sidebar_position: 4
---

# RichText support
# RichText

Contentful supports Rich Text fields. Behind the scenes, these are JSON objects that represent
the content through a Contentful-specific data model. Sooner or later you might want to convert such values to and from HTML.
Expand All @@ -28,4 +23,10 @@ The conversion works the other way around too, when you need to source data from
myRichText := HtmlToRichText(htmlSrc)
```

You can also convert RichText to plain text with

```go
txt, err := RichTextToPlainText(rt interface{}, locale Locale)
```

See the [API Reference](./api-reference) for more details about these functions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
---
sidebar_label: References
sidebar_position: 5
---

# More on references

When working with references it's often useful to know if there are any broken ones in the space.
Expand All @@ -28,6 +23,6 @@ Finally, you can get the parents (AKA referring) entries of either an entry or
an EntryReference with the _GetParents()_ method. This returns a slice of `[]EntryReference`:

```go
(vo *CfPerson) GetParents() (parents []EntryReference, err error)
(vo *CfPerson) GetParents(ctx) (parents []EntryReference, err error)
(ref *EntryReference) GetParents(cc *ContentfulClient) (parents []EntryReference, err error)
```
Loading

0 comments on commit be3d18b

Please sign in to comment.