Skip to content

Commit

Permalink
feat: offline client
Browse files Browse the repository at this point in the history
feat: custom tag in image resolver
fix: race condition in entry save vs read
  • Loading branch information
Cristian Vidmar committed Nov 18, 2021
1 parent b0df466 commit dc073a2
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 20 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ The parameters to pass to NewContentfulClient are:
- *logLevel* (int) is the debug level (see function above). Please note that LogDebug is very verbose and even logs when you request a field value but that is not set for the entry.
- *debug* (bool) is the Contentful API client debug switch. If set to *true* it will log on stdout all the CURL calls to Contentful. This is extremely verbose and extremely valuable when something fails in a call to the API because it's the only way to see the REST API response.

_NOTE:_ Gocontentful provides an offline version of the client that can load data from a JSON space export file (as exported by the _contentful_ CLI tool). This is the way you can write unit tests against your generated API that don't require to be online and the management of a safe API key storage. See function reference below

### Caching

<pre><code>contentTypes := []string{"person", "pet"}
Expand Down Expand Up @@ -255,6 +257,10 @@ Public functions and methods
Creates a Contentful client, this is the first function you need to call. For usage details please refer to the Quickstart above.

>**NewOfflineContentfulClient**(filename string, logFn func(fields map[string]interface{}, level int, args ...interface{}), logLevel int, cacheAssets bool) (*ContentfulClient, error)
Creates an offline Contentful client that loads space data from a JSON file containing a space export (use the contentful CLI tool to get one).

>**SetEnvironment**(environment string)
Sets the Contentful client's environment. All subsequent API calls will be directed to that environment in the selected space. Pass an empty string to reset to the _master_ environment.
Expand Down Expand Up @@ -420,13 +426,13 @@ Converts an HTML fragment to a RichTextNode. This is useful to migrate data from

