chore: integrate and fix more changes from master

This commit is contained in:
Cristian Vidmar 2024-04-29 14:53:50 +02:00
parent 22c3300688
commit 441c2f9136
8 changed files with 82 additions and 37 deletions

View File

@ -1,11 +1,42 @@
# 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._

View File

@ -15,7 +15,19 @@ 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

View File

@ -625,12 +625,12 @@ func (genericEntry *GenericEntry) FieldAsString(fieldName string, locale ...Loca
}
}
func (genericEntry *GenericEntry) InheritAsString(fieldName string, parentTypes []string, locale ...Locale) (string, error) {
func (genericEntry *GenericEntry) InheritAsString(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (string, error) {
val, err := genericEntry.FieldAsString(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(genericEntry.CC, genericEntry.Sys.ID, parentTypes)
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return "", err
}
@ -681,12 +681,12 @@ func (genericEntry *GenericEntry) FieldAsFloat64(fieldName string, locale ...Loc
}
}
func (genericEntry *GenericEntry) InheritAsFloat64(fieldName string, parentTypes []string, locale ...Locale) (float64, error) {
func (genericEntry *GenericEntry) InheritAsFloat64(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (float64, error) {
val, err := genericEntry.FieldAsFloat64(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(genericEntry.CC, genericEntry.Sys.ID, parentTypes)
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return 0, err
}
@ -749,12 +749,12 @@ func (genericEntry *GenericEntry) FieldAsReference(fieldName string, locale ...L
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) InheritAsReference(fieldName string, parentTypes []string, locale ...Locale) (*EntryReference, error) {
func (genericEntry *GenericEntry) InheritAsReference(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (*EntryReference, error) {
val, err := genericEntry.FieldAsReference(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(genericEntry.CC, genericEntry.Sys.ID, parentTypes)
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return nil, err
}
@ -800,7 +800,7 @@ func (genericEntry *GenericEntry) SetField(fieldName string, fieldValue interfac
}
func (genericEntry *GenericEntry) Upsert() error {
func (genericEntry *GenericEntry) Upsert(ctx context.Context) error {
cfEntry := &contentful.Entry{
Fields: map[string]interface{}{},
}
@ -818,7 +818,7 @@ func (genericEntry *GenericEntry) Upsert() error {
cfEntry.Fields[key] = fieldValue
}
// upsert the entry
err := genericEntry.CC.Client.Entries.Upsert(genericEntry.CC.SpaceID, cfEntry)
err := genericEntry.CC.Client.Entries.Upsert(ctx, genericEntry.CC.SpaceID, cfEntry)
if err != nil {
if genericEntry.CC.logFn != nil && genericEntry.CC.logLevel <= LogWarn {
genericEntry.CC.logFn(map[string]interface{}{"task": "UpdateCache"}, LogWarn, fmt.Errorf("CfAkeneoSettings UpsertEntry: Operation failed: %w", err))
@ -996,6 +996,7 @@ func (cc *ContentfulClient) syncCache(ctx context.Context, contentTypes []string
if err := updateCacheForContentTypeAndEntity(ctx, cc, entry.Sys.ContentType.Sys.ID, entry.Sys.ID, entry, false); err != nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"id": entry.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to update cache for entry")
}
} else {
syncEntryCount[entry.Sys.ContentType.Sys.ID]++
}
@ -1006,6 +1007,7 @@ func (cc *ContentfulClient) syncCache(ctx context.Context, contentTypes []string
if err := updateCacheForContentTypeAndEntity(ctx, cc, contentType, entry.Sys.ID, entry, true); err != nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"id": entry.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to delete cache for entry")
}
} else {
syncEntryCount["deletedEntry"]++
}
@ -1836,7 +1838,7 @@ func updateCacheForContentTypeAndEntity(ctx context.Context, cc *ContentfulClien
return nil
}
func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, contentType []string) (parents []EntryReference, err error) {
func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, contentTypes []string) (parents []EntryReference, err error) {
parents = []EntryReference{}
cc.cacheMutex.sharedDataGcLock.RLock()
cacheInit := cc.cacheInit

View File

@ -140,7 +140,8 @@ func TestGenericEntries(t *testing.T) {
sku, err := genericProduct.FieldAsString("sku")
require.Error(t, err)
require.Equal(t, "", sku)
inheritedSKU, err := genericProduct.InheritAsString("sku", nil)
ctx := context.Background()
inheritedSKU, err := genericProduct.InheritAsString(ctx, "sku", nil)
require.NoError(t, err)
require.Equal(t, "B00MG4ULK2", inheritedSKU)
}

View File

@ -178,9 +178,9 @@ var (
InfoPreservingExistingCache = "could not connect for cache update, preserving the existing cache"
InfoUpdateCacheTime = "space caching done, time recorded"
ErrorEnvironmentSetToMaster = "environment was empty string, set to master"
ErrorEntryNotFound = "entry not found"
ErrorEntryIsNil = "entry is nil"
ErrorEntrySysIsNil = "entry.Sys is nil"
ErrorEntryNotFound = "entry not found"
ErrorEntrySysContentTypeIsNil = "entry.Sys.ContentType is nil"
ErrorEntrySysContentTypeSysIsNil = "entry.Sys.ContentType.Sys is nil"
ErrorEntryCachingFailed = "entry caching failed"
@ -657,12 +657,12 @@ func (genericEntry *GenericEntry) FieldAsString(fieldName string, locale ...Loca
}
}
func (genericEntry *GenericEntry) InheritAsString(fieldName string, parentTypes []string, locale ...Locale) (string, error) {
func (genericEntry *GenericEntry) InheritAsString(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (string, error) {
val, err := genericEntry.FieldAsString(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(genericEntry.CC, genericEntry.Sys.ID, parentTypes)
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return "", err
}
@ -713,12 +713,12 @@ func (genericEntry *GenericEntry) FieldAsFloat64(fieldName string, locale ...Loc
}
}
func (genericEntry *GenericEntry) InheritAsFloat64(fieldName string, parentTypes []string, locale ...Locale) (float64, error) {
func (genericEntry *GenericEntry) InheritAsFloat64(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (float64, error) {
val, err := genericEntry.FieldAsFloat64(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(genericEntry.CC, genericEntry.Sys.ID, parentTypes)
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return 0, err
}
@ -781,12 +781,12 @@ func (genericEntry *GenericEntry) FieldAsReference(fieldName string, locale ...L
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) InheritAsReference(fieldName string, parentTypes []string, locale ...Locale) (*EntryReference, error) {
func (genericEntry *GenericEntry) InheritAsReference(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (*EntryReference, error) {
val, err := genericEntry.FieldAsReference(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(genericEntry.CC, genericEntry.Sys.ID, parentTypes)
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return nil, err
}
@ -831,7 +831,7 @@ func (genericEntry *GenericEntry) SetField(fieldName string, fieldValue interfac
return ErrNotSet
}
func (genericEntry *GenericEntry) Upsert() error {
func (genericEntry *GenericEntry) Upsert(ctx context.Context) error {
cfEntry := &contentful.Entry{
Fields: map[string]interface{}{},
}
@ -849,7 +849,7 @@ func (genericEntry *GenericEntry) Upsert() error {
cfEntry.Fields[key] = fieldValue
}
// upsert the entry
err := genericEntry.CC.Client.Entries.Upsert(genericEntry.CC.SpaceID, cfEntry)
err := genericEntry.CC.Client.Entries.Upsert(ctx, genericEntry.CC.SpaceID, cfEntry)
if err != nil {
if genericEntry.CC.logFn != nil && genericEntry.CC.logLevel <= LogWarn {
genericEntry.CC.logFn(map[string]interface{}{"task": "UpdateCache"}, LogWarn, fmt.Errorf("CfAkeneoSettings UpsertEntry: Operation failed: %w", err))
@ -1059,6 +1059,8 @@ func (cc *ContentfulClient) syncCache(ctx context.Context, contentTypes []string
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"id": asset.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to update cache for entry")
}
} else {
syncAssetCount++
}
case sysTypeDeletedAsset:
if err := updateCacheForContentTypeAndEntity(ctx, cc, assetWorkerType, asset.Sys.ID, nil, true); err != nil {
@ -1943,7 +1945,7 @@ func updateCacheForContentTypeAndEntity(ctx context.Context, cc *ContentfulClien
return nil
}
func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, contentType []string) (parents []EntryReference, err error) {
func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, contentTypes []string) (parents []EntryReference, err error) {
parents = []EntryReference{}
cc.cacheMutex.sharedDataGcLock.RLock()
cacheInit := cc.cacheInit

View File

@ -797,7 +797,6 @@ func (cc *ContentfulClient) cacheBrandByID(ctx context.Context, id string, entry
defer cc.cacheMutex.parentMapGcLock.Unlock()
cc.cacheMutex.genericEntriesGcLock.Lock()
defer cc.cacheMutex.genericEntriesGcLock.Unlock()
var col *contentful.Collection
if entryPayload != nil {
col = &contentful.Collection{

View File

@ -561,7 +561,6 @@ func (cc *ContentfulClient) cacheCategoryByID(ctx context.Context, id string, en
defer cc.cacheMutex.parentMapGcLock.Unlock()
cc.cacheMutex.genericEntriesGcLock.Lock()
defer cc.cacheMutex.genericEntriesGcLock.Unlock()
var col *contentful.Collection
if entryPayload != nil {
col = &contentful.Collection{

View File

@ -612,7 +612,7 @@ func (vo *CfProduct) Brand(ctx context.Context, locale ...Locale) *EntryReferenc
return nil
}
func (vo *CfProduct) SubProduct(locale ...Locale) *EntryReference {
func (vo *CfProduct) SubProduct(ctx context.Context, locale ...Locale) *EntryReference {
if vo == nil {
return nil
}
@ -647,7 +647,7 @@ func (vo *CfProduct) SubProduct(locale ...Locale) *EntryReference {
}
}
localizedSubProduct := vo.Fields.SubProduct[string(loc)]
contentType, err := vo.CC.GetContentTypeOfID(localizedSubProduct.Sys.ID)
contentType, err := vo.CC.GetContentTypeOfID(ctx, localizedSubProduct.Sys.ID)
if err != nil {
if vo.CC.logFn != nil && vo.CC.logLevel <= LogError {
vo.CC.logFn(map[string]interface{}{"content type": vo.Sys.ContentType.Sys.ID, "entry ID": vo.Sys.ID, "method": "SubProduct()"}, LogError, ErrNoTypeOfRefEntry)
@ -657,7 +657,7 @@ func (vo *CfProduct) SubProduct(locale ...Locale) *EntryReference {
switch contentType {
case ContentTypeBrand:
referencedVO, err := vo.CC.GetBrandByID(localizedSubProduct.Sys.ID)
referencedVO, err := vo.CC.GetBrandByID(ctx, localizedSubProduct.Sys.ID)
if err != nil {
if vo.CC.logFn != nil && vo.CC.logLevel <= LogError {
vo.CC.logFn(map[string]interface{}{"content type": vo.Sys.ContentType.Sys.ID, "entry ID": vo.Sys.ID, "method": "SubProduct()"}, LogError, err)
@ -667,7 +667,7 @@ func (vo *CfProduct) SubProduct(locale ...Locale) *EntryReference {
return &EntryReference{ContentType: contentType, ID: localizedSubProduct.Sys.ID, VO: referencedVO}
case ContentTypeCategory:
referencedVO, err := vo.CC.GetCategoryByID(localizedSubProduct.Sys.ID)
referencedVO, err := vo.CC.GetCategoryByID(ctx, localizedSubProduct.Sys.ID)
if err != nil {
if vo.CC.logFn != nil && vo.CC.logLevel <= LogError {
vo.CC.logFn(map[string]interface{}{"content type": vo.Sys.ContentType.Sys.ID, "entry ID": vo.Sys.ID, "method": "SubProduct()"}, LogError, err)
@ -677,7 +677,7 @@ func (vo *CfProduct) SubProduct(locale ...Locale) *EntryReference {
return &EntryReference{ContentType: contentType, ID: localizedSubProduct.Sys.ID, VO: referencedVO}
case ContentTypeProduct:
referencedVO, err := vo.CC.GetProductByID(localizedSubProduct.Sys.ID)
referencedVO, err := vo.CC.GetProductByID(ctx, localizedSubProduct.Sys.ID)
if err != nil {
if vo.CC.logFn != nil && vo.CC.logLevel <= LogError {
vo.CC.logFn(map[string]interface{}{"content type": vo.Sys.ContentType.Sys.ID, "entry ID": vo.Sys.ID, "method": "SubProduct()"}, LogError, err)
@ -1434,7 +1434,6 @@ func (cc *ContentfulClient) cacheProductByID(ctx context.Context, id string, ent
defer cc.cacheMutex.parentMapGcLock.Unlock()
cc.cacheMutex.genericEntriesGcLock.Lock()
defer cc.cacheMutex.genericEntriesGcLock.Unlock()
var col *contentful.Collection
if entryPayload != nil {
col = &contentful.Collection{