>**RichTextToHtml**(rt interface{}, linkResolver LinkResolverFunc, entryLinkResolver EntryLinkResolverFunc, imageResolver ImageResolverFunc, locale Locale) (string, error) {
Converts an interface representing a Contentful RichText value (usually from a field getter) into HTML. It currently supports all tags except for embedded and inline entries and assets. It takes in three (optional) functions to resolve hyperlink URLs, permalinks to entries and to derive IMG tag attributes for embedded image assets. The three functions return a map of attributes for the HTML tag the RichTextToHtml function will emit (either an A or an IMG) and have the following signature:
Converts an interface representing a Contentful RichText value (usually from a field getter) into HTML. It currently supports all tags except for embedded and inline entries and assets. It takes in three (optional) functions to resolve hyperlink URLs, permalinks to entries and to derive IMG tag attributes for embedded image assets. The three functions return a map of attributes for the HTML tag the RichTextToHtml function will emit (either an A or an IMG) and have the following signature. Note that the ImageResolverFunc function must return a customHTML value that can be empty but if set it will substitute the IMG tag with the returned HTML snippet. This allows you to emit custom mark-up for your images, e.g. a PICTURE tag.

>type LinkResolverFunc func(url string) (resolvedAttrs map[string]string, resolveError error)
>type EntryLinkResolverFunc func(entryID string, locale Locale) (resolvedAttrs map[string]string, resolveError error)
>type ImageResolverFunc func(assetID string, locale Locale) (attrs map[string]string, resolveError error)
>type ImageResolverFunc func(assetID string, locale Locale) (attrs map[string]string, customHTML string, resolveError error)
>type EmbeddedEntryResolverFunc func(entryID string, locale Locale) (html string, resolveError error)
Expand Down
3 changes: 2 additions & 1 deletion erm/templates/contentful_vo.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ type Cf{{ firstCap $contentType.Sys.ID }} struct {
// Cf{{ firstCap $contentType.Sys.ID }}Fields is a CfNameFields VO
type Cf{{ firstCap $contentType.Sys.ID }}Fields struct {
{{ range $fieldIndex, $field := $contentType.Fields }}
{{ firstCap $field.ID }} map[string]{{ mapFieldType $contentType.Sys.ID $field }} `json:"{{ $field.ID }},omitempty"`{{ end }}
{{ firstCap $field.ID }} map[string]{{ mapFieldType $contentType.Sys.ID $field }} `json:"{{ $field.ID }},omitempty"`
RWLock{{ firstCap $field.ID }} sync.RWMutex `json:"-"`{{ end }}
}
{{ range $fieldIndex, $field := $contentType.Fields }}
{{ if fieldIsAsset $field }}
Expand Down
1 change: 1 addition & 0 deletions erm/templates/contentful_vo_base.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type RichTextGenericNode struct {
type richTextHtmlTag struct {
attrs map[string]string
name string
customHTML string
}

type richTextHtmlTags []richTextHtmlTag
Expand Down
77 changes: 64 additions & 13 deletions erm/templates/contentful_vo_lib.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"html"
"io"
"io/ioutil"
"regexp"
"strings"
"sync"
Expand Down Expand Up @@ -51,6 +52,13 @@ type ContentfulClient struct {
logLevel int
optimisticPageSize uint16 // Start downloading entries at this page size
SpaceID string
offline bool
offlineTemp offlineTemp
}

type offlineTemp struct {
Entries []contentful.Entry `json:"entries"`
Assets []contentful.Asset `json:"assets"`
}

type ContentTypeResult struct {
Expand All @@ -65,7 +73,7 @@ type EntryReference struct {
VO interface{}
}

type ImageResolverFunc func(assetID string, locale Locale) (attrs map[string]string, resolveError error)
type ImageResolverFunc func(assetID string, locale Locale) (attrs map[string]string, customHTML string, resolveError error)
type EntryLinkResolverFunc func(entryID string, locale Locale) (resolvedAttrs map[string]string, resolveError error)
type LinkResolverFunc func(url string) (resolvedAttrs map[string]string, resolveError error)
type EmbeddedEntryResolverFunc func(entryID string, locale Locale) (htmlSnippet string, resolveError error)
Expand Down Expand Up @@ -379,6 +387,36 @@ func NewContentfulClient(spaceID string, clientMode ClientMode, clientKey string
return cc, nil
}

func NewOfflineContentfulClient(filename string, logFn func(fields map[string]interface{}, level int, args ...interface{}), logLevel int, cacheAssets bool) (*ContentfulClient, error) {
fileBytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("NewOfflineContentfulClient could not read space export file: %v", err)
}
offlineTemp := offlineTemp{}
err = json.Unmarshal(fileBytes, &offlineTemp)
if err != nil {
return nil, fmt.Errorf("NewOfflineContentfulClient could not parse space export file: %v", err)
}
cc := &ContentfulClient{
clientMode: ClientModeCDA,
Client: contentful.NewCDA(""),
locales: []Locale{
{{ range $index , $locale := $locales }}SpaceLocale{{ onlyLetters $locale.Name }},
{{ end}}},
logFn: logFn,
logLevel: logLevel,
SpaceID: "OFFLINE",
offline: true,
offlineTemp: offlineTemp,
}
fmt.Println(len(offlineTemp.Entries), len(offlineTemp.Assets))
err = cc.UpdateCache(context.TODO(), spaceContentTypes, cacheAssets)
if err != nil {
return nil, fmt.Errorf("NewOfflineContentfulClient could not cache offline space: %v", err)
}
return cc, nil
}

func RichTextToHtml(rt interface{}, linkResolver LinkResolverFunc, entryLinkResolver EntryLinkResolverFunc, imageResolver ImageResolverFunc, embeddedEntryResolver EmbeddedEntryResolverFunc, locale Locale) (string, error) {
w := bytes.NewBuffer([]byte{})
node := &RichTextGenericNode{}
Expand Down Expand Up @@ -585,18 +623,24 @@ func (cc *ContentfulClient) getAllAssets(tryCacheFirst bool) (map[string]*conten
if cc.Cache != nil && cc.Cache.assets != nil && tryCacheFirst {
return cc.Cache.assets, nil
}
col := cc.Client.Assets.List(cc.SpaceID)
col.Query.Locale("*").Limit(assetPageSize)
assets := map[string]*contentful.Asset{}
allItems := []interface{}{}
for {
_, err := col.Next()
if err != nil {
return nil, err
assets := map[string]*contentful.Asset{}
if cc.offline {
for _, asset := range cc.offlineTemp.Assets {
allItems = append(allItems,asset)
}
allItems = append(allItems, col.Items...)
if uint16(len(col.Items)) < assetPageSize {
break
} else {
col := cc.Client.Assets.List(cc.SpaceID)
col.Query.Locale("*").Limit(assetPageSize)
for {
_, err := col.Next()
if err != nil {
return nil, err
}
allItems = append(allItems, col.Items...)
if uint16(len(col.Items)) < assetPageSize {
break
}
}
}
for _, item := range allItems {
Expand Down Expand Up @@ -821,6 +865,10 @@ func richTextMapTagNodeType(tag string) string {
func (ts richTextHtmlTags) richTextHtmlTagsOpen(w io.Writer) {

for _, t := range ts {
if t.customHTML != "" {
w.Write([]byte(t.customHTML))
continue
}
tagString := "<" + t.name
if len(t.attrs) > 0 {
for name, value := range t.attrs {
Expand All @@ -833,6 +881,9 @@ func (ts richTextHtmlTags) richTextHtmlTagsOpen(w io.Writer) {

func (ts richTextHtmlTags) richTextHtmlTagsClose(w io.Writer) {
for _, t := range ts {
if t.customHTML != "" {
continue
}
w.Write([]byte("</" + t.name + ">"))
}
}
Expand Down Expand Up @@ -938,11 +989,11 @@ func (n *RichTextGenericNode) richTextRenderHTML(w io.Writer, linkResolver LinkR
if dataObj.Target == nil {
return errors.New("Data target is empty")
}
attrs, err := imageResolver(dataObj.Target.Sys.ID, locale)
attrs, customHTML, err := imageResolver(dataObj.Target.Sys.ID, locale)
if err != nil {
return err
}
tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlImage, attrs: attrs}}
tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlImage, attrs: attrs, customHTML: customHTML}}
case RichTextNodeEmbeddedEntry:
dataObj := RichTextData{}
byt, errMarshal := json.Marshal(n.Data)
Expand Down
34 changes: 30 additions & 4 deletions erm/templates/contentful_vo_lib_contenttype.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) {{ firstCap $field.ID }}(locale
if vo == nil {
return {{ mapFieldTypeLiteral $contentType.Sys.ID $field }}
}
vo.Fields.RWLock{{ firstCap $field.ID }}.RLock()
defer vo.Fields.RWLock{{ firstCap $field.ID }}.RUnlock()
loc := defaultLocale
if len(locale) != 0 {
loc = locale[0]
Expand Down Expand Up @@ -188,6 +190,8 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) {{ firstCap $field.ID }}(locale
if vo == nil {
return {{ mapFieldTypeLiteral $contentType.Sys.ID $field }}
}
vo.Fields.RWLock{{ firstCap $field.ID }}.RLock()
defer vo.Fields.RWLock{{ firstCap $field.ID }}.RUnlock()
loc := defaultLocale
if len(locale) != 0 {
loc = locale[0]
Expand Down Expand Up @@ -222,6 +226,8 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) {{ firstCap $field.ID }}(locale
if vo == nil {
return nil
}
vo.Fields.RWLock{{ firstCap $field.ID }}.RLock()
defer vo.Fields.RWLock{{ firstCap $field.ID }}.RUnlock()
{{ $field.ID }} := []*EntryReference{}
loc := defaultLocale
if len(locale) != 0 {
Expand Down Expand Up @@ -278,6 +284,8 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) {{ firstCap $field.ID }}(locale
if vo == nil {
return nil
}
vo.Fields.RWLock{{ firstCap $field.ID }}.RLock()
defer vo.Fields.RWLock{{ firstCap $field.ID }}.RUnlock()
loc := defaultLocale
if len(locale) != 0 {
loc = locale[0]
Expand Down Expand Up @@ -332,6 +340,8 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) {{ firstCap $field.ID }}(locale
if vo == nil {
return nil
}
vo.Fields.RWLock{{ firstCap $field.ID }}.RLock()
defer vo.Fields.RWLock{{ firstCap $field.ID }}.RUnlock()
{{ $field.ID }} := []*contentful.AssetNoLocale{}
loc := defaultLocale
reqLoc := defaultLocale
Expand Down Expand Up @@ -396,6 +406,8 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) {{ firstCap $field.ID }}(locale
if vo == nil {
return nil
}
vo.Fields.RWLock{{ firstCap $field.ID }}.RLock()
defer vo.Fields.RWLock{{ firstCap $field.ID }}.RUnlock()
loc := defaultLocale
reqLoc := defaultLocale
if len(locale) != 0 {
Expand Down Expand Up @@ -467,6 +479,8 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) Set{{ firstCap $field.ID }}({{ $
return ErrLocaleUnsupported
}
}
vo.Fields.RWLock{{ firstCap $field.ID }}.Lock()
defer vo.Fields.RWLock{{ firstCap $field.ID }}.Unlock()
if vo.Fields.{{ firstCap $field.ID }} == nil {
vo.Fields.{{ firstCap $field.ID }} = make(map[string]{{ mapFieldType $contentType.Sys.ID $field }})
}
Expand Down Expand Up @@ -615,11 +629,23 @@ func (cc *ContentfulClient) cacheAll{{ firstCap $contentType.Sys.ID }}(ctx conte
if cc == nil || cc.Client == nil {
return nil, errors.New("cacheAll{{ firstCap $contentType.Sys.ID }}: No CDA client available")
}
col, err := cc.optimisticPageSizeGetAll("{{ $contentType.Sys.ID }}", cc.optimisticPageSize)
if err != nil {
return nil, err
var all{{ firstCap $contentType.Sys.ID }} []*Cf{{ firstCap $contentType.Sys.ID }}
col := &contentful.Collection{
Items: []interface{}{},
}
all{{ firstCap $contentType.Sys.ID }}, err := colToCf{{ firstCap $contentType.Sys.ID }}(col,cc)
if cc.offline {
for _, entry := range cc.offlineTemp.Entries {
if entry.Sys.ContentType.Sys.ID == ContentType{{ firstCap $contentType.Sys.ID }} {
col.Items = append(col.Items, entry)
}
}
} else {
col, err = cc.optimisticPageSizeGetAll("{{ $contentType.Sys.ID }}", cc.optimisticPageSize)
if err != nil {
return nil, err
}
}
all{{ firstCap $contentType.Sys.ID }}, err = colToCf{{ firstCap $contentType.Sys.ID }}(col,cc)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit dc073a2

Please sign in to comment.