├── .ruby-version
├── spec
├── fixtures
│ ├── file.txt
│ ├── ssg
│ │ ├── hugo
│ │ │ ├── yaml
│ │ │ │ └── config.yaml
│ │ │ ├── toml
│ │ │ │ └── config.toml
│ │ │ └── json
│ │ │ │ └── config.json
│ │ ├── middleman
│ │ │ └── Gemfile
│ │ └── hexo
│ │ │ └── package.json
│ ├── image
│ ├── image.gif
│ ├── image.jpg
│ ├── image.png
│ ├── blurhash.jpg
│ ├── dato-logo.jpg
│ └── config.rb
├── dato_spec.rb
├── dato
│ ├── dump
│ │ ├── format
│ │ │ └── yaml_spec.rb
│ │ ├── ssg_detector_spec.rb
│ │ └── runner_spec.rb
│ ├── local
│ │ ├── loader_spec.rb
│ │ ├── field_type
│ │ │ ├── lat_lon_spec.rb
│ │ │ ├── color_spec.rb
│ │ │ ├── upload_id_spec.rb
│ │ │ ├── video_spec.rb
│ │ │ └── seo_spec.rb
│ │ ├── entities_repo_spec.rb
│ │ ├── items_repo_integration_spec.rb
│ │ ├── json_api_entity_spec.rb
│ │ └── item_spec.rb
│ ├── utils
│ │ ├── meta_tags
│ │ │ ├── og_locale_spec.rb
│ │ │ ├── robots_spec.rb
│ │ │ ├── article_modified_time_spec.rb
│ │ │ ├── og_type_spec.rb
│ │ │ ├── og_site_name_spec.rb
│ │ │ ├── twitter_site_spec.rb
│ │ │ ├── article_publisher_spec.rb
│ │ │ ├── twitter_card_spec.rb
│ │ │ ├── description_spec.rb
│ │ │ ├── image_spec.rb
│ │ │ └── title_spec.rb
│ │ ├── seo_tags_builder_spec.rb
│ │ ├── build_modular_block_spec.rb
│ │ └── favicon_tags_builder_spec.rb
│ ├── cli_spec.rb
│ ├── json_api_serializer_spec.rb
│ ├── account
│ │ └── client_spec.rb
│ └── upload
│ │ └── file_spec.rb
├── support
│ ├── account_client_provider.rb
│ └── shared_contexts
│ │ ├── new_site.rb
│ │ └── meta_tags.rb
└── spec_helper.rb
├── .rspec
├── lib
├── dato
│ ├── version.rb
│ ├── upload
│ │ ├── image.rb
│ │ ├── file.rb
│ │ └── create_upload_path.rb
│ ├── account
│ │ └── client.rb
│ ├── local
│ │ ├── field_type
│ │ │ ├── slug.rb
│ │ │ ├── text.rb
│ │ │ ├── float.rb
│ │ │ ├── string.rb
│ │ │ ├── boolean.rb
│ │ │ ├── integer.rb
│ │ │ ├── link.rb
│ │ │ ├── date.rb
│ │ │ ├── json.rb
│ │ │ ├── single_block.rb
│ │ │ ├── date_time.rb
│ │ │ ├── links.rb
│ │ │ ├── rich_text.rb
│ │ │ ├── gallery.rb
│ │ │ ├── lat_lon.rb
│ │ │ ├── seo.rb
│ │ │ ├── theme.rb
│ │ │ ├── global_seo.rb
│ │ │ ├── color.rb
│ │ │ ├── upload_id.rb
│ │ │ ├── structured_text.rb
│ │ │ ├── video.rb
│ │ │ └── file.rb
│ │ ├── json_api_meta.rb
│ │ ├── entities_repo.rb
│ │ ├── site.rb
│ │ ├── json_api_entity.rb
│ │ ├── item.rb
│ │ ├── loader.rb
│ │ └── items_repo.rb
│ ├── utils
│ │ ├── meta_tags
│ │ │ ├── robots.rb
│ │ │ ├── og_locale.rb
│ │ │ ├── twitter_card.rb
│ │ │ ├── article_modified_time.rb
│ │ │ ├── og_type.rb
│ │ │ ├── og_site_name.rb
│ │ │ ├── twitter_site.rb
│ │ │ ├── article_publisher.rb
│ │ │ ├── description.rb
│ │ │ ├── image.rb
│ │ │ ├── title.rb
│ │ │ └── base.rb
│ │ ├── locale_value.rb
│ │ ├── build_modular_block.rb
│ │ ├── seo_tags_builder.rb
│ │ └── favicon_tags_builder.rb
│ ├── dump
│ │ ├── dsl
│ │ │ ├── add_to_data_file.rb
│ │ │ ├── create_data_file.rb
│ │ │ ├── directory.rb
│ │ │ ├── create_post.rb
│ │ │ └── root.rb
│ │ ├── format
│ │ │ ├── json.rb
│ │ │ ├── toml.rb
│ │ │ └── yaml.rb
│ │ ├── operation
│ │ │ ├── root.rb
│ │ │ ├── create_data_file.rb
│ │ │ ├── directory.rb
│ │ │ ├── create_post.rb
│ │ │ └── add_to_data_file.rb
│ │ ├── format.rb
│ │ ├── runner.rb
│ │ └── ssg_detector.rb
│ ├── api_error.rb
│ ├── paginator.rb
│ ├── site
│ │ └── client.rb
│ ├── json_schema_type.rb
│ ├── json_schema_relationships.rb
│ ├── json_api_deserializer.rb
│ ├── cli.rb
│ ├── repo.rb
│ ├── json_api_serializer.rb
│ └── api_client.rb
└── dato.rb
├── Gemfile
├── bin
├── setup
├── console
└── rspec
├── .gitignore
├── .travis.yml
├── exe
└── dato
├── Rakefile
├── TODO.md
├── LICENSE
├── LICENSE.txt
├── .rubocop.yml
├── dato.gemspec
├── CODE_OF_CONDUCT.md
├── CHANGELOG.md
└── README.md
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.7.5
2 |
--------------------------------------------------------------------------------
/spec/fixtures/file.txt:
--------------------------------------------------------------------------------
1 | Hi there!
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format progress
2 | --color
3 |
--------------------------------------------------------------------------------
/spec/fixtures/ssg/hugo/yaml/config.yaml:
--------------------------------------------------------------------------------
1 | baseurl: http://google.com/
2 |
--------------------------------------------------------------------------------
/spec/fixtures/ssg/hugo/toml/config.toml:
--------------------------------------------------------------------------------
1 | baseurl = "http://google.com/"
2 |
--------------------------------------------------------------------------------
/spec/fixtures/ssg/hugo/json/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseurl": "http://google.com/"
3 | }
4 |
--------------------------------------------------------------------------------
/spec/fixtures/image:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/datocms/ruby-datocms-client/HEAD/spec/fixtures/image
--------------------------------------------------------------------------------
/lib/dato/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | VERSION = "0.8.3"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/fixtures/image.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/datocms/ruby-datocms-client/HEAD/spec/fixtures/image.gif
--------------------------------------------------------------------------------
/spec/fixtures/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/datocms/ruby-datocms-client/HEAD/spec/fixtures/image.jpg
--------------------------------------------------------------------------------
/spec/fixtures/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/datocms/ruby-datocms-client/HEAD/spec/fixtures/image.png
--------------------------------------------------------------------------------
/spec/fixtures/blurhash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/datocms/ruby-datocms-client/HEAD/spec/fixtures/blurhash.jpg
--------------------------------------------------------------------------------
/spec/fixtures/dato-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/datocms/ruby-datocms-client/HEAD/spec/fixtures/dato-logo.jpg
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Specify your gem's dependencies in dato.gemspec
6 | gemspec
7 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/lib/dato/upload/image.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/upload/file"
4 |
5 | module Dato
6 | module Upload
7 | Image = Dato::Upload::File
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /coverage/
10 | /tmp/
11 | build.zip
12 | .env
13 | dato.config.rb
14 |
--------------------------------------------------------------------------------
/spec/dato_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe Dato do
6 | it "has a version number" do
7 | expect(Dato::VERSION).not_to be nil
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.6.0
4 | - 2.7.5
5 | before_install:
6 | - gem install bundler -v 1.13.5
7 | - echo 'puts "ruby \"#{RUBY_VERSION}\""' | ruby >> Gemfile
8 | - cat Gemfile
9 | script: rubocop && rake
--------------------------------------------------------------------------------
/exe/dato:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5 |
6 | require "dotenv"
7 | Dotenv.load
8 |
9 | require "dato"
10 | Dato::Cli.start(ARGV)
11 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 |
6 | RSpec::Core::RakeTask.new(:spec) do |t|
7 | t.rspec_opts = "-b"
8 | end
9 |
10 | task default: :spec
11 |
--------------------------------------------------------------------------------
/lib/dato/account/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/api_client"
4 |
5 | module Dato
6 | module Account
7 | class Client
8 | include ApiClient
9 |
10 | json_schema "account-api"
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/slug.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Slug
7 | def self.parse(value, _repo)
8 | value
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/text.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Text
7 | def self.parse(value, _repo)
8 | value
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/fixtures/ssg/middleman/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "middleman"
6 | gem "middleman-autoprefixer"
7 | gem "middleman-dato", "0.5.14"
8 | gem "middleman-livereload"
9 | gem "middleman-search_engine_sitemap"
10 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/float.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Float
7 | def self.parse(value, _repo)
8 | value
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/string.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class String
7 | def self.parse(value, _repo)
8 | value
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/boolean.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Boolean
7 | def self.parse(value, _repo)
8 | value
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/integer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Integer
7 | def self.parse(value, _repo)
8 | value
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/link.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Link
7 | def self.parse(value, repo)
8 | value && repo.find(value)
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/date.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Date
7 | def self.parse(value, _repo)
8 | value && ::Date.parse(value)
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/json.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Json
7 | def self.parse(value, _repo)
8 | value && JSON.parse(value)
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/single_block.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class SingleBlock
7 | def self.parse(value, repo)
8 | value && repo.find(value)
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/date_time.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class DateTime
7 | def self.parse(value, _repo)
8 | value && ::Time.parse(value).utc
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/fixtures/ssg/hexo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "hexo": "3.2.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/robots.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class Robots < Base
9 | def build
10 | meta_tag("robots", "noindex") if site.no_index
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/dato/dump/dsl/add_to_data_file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/dump/operation/add_to_data_file"
4 |
5 | module Dato
6 | module Dump
7 | module Dsl
8 | module AddToDataFile
9 | def add_to_data_file(*args)
10 | operations.add Operation::AddToDataFile.new(operations, *args)
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/dato/dump/dsl/create_data_file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/dump/operation/create_data_file"
4 |
5 | module Dato
6 | module Dump
7 | module Dsl
8 | module CreateDataFile
9 | def create_data_file(*args)
10 | operations.add Operation::CreateDataFile.new(operations, *args)
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/dato/dump/format/json.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "json"
4 |
5 | module Dato
6 | module Dump
7 | module Format
8 | module Json
9 | def self.dump(value)
10 | JSON.dump(value)
11 | end
12 |
13 | def self.frontmatter_dump(value)
14 | "#{dump(value)}\n"
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/og_locale.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class OgLocale < Base
9 | def build
10 | locale = I18n.locale
11 | og_tag("og:locale", "#{locale}_#{locale.upcase}")
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/twitter_card.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class TwitterCard < Base
9 | def build
10 | card_tag("twitter:card", seo_field_with_fallback(:twitter_card, nil) || "summary")
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/dato.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_support/all"
4 |
5 | require "dato/version"
6 |
7 | require "dato/site/client"
8 | require "dato/account/client"
9 | require "dato/local/site"
10 | require "dato/cli"
11 | require "dato/utils/seo_tags_builder"
12 | require "dato/utils/favicon_tags_builder"
13 | require "dato/utils/build_modular_block"
14 |
15 | module Dato
16 | end
17 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/article_modified_time.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 | require "time"
5 |
6 | module Dato
7 | module Utils
8 | module MetaTags
9 | class ArticleModifiedTime < Base
10 | def build
11 | og_tag("article:modified_time", item.updated_at.iso8601) if item
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "dato"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require "irb"
15 | IRB.start
16 |
--------------------------------------------------------------------------------
/lib/dato/utils/locale_value.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "i18n/backend/fallbacks"
4 |
5 | module Dato
6 | module Utils
7 | module LocaleValue
8 | def self.find(obj)
9 | locale_with_value = I18n.fallbacks[I18n.locale]
10 | .find { |locale| obj[locale] }
11 |
12 | obj[locale_with_value || I18n.locale]
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/dato/dump/format/yaml_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Dump
7 | module Format
8 | RSpec.describe Yaml do
9 | describe ".dump" do
10 | it "dumps into Yaml stringifying symbols" do
11 | expect(Yaml.dump([{ foo: "bar" }])).to eq "- foo: bar"
12 | end
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/og_type.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class OgType < Base
9 | def build
10 | if !item || item.singleton?
11 | og_tag("og:type", "website")
12 | else
13 | og_tag("og:type", "article")
14 | end
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/og_site_name.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class OgSiteName < Base
9 | def build
10 | og_tag("og:site_name", site_name) if site_name
11 | end
12 |
13 | def site_name
14 | site.global_seo && site.global_seo.site_name
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/twitter_site.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class TwitterSite < Base
9 | def build
10 | card_tag("twitter:site", twitter_account) if twitter_account
11 | end
12 |
13 | def twitter_account
14 | site.global_seo && site.global_seo.twitter_account
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/article_publisher.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class ArticlePublisher < Base
9 | def build
10 | og_tag("article:publisher", facebook_page_url) if facebook_page_url
11 | end
12 |
13 | def facebook_page_url
14 | site.global_seo && site.global_seo.facebook_page_url
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/dato/api_error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | class ApiError < StandardError
5 | attr_reader :response, :body
6 |
7 | def initialize(response)
8 | body = JSON.parse(response[:body]) if response[:body]
9 |
10 | message = [
11 | "DatoCMS API Error",
12 | "Status: #{response[:status]}",
13 | "Response:",
14 | JSON.pretty_generate(body),
15 | ].join("\n")
16 |
17 | super(message)
18 |
19 | @response = response
20 | @body = body
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/dato/dump/operation/root.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Dump
5 | module Operation
6 | class Root
7 | attr_reader :path
8 |
9 | def initialize(path)
10 | @operations = []
11 | @path = path
12 | end
13 |
14 | def add(operation)
15 | @operations << operation
16 | end
17 |
18 | def perform
19 | operations.each(&:perform)
20 | end
21 |
22 | private
23 |
24 | attr_reader :operations
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/links.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Links < Array
7 | def self.parse(ids, repo)
8 | items = if ids
9 | ids.map { |id| repo.find(id) }
10 | else
11 | []
12 | end
13 | new(items)
14 | end
15 |
16 | def to_hash(max_depth = 3, current_depth = 0)
17 | map { |item| item.to_hash(max_depth, current_depth) }
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/rich_text.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class RichText < Array
7 | def self.parse(ids, repo)
8 | items = if ids
9 | ids.map { |id| repo.find(id) }
10 | else
11 | []
12 | end
13 | new(items)
14 | end
15 |
16 | def to_hash(max_depth = 3, current_depth = 0)
17 | map { |item| item.to_hash(max_depth, current_depth) }
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/dato/local/loader_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe Dato::Local::Loader, :vcr do
6 | include_context "with a new site"
7 |
8 | subject(:loader) do
9 | described_class.new(client, true)
10 | end
11 |
12 | it "fetches an entire site" do
13 | loader.load
14 | repo = loader.items_repo
15 |
16 | expect(repo.articles.size).to eq 1
17 | expect(repo.articles.first.title).to eq "First post"
18 | expect(repo.articles.first.image.format).to eq "png"
19 | expect(repo.articles.first.file.format).to eq "txt"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/og_locale_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe OgLocale do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | it "returns current i18n locale" do
15 | result = I18n.with_locale(:en) { builder.build }
16 | expect(result[:attributes][:content]).to eq("en_EN")
17 | end
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/dato/utils/seo_tags_builder_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | describe SeoTagsBuilder do
8 | include_context "items repo"
9 |
10 | subject(:builder) { described_class.new(item, site) }
11 |
12 | describe "#meta_tags" do
13 | it "returns an array of tags" do
14 | expect(builder.meta_tags).to be_an Array
15 | expect(builder.meta_tags.first).to be_an Hash
16 | expect(builder.meta_tags.first[:tag_name]).to eq "meta"
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/dato/dump/format/toml.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "toml"
4 |
5 | class Time
6 | def to_toml(_path = "")
7 | utc.strftime("%Y-%m-%dT%H:%M:%SZ")
8 | end
9 | end
10 |
11 | class Date
12 | def to_toml(_path = "")
13 | strftime("%Y-%m-%d")
14 | end
15 | end
16 |
17 | module Dato
18 | module Dump
19 | module Format
20 | module Toml
21 | def self.dump(value)
22 | TOML::Generator.new(value).body
23 | end
24 |
25 | def self.frontmatter_dump(value)
26 | "+++\n#{dump(value)}+++"
27 | end
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/dato/local/field_type/lat_lon_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | RSpec.describe LatLon do
9 | subject(:latlon) { described_class.parse(attributes, nil) }
10 | let(:attributes) do
11 | {
12 | latitude: 12,
13 | longitude: 10,
14 | }
15 | end
16 |
17 | it "responds to latitude and longitude methods" do
18 | expect(latlon.latitude).to eq 12
19 | expect(latlon.longitude).to eq 10
20 | end
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/fixtures/config.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | def helper_method_example
4 | puts "A helper method"
5 | end
6 |
7 | dato.available_locales.each do |_locale|
8 | create_data_file "site.yml", :yml, dato.site.to_hash
9 |
10 | directory "posts" do
11 | helper_method_example
12 |
13 | dato.articles.each do |post|
14 | create_post "#{post.slug}.md" do
15 | frontmatter :yaml, post.to_hash.merge!(image_url_with_focal_point: post.image.url(w: 150, h: 50, fit: :crop))
16 | content post.title
17 | end
18 | end
19 | end
20 |
21 | add_to_data_file "foobar.toml", :toml, sitename: dato.site.name
22 | end
23 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/gallery.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/local/field_type/file"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | class Gallery < Array
9 | def self.parse(value, repo)
10 | images = if value
11 | value.map { |image| FieldType::File.parse(image, repo) }
12 | else
13 | []
14 | end
15 | new(images)
16 | end
17 |
18 | def to_hash(max_depth = 3, current_depth = 0)
19 | map { |item| item.to_hash(max_depth, current_depth) }
20 | end
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/description.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class Description < Base
9 | def build
10 | return unless description.present?
11 |
12 | [
13 | meta_tag("description", description),
14 | og_tag("og:description", description),
15 | card_tag("twitter:description", description),
16 | ]
17 | end
18 |
19 | def description
20 | @description ||= seo_field_with_fallback(:description, nil)
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/dato/dump/format.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/dump/format/toml"
4 | require "dato/dump/format/yaml"
5 | require "dato/dump/format/json"
6 |
7 | module Dato
8 | module Dump
9 | module Format
10 | def self.dump(format, value)
11 | converter_for(format).dump(value)
12 | end
13 |
14 | def self.frontmatter_dump(format, value)
15 | converter_for(format).frontmatter_dump(value)
16 | end
17 |
18 | def self.converter_for(format)
19 | case format.to_sym
20 | when :toml
21 | Format::Toml
22 | when :yaml, :yml
23 | Format::Yaml
24 | when :json
25 | Format::Json
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/lat_lon.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class LatLon
7 | attr_reader :latitude, :longitude
8 |
9 | def self.parse(value, _repo)
10 | value && new(value[:latitude], value[:longitude])
11 | end
12 |
13 | def initialize(latitude, longitude)
14 | @latitude = latitude
15 | @longitude = longitude
16 | end
17 |
18 | def values
19 | [latitude, longitude]
20 | end
21 |
22 | def to_hash(*_args)
23 | {
24 | latitude: latitude,
25 | longitude: longitude,
26 | }
27 | end
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/dato/dump/operation/create_data_file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "fileutils"
4 | require "dato/dump/format"
5 |
6 | module Dato
7 | module Dump
8 | module Operation
9 | class CreateDataFile
10 | attr_reader :context, :path, :format, :value
11 |
12 | def initialize(context, path, format, value)
13 | @context = context
14 | @path = path
15 | @format = format
16 | @value = value
17 | end
18 |
19 | def perform
20 | FileUtils.mkdir_p(File.dirname(path))
21 |
22 | File.open(File.join(context.path, path), "w") do |file|
23 | file.write Format.dump(format, value)
24 | end
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/dato/dump/format/yaml.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "yaml"
4 |
5 | class Array
6 | def deep_stringify_keys
7 | each_with_object([]) do |value, accum|
8 | if value.is_a?(Hash) || value.is_a?(Array)
9 | new_val = value.deep_stringify_keys
10 | accum.push new_val
11 | else
12 | accum.push value
13 | end
14 | accum
15 | end
16 | end
17 | end
18 |
19 | module Dato
20 | module Dump
21 | module Format
22 | module Yaml
23 | def self.dump(value)
24 | YAML.dump(value.deep_stringify_keys).chomp.gsub(/^-+\n/, "")
25 | end
26 |
27 | def self.frontmatter_dump(value)
28 | "---\n#{dump(value)}\n---"
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/dato/dump/operation/directory.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "fileutils"
4 |
5 | module Dato
6 | module Dump
7 | module Operation
8 | class Directory
9 | attr_reader :context, :path
10 |
11 | def initialize(context, path)
12 | @context = context
13 | @path = File.join(context.path, path)
14 | @operations = []
15 | end
16 |
17 | def add(operation)
18 | @operations << operation
19 | end
20 |
21 | def perform
22 | FileUtils.remove_dir(path) if Dir.exist?(path)
23 |
24 | FileUtils.mkdir_p(path)
25 |
26 | operations.each(&:perform)
27 | end
28 |
29 | private
30 |
31 | attr_reader :operations
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | ```ruby
2 | StructuredTextRenderer.new(
3 | foo.content,
4 | adapter: Adapter.new(
5 | render_text: lambda do |text|
6 | text.gsub(/this/, "that")
7 | end,
8 | render_fragment: lambda do |children|
9 | children.join("")
10 | end,
11 | render_node: lambda do |tagname, attrs, children|
12 | # we could ActionView::Helpers::TagHelper
13 | content_tag(tagname, children, attrs)
14 | end,
15 | )
16 | custom_rules: {
17 | heading: lambda do |node, children, adapter|
18 | adapter.render_node("h#{node[:level] + 1}", {}, children)
19 | end
20 | },
21 | render_link_to_record: lambda do |record, children, adapter|
22 | end,
23 | render_inline_record: lambda do |record, adapter|
24 | end,
25 | render_block: lambda do |record, adapter|
26 | end
27 | )
28 | ```
--------------------------------------------------------------------------------
/lib/dato/local/json_api_meta.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | class JsonApiMeta
6 | attr_reader :payload
7 |
8 | def initialize(payload)
9 | @payload = payload || {}
10 | end
11 |
12 | def [](key)
13 | @payload[key]
14 | end
15 |
16 | def respond_to_missing?(method, include_private = false)
17 | if @payload.key?(method)
18 | true
19 | else
20 | super
21 | end
22 | end
23 |
24 | private
25 |
26 | def method_missing(method, *arguments, &block)
27 | return super unless arguments.empty?
28 |
29 | if @payload.key?(method)
30 | @payload[method]
31 | else
32 | super
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/support/account_client_provider.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module AccountClientProvider
4 | def generate_account_client!(options = {})
5 | random_string = (0...8).map { rand(65..90).chr }.join
6 |
7 | anonymous_client = Dato::Account::Client.new(
8 | nil,
9 | base_url: ENV.fetch("ACCOUNT_API_BASE_URL"),
10 | )
11 |
12 | account = anonymous_client.account.create(
13 | email: "#{random_string}@delete-this-at-midnight-utc.tk",
14 | password: "veryst_9rong_passowrd4_",
15 | name: "Test",
16 | company: "DatoCMS",
17 | )
18 |
19 | Dato::Account::Client.new(
20 | account[:id],
21 | options.merge(
22 | base_url: ENV.fetch("ACCOUNT_API_BASE_URL"),
23 | ),
24 | )
25 | end
26 | end
27 |
28 | RSpec.configure do |_config|
29 | include AccountClientProvider
30 | end
31 |
--------------------------------------------------------------------------------
/lib/dato/dump/dsl/directory.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/dump/dsl/create_post"
4 | require "dato/dump/dsl/create_data_file"
5 | require "dato/dump/dsl/add_to_data_file"
6 |
7 | module Dato
8 | module Dump
9 | module Dsl
10 | class Directory
11 | include Dsl::CreateDataFile
12 | include Dsl::CreatePost
13 | include Dsl::AddToDataFile
14 |
15 | attr_reader :dato, :operations
16 |
17 | def initialize(dato, operations, &block)
18 | @dato = dato
19 | @operations = operations
20 | @self_before_instance_eval = eval "self", block.binding
21 |
22 | instance_eval(&block)
23 | end
24 |
25 | def method_missing(method, *args, &block)
26 | @self_before_instance_eval.send method, *args, &block
27 | end
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/dato/utils/build_modular_block.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/json_api_serializer"
4 |
5 | module Dato
6 | module Utils
7 | module BuildModularBlock
8 | def self.build(unserialized_body)
9 | json_api_serializer = JsonApiSerializer.new(type: "item")
10 | attributes = json_api_serializer.serialized_attributes(unserialized_body)
11 |
12 | payload = {
13 | type: "item",
14 | attributes: attributes,
15 | relationships: {
16 | item_type: {
17 | data: {
18 | id: unserialized_body[:item_type],
19 | type: "item_type",
20 | },
21 | },
22 | },
23 | }
24 |
25 | payload[:id] = unserialized_body[:id] if unserialized_body[:id]
26 |
27 | payload
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/dato/local/field_type/color_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | RSpec.describe Dato::Local::FieldType::Color do
9 | subject(:file) { described_class.parse(attributes, nil) }
10 | let(:attributes) do
11 | {
12 | red: 255,
13 | green: 127,
14 | blue: 0,
15 | alpha: 255,
16 | }
17 | end
18 |
19 | it "responds to red, green, blue, hex and rgb methods" do
20 | expect(file.red).to eq 255
21 | expect(file.green).to eq 127
22 | expect(file.blue).to eq 0
23 | expect(file.alpha).to eq 1.0
24 | expect(file.rgb).to eq "rgb(255, 127, 0)"
25 | expect(file.hex).to eq "#ff7f00"
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/robots_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe Robots do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | let(:result) { builder.build }
15 |
16 | context "with site noIndex set" do
17 | let(:no_index) { true }
18 |
19 | it "returns robots meta tag" do
20 | expect(result[:attributes][:content]).to eq("noindex")
21 | end
22 | end
23 |
24 | context "with site noIndex not set" do
25 | it "returns no tags" do
26 | expect(result).to be_nil
27 | end
28 | end
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/dato/dump/dsl/create_post.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/dump/operation/create_post"
4 |
5 | module Dato
6 | module Dump
7 | module Dsl
8 | class DataFile
9 | def initialize(operation, &block)
10 | @operation = operation
11 | instance_eval(&block)
12 | end
13 |
14 | def frontmatter(format, value)
15 | @operation.frontmatter_format = format
16 | @operation.frontmatter_value = value
17 | end
18 |
19 | def content(value)
20 | @operation.content = value
21 | end
22 | end
23 |
24 | module CreatePost
25 | def create_post(path, &block)
26 | operation = Operation::CreatePost.new(operations, path)
27 | DataFile.new(operation, &block)
28 |
29 | operations.add operation
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/dato/dump/operation/create_post.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "fileutils"
4 | require "dato/dump/format"
5 |
6 | module Dato
7 | module Dump
8 | module Operation
9 | class CreatePost
10 | attr_reader :context, :path
11 |
12 | attr_accessor :frontmatter_format, :frontmatter_value, :content
13 |
14 | def initialize(context, path)
15 | @context = context
16 | @path = path
17 | end
18 |
19 | def perform
20 | FileUtils.mkdir_p(File.dirname(path))
21 |
22 | File.open(File.join(context.path, path), "w") do |file|
23 | file.write Format.frontmatter_dump(
24 | frontmatter_format,
25 | frontmatter_value,
26 | )
27 | file.write "\n\n"
28 | file.write content
29 | end
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/article_modified_time_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe ArticleModifiedTime do
9 | include_context "items repo"
10 | subject(:builder) { described_class.new(item, site) }
11 |
12 | describe "#build" do
13 | let(:result) { builder.build }
14 |
15 | context "with no item" do
16 | it "returns no tags" do
17 | expect(result).to be_nil
18 | end
19 | end
20 |
21 | context "with item" do
22 | let(:item) { items_repo.articles.first }
23 |
24 | it "returns iso 8601 datetime" do
25 | expect(result[:attributes][:content]).to eq("2016-12-07T09:14:22Z")
26 | end
27 | end
28 | end
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/og_type_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe OgType do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | let(:result) { builder.build }
15 |
16 | context "with no item" do
17 | it "returns website og:type" do
18 | expect(result[:attributes][:content]).to eq("website")
19 | end
20 | end
21 |
22 | context "with item" do
23 | let(:item) { items_repo.articles.first }
24 |
25 | it "returns article og:type" do
26 | expect(result[:attributes][:content]).to eq("article")
27 | end
28 | end
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/dato/upload/file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/upload/create_upload_path"
4 |
5 | module Dato
6 | module Upload
7 | class File
8 | attr_reader :client, :source, :upload_attributes, :field_attributes
9 |
10 | def initialize(client, source, upload_attributes = {}, field_attributes = {})
11 | @client = client
12 | @source = source
13 | @upload_attributes = upload_attributes
14 | @field_attributes = field_attributes
15 | end
16 |
17 | def upload
18 | upload_path = CreateUploadPath.new(client, source).upload_path
19 |
20 | upload = client.uploads.create(
21 | upload_attributes.merge(path: upload_path),
22 | )
23 |
24 | {
25 | alt: nil,
26 | title: nil,
27 | custom_data: {},
28 | }.merge(field_attributes).merge(upload_id: upload["id"])
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'rspec' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("bundle", __dir__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("rspec-core", "rspec")
30 |
--------------------------------------------------------------------------------
/lib/dato/paginator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | class Paginator
5 | def initialize(client, base_endpoint, filters)
6 | @client = client
7 | @base_endpoint = base_endpoint
8 | @filters = filters
9 | end
10 |
11 | def response
12 | items_per_page = 100
13 |
14 | base_response = @client.get(
15 | @base_endpoint, @filters.dup.merge("page[limit]" => items_per_page)
16 | )
17 |
18 | extra_pages = (
19 | base_response[:meta][:total_count] / items_per_page.to_f
20 | ).ceil - 1
21 |
22 | extra_pages.times do |page|
23 | base_response[:data] += @client.get(
24 | @base_endpoint,
25 | @filters.dup.merge(
26 | "page[offset]" => items_per_page * (page + 1),
27 | "page[limit]" => items_per_page,
28 | ),
29 | )[:data]
30 | end
31 |
32 | base_response
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/og_site_name_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe OgSiteName do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | let(:result) { builder.build }
15 |
16 | context "with site name not set" do
17 | it "returns no tags" do
18 | expect(result).to be_nil
19 | end
20 | end
21 |
22 | context "with site name set" do
23 | let(:global_seo) do
24 | { site_name: "My site" }
25 | end
26 |
27 | it "returns og:site_name tag" do
28 | expect(result[:attributes][:content]).to eq("My site")
29 | end
30 | end
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/dato/cli_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | describe Cli do
7 | let(:client) { double("Dato::Site::Client", get: site_data) }
8 | let(:site_data) { { "data" => { "id" => "id" } } }
9 | let(:runner) { instance_double(Dato::Dump::Runner, run: nil) }
10 |
11 | before do
12 | allow(Dato::Site::Client).to receive(:new) { client }
13 | allow(client).to receive_message_chain(:items, :all).and_return({})
14 | allow(client).to receive_message_chain(:uploads, :all).and_return({})
15 | allow(Dato::Dump::Runner)
16 | .to receive(:new).with(anything, anything, anything, anything) { runner }
17 | end
18 |
19 | describe "#dump" do
20 | context "in watch mode" do
21 | it "dumps data" do
22 | described_class.start(%w[dump --token sometoken])
23 | expect(runner).to have_received(:run)
24 | end
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/seo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Seo
7 | attr_reader :title, :description, :twitter_card
8 |
9 | def self.parse(value, repo)
10 | value && new(value[:title], value[:description], value[:image], value[:twitter_card], repo)
11 | end
12 |
13 | def initialize(title, description, image, twitter_card, repo)
14 | @title = title
15 | @description = description
16 | @image = image
17 | @repo = repo
18 | @twitter_card = twitter_card
19 | end
20 |
21 | def image
22 | @image && UploadId.parse(@image, @repo)
23 | end
24 |
25 | def to_hash(*args)
26 | {
27 | title: title,
28 | description: description,
29 | image: image && image.to_hash(*args),
30 | }
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/twitter_site_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe TwitterSite do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | let(:result) { builder.build }
15 |
16 | context "with twitter account not set" do
17 | it "returns no tags" do
18 | expect(result).to be_nil
19 | end
20 | end
21 |
22 | context "with twitter account set" do
23 | let(:global_seo) do
24 | {
25 | twitter_account: "@steffoz",
26 | }
27 | end
28 |
29 | it "returns robots meta tag" do
30 | expect(result[:attributes][:content]).to eq("@steffoz")
31 | end
32 | end
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/image.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class Image < Base
9 | def build
10 | return unless image
11 |
12 | [
13 | og_tag("og:image", image.url),
14 | card_tag("twitter:image", image.url),
15 | ]
16 | end
17 |
18 | def image
19 | @image ||= seo_field_with_fallback(:image, item_image)
20 | end
21 |
22 | def item_image
23 | item && item.fields
24 | .select { |field| field.field_type == "file" }
25 | .map { |field| item[field.api_key] }
26 | .compact
27 | .find do |image|
28 | image.width && image.height &&
29 | image.width >= 200 && image.height >= 200
30 | end
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/dato/utils/build_modular_block_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | describe BuildModularBlock do
8 | describe "#build" do
9 | it "returns an array of tags" do
10 | attributes = {
11 | id: "111",
12 | item_type: "1234",
13 | title: "Title",
14 | description: "Description",
15 | }
16 | payload = {
17 | id: "111",
18 | type: "item",
19 | attributes: {
20 | title: "Title",
21 | description: "Description",
22 | },
23 | relationships: {
24 | item_type: {
25 | data: {
26 | id: "1234",
27 | type: "item_type",
28 | },
29 | },
30 | },
31 | }
32 | expect(BuildModularBlock.build(attributes)).to eq payload
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/article_publisher_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe ArticlePublisher do
9 | include_context "items repo"
10 | subject(:builder) { described_class.new(item, site) }
11 |
12 | describe "#build" do
13 | let(:result) { builder.build }
14 |
15 | context "with FB page not set" do
16 | it "returns no tags" do
17 | expect(result).to be_nil
18 | end
19 | end
20 |
21 | context "with FB page set" do
22 | let(:global_seo) do
23 | {
24 | facebook_page_url: "http://facebook.com/mark.smith",
25 | }
26 | end
27 |
28 | it "returns robots meta tag" do
29 | expect(result[:attributes][:content]).to eq("http://facebook.com/mark.smith")
30 | end
31 | end
32 | end
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/dato/dump/dsl/root.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/dump/dsl/directory"
4 | require "dato/dump/dsl/create_post"
5 | require "dato/dump/dsl/create_data_file"
6 | require "dato/dump/dsl/add_to_data_file"
7 |
8 | require "dato/dump/operation/directory"
9 |
10 | module Dato
11 | module Dump
12 | module Dsl
13 | class Root
14 | include Dsl::CreateDataFile
15 | include Dsl::CreatePost
16 | include Dsl::AddToDataFile
17 |
18 | attr_reader :dato, :operations
19 |
20 | def initialize(config_code, dato, operations)
21 | @dato = dato
22 | @operations = operations
23 |
24 | # rubocop:disable Security/Eval
25 | eval(config_code)
26 | # rubocop:enable Security/Eval
27 | end
28 |
29 | def directory(path, &block)
30 | operation = Operation::Directory.new(operations, path)
31 | operations.add operation
32 |
33 | Directory.new(dato, operation, &block)
34 | end
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Cantiere Creativo SRL
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/dato/site/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/api_client"
4 | require "dato/upload/file"
5 | require "dato/upload/create_upload_path"
6 |
7 | module Dato
8 | module Site
9 | class Client
10 | include ApiClient
11 |
12 | json_schema "site-api"
13 |
14 | def create_upload_path(path_or_url)
15 | file = Upload::CreateUploadPath.new(self, path_or_url)
16 | file.upload_path
17 | end
18 |
19 | def upload_file(path_or_url, upload_attributes = {}, field_attributes = {})
20 | file = Upload::File.new(self, path_or_url, upload_attributes, field_attributes)
21 | file.upload
22 | end
23 |
24 | def upload_image(path_or_url, upload_attributes = {}, field_attributes = {})
25 | file = Upload::File.new(self, path_or_url, upload_attributes, field_attributes)
26 | file.upload
27 | end
28 |
29 | def pusher_token(socket_id, channel)
30 | request(
31 | :post,
32 | "/pusher/authenticate",
33 | { socket_id: socket_id, channel_name: channel },
34 | )
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Stefano Verna
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/title.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/base"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | class Title < Base
9 | def build
10 | return unless item_title
11 |
12 | [
13 | content_tag("title", item_title_with_suffix),
14 | og_tag("og:title", item_title),
15 | card_tag("twitter:title", item_title),
16 | ]
17 | end
18 |
19 | def title_field
20 | item && item.item_type.title_field
21 | end
22 |
23 | def item_title
24 | @item_title ||= seo_field_with_fallback(
25 | :title,
26 | title_field && item[title_field.api_key],
27 | )
28 | end
29 |
30 | def suffix
31 | (site.global_seo && site.global_seo.title_suffix) || ""
32 | end
33 |
34 | def item_title_with_suffix
35 | if (item_title + suffix).size <= 60
36 | item_title + suffix
37 | else
38 | item_title
39 | end
40 | end
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/dato/dump/runner.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/dump/dsl/root"
4 | require "dato/dump/operation/root"
5 | require "dato/dump/ssg_detector"
6 | require "dato/local/loader"
7 |
8 | module Dato
9 | module Dump
10 | class Runner
11 | attr_reader :config_path, :client, :destination_path, :preview_mode, :loader
12 |
13 | def initialize(config_path, client, preview_mode, loader, destination_path = Dir.pwd)
14 | @config_path = config_path
15 | @preview_mode = preview_mode
16 | @client = client
17 | @destination_path = destination_path
18 | @loader = loader
19 | end
20 |
21 | def run
22 | I18n.available_locales = loader.items_repo.available_locales
23 | I18n.locale = I18n.available_locales.first
24 |
25 | Dsl::Root.new(
26 | File.read(config_path),
27 | loader.items_repo,
28 | operation,
29 | )
30 |
31 | operation.perform
32 |
33 | puts "\e[32m✓\e[0m Done!"
34 | end
35 |
36 | def operation
37 | @operation ||= Operation::Root.new(destination_path)
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/dato/json_api_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | describe JsonApiSerializer do
7 | subject(:serializer) do
8 | described_class.new(type: "menu_item", link: link)
9 | end
10 |
11 | let(:link) do
12 | response = VCR.use_cassette("json_schema", record: :new_episodes) do
13 | url = URI.parse("https://site-api.datocms.com/docs/site-api-hyperschema.json")
14 | Net::HTTP.get(url)
15 | end
16 |
17 | schema = JsonSchema.parse!(JSON.parse(response))
18 | schema.expand_references!
19 |
20 | schema.definitions["menu_item"].links.find do |x|
21 | x.rel == "create"
22 | end
23 | end
24 |
25 | describe "#serialize" do
26 | it "returns a JSON-API serialized version of the argument" do
27 | expect(serializer.serialize({ label: "Ciao", position: 1 }, "12")).to eq(
28 | data: {
29 | id: "12",
30 | type: "menu_item",
31 | attributes: {
32 | label: "Ciao",
33 | position: 1,
34 | },
35 | },
36 | )
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/dato/json_schema_type.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | class JsonSchemaType
5 | attr_reader :schema
6 |
7 | def initialize(schema)
8 | @schema = schema
9 | end
10 |
11 | def call
12 | type_property = find_info_for_type_property
13 |
14 | return nil unless type_property
15 |
16 | type_property.pattern.to_s.gsub(/(^(\(\?-mix:\^)|(\$\))$)/, "")
17 | end
18 |
19 | private
20 |
21 | def find_info_for_type_property
22 | entity = find_entity_in_data
23 |
24 | return nil unless entity
25 |
26 | entity.properties["type"]
27 | end
28 |
29 | def find_entity_in_data
30 | return nil if !schema || !schema.properties["data"]
31 |
32 | if schema.properties["data"].type.first == "array"
33 | return schema.properties["data"].items if schema.properties["data"].items
34 |
35 | return nil
36 | end
37 |
38 | return schema.properties["data"] if schema.properties["data"].type.first == "object"
39 |
40 | if schema.properties["data"].any_of
41 | return schema.properties["data"].any_of.reject { |x| x.definitions.type.example == "job" }
42 | end
43 |
44 | nil
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/dato/dump/operation/add_to_data_file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/dump/format"
4 |
5 | module Dato
6 | module Dump
7 | module Operation
8 | class AddToDataFile
9 | attr_reader :context, :path, :format, :value
10 |
11 | def initialize(context, path, format, value)
12 | @context = context
13 | @path = path
14 | @format = format
15 | @value = value
16 | end
17 |
18 | def perform
19 | complete_path = File.join(context.path, path)
20 | FileUtils.mkdir_p(File.dirname(complete_path))
21 |
22 | content_to_add = Format.dump(format, value)
23 |
24 | old_content = if File.exist? complete_path
25 | ::File.read(complete_path)
26 | else
27 | ""
28 | end
29 |
30 | new_content = old_content.sub(
31 | /\n*(#\s*datocms:start.*#\s*datocms:end|\Z)/m,
32 | "\n\n# datocms:start\n#{content_to_add}\n# datocms:end",
33 | )
34 |
35 | File.open(complete_path, "w") do |f|
36 | f.write new_content
37 | end
38 | end
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/dato/account/client_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Account
7 | describe Client, :vcr do
8 | let(:client) do
9 | generate_account_client!
10 | end
11 |
12 | describe "Not found" do
13 | it "raises Dato::ApiError" do
14 | expect { client.sites.find(9999) }.to raise_error Dato::ApiError
15 | end
16 | end
17 |
18 | describe "Account" do
19 | it "fetch, update" do
20 | account = client.account.find
21 |
22 | client.account.update(account.merge(company: "Dundler Mifflin"))
23 | expect(client.account.find[:company]).to eq "Dundler Mifflin"
24 | end
25 | end
26 |
27 | describe "Sites" do
28 | it "fetch, create, update and destroy" do
29 | name = "Integration test"
30 |
31 | new_site = client.sites.create(name: name)
32 |
33 | client.sites.update(
34 | new_site[:id],
35 | new_site.merge(name: "#{name}!"),
36 | )
37 |
38 | expect(client.sites.find(new_site[:id])[:name]).to eq("#{name}!")
39 |
40 | client.sites.destroy(new_site[:id])
41 | end
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/theme.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Theme
7 | attr_reader :primary_color, :dark_color, :light_color, :accent_color
8 |
9 | def self.parse(value, repo)
10 | value && new(
11 | value[:logo],
12 | value[:primary_color],
13 | value[:dark_color],
14 | value[:light_color],
15 | value[:accent_color],
16 | repo,
17 | )
18 | end
19 |
20 | def initialize(logo, primary_color, dark_color, light_color, accent_color, repo)
21 | @logo = logo
22 | @primary_color = primary_color
23 | @dark_color = dark_color
24 | @light_color = light_color
25 | @accent_color = accent_color
26 | @repo = repo
27 | end
28 |
29 | def logo
30 | @logo && UploadId.parse(@logo, @repo)
31 | end
32 |
33 | def to_hash(*args)
34 | {
35 | primary_color: primary_color,
36 | dark_color: dark_color,
37 | light_color: light_color,
38 | accent_color: accent_color,
39 | logo: logo && logo.to_hash(*args),
40 | }
41 | end
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/spec/dato/local/entities_repo_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | RSpec.describe EntitiesRepo do
8 | subject(:source) { described_class.new(payload) }
9 |
10 | describe "#entities" do
11 | context "a payload with a data key" do
12 | context "object" do
13 | let(:payload) do
14 | { data: { id: "bar", type: "item" } }
15 | end
16 |
17 | it "inserts the object into entities" do
18 | expect(source.entities["item"]["bar"]).to be_a JsonApiEntity
19 | end
20 | end
21 |
22 | context "array" do
23 | let(:payload) do
24 | { data: [{ id: "bar", type: "item" }] }
25 | end
26 |
27 | it "inserts the objects into entities" do
28 | expect(source.entities["item"]["bar"]).to be_a JsonApiEntity
29 | end
30 | end
31 | end
32 |
33 | context "a payload with an included key" do
34 | let(:payload) do
35 | { included: [{ id: "bar", type: "item" }] }
36 | end
37 |
38 | it "inserts the objects into entities" do
39 | expect(source.entities["item"]["bar"]).to be_a JsonApiEntity
40 | end
41 | end
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/dato/utils/seo_tags_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/utils/meta_tags/title"
4 | require "dato/utils/meta_tags/description"
5 | require "dato/utils/meta_tags/image"
6 | require "dato/utils/meta_tags/robots"
7 | require "dato/utils/meta_tags/og_locale"
8 | require "dato/utils/meta_tags/og_type"
9 | require "dato/utils/meta_tags/og_site_name"
10 | require "dato/utils/meta_tags/article_modified_time"
11 | require "dato/utils/meta_tags/article_publisher"
12 | require "dato/utils/meta_tags/twitter_card"
13 | require "dato/utils/meta_tags/twitter_site"
14 |
15 | module Dato
16 | module Utils
17 | class SeoTagsBuilder
18 | META_TAGS = [
19 | MetaTags::Title,
20 | MetaTags::Description,
21 | MetaTags::Image,
22 | MetaTags::Robots,
23 | MetaTags::OgLocale,
24 | MetaTags::OgType,
25 | MetaTags::OgSiteName,
26 | MetaTags::ArticleModifiedTime,
27 | MetaTags::ArticlePublisher,
28 | MetaTags::TwitterCard,
29 | MetaTags::TwitterSite,
30 | ].freeze
31 |
32 | attr_reader :site, :item
33 |
34 | def initialize(item, site)
35 | @item = item
36 | @site = site
37 | end
38 |
39 | def meta_tags
40 | META_TAGS.map do |klass|
41 | klass.new(item, site).build
42 | end.flatten.compact
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/spec/dato/local/field_type/upload_id_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | RSpec.describe Dato::Local::FieldType::UploadId do
9 | subject(:upload) { described_class.parse(attributes, repo) }
10 |
11 | let(:repo) { instance_double("Dato::Local::ItemsRepo", site: site, entities_repo: entities_repo) }
12 | let(:site) { instance_double("Dato::Local::Site", entity: site_entity) }
13 | let(:site_entity) { double("Dato::Local::JsonApiEntity", imgix_host: "foobar.com") }
14 | let(:entities_repo) { instance_double("Dato::Local::EntitiesRepo", find_entity: upload_entity) }
15 | let(:upload_entity) { double("Dato::Local::JsonApiEntity", attributes) }
16 |
17 | let(:attributes) do
18 | {
19 | path: "/foo.png",
20 | format: "jpg",
21 | size: 4000,
22 | width: 20,
23 | height: 20,
24 | }
25 | end
26 |
27 | it "responds to path, format and size methods" do
28 | expect(upload.path).to eq "/foo.png"
29 | expect(upload.format).to eq "jpg"
30 | expect(upload.size).to eq 4000
31 | expect(upload.width).to eq 20
32 | expect(upload.height).to eq 20
33 | end
34 |
35 | it "responds to url method" do
36 | expect(upload.url(w: 300)).to eq "https://foobar.com/foo.png?w=300"
37 | end
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/dato/local/field_type/video_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | RSpec.describe Video do
9 | subject(:video) { described_class.parse(attributes, nil) }
10 | let(:attributes) do
11 | {
12 | url: "https://www.youtube.com/watch?v=oHg5SJYRHA0",
13 | provider_uid: "oHg5SJYRHA0",
14 | thumbnail_url: "http://i3.ytimg.com/vi/oHg5SJYRHA0/hqdefault.jpg",
15 | title: "RickRoll'D",
16 | provider: "youtube",
17 | width: 560,
18 | height: 315,
19 | }
20 | end
21 |
22 | it "responds to path, format, size, width and height" do
23 | expect(video.url).to eq "https://www.youtube.com/watch?v=oHg5SJYRHA0"
24 | expect(video.provider).to eq "youtube"
25 | expect(video.provider_uid).to eq "oHg5SJYRHA0"
26 | expect(video.thumbnail_url).to eq "http://i3.ytimg.com/vi/oHg5SJYRHA0/hqdefault.jpg"
27 | expect(video.title).to eq "RickRoll'D"
28 | expect(video.width).to eq 560
29 | expect(video.height).to eq 315
30 | end
31 |
32 | describe "iframe_embed" do
33 | it "returns a iframe embed HTML fragment" do
34 | expect(video.iframe_embed).to eq ''
35 | end
36 | end
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/dato/local/field_type/seo_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | RSpec.describe Seo do
9 | subject(:seo) { described_class.parse(attributes, repo) }
10 |
11 | let(:repo) { instance_double("Dato::Local::ItemsRepo", site: site, entities_repo: entities_repo) }
12 | let(:site) { instance_double("Dato::Local::Site", entity: site_entity) }
13 | let(:site_entity) { double("Dato::Local::JsonApiEntity", imgix_host: "foobar.com") }
14 | let(:entities_repo) { instance_double("Dato::Local::EntitiesRepo", find_entity: upload_entity) }
15 | let(:upload_entity) { double("Dato::Local::JsonApiEntity", upload_attributes) }
16 |
17 | let(:attributes) do
18 | {
19 | title: "title",
20 | description: "description",
21 | image: upload_entity,
22 | }
23 | end
24 |
25 | let(:upload_attributes) do
26 | {
27 | id: "333",
28 | path: "/foo.png",
29 | format: "jpg",
30 | size: 4000,
31 | width: 20,
32 | height: 20,
33 | }
34 | end
35 |
36 | it "responds to title, description and image methods" do
37 | expect(seo.title).to eq "title"
38 | expect(seo.description).to eq "description"
39 | expect(seo.image).to be_a Dato::Local::FieldType::UploadId
40 | expect(seo.image.path).to eq "/foo.png"
41 | end
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/global_seo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/local/field_type/seo"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | class GlobalSeo
9 | attr_reader :site_name, :title_suffix, :twitter_account, :facebook_page_url
10 |
11 | def self.parse(value, repo)
12 | value && new(
13 | value[:site_name],
14 | value[:title_suffix],
15 | value[:twitter_account],
16 | value[:facebook_page_url],
17 | value[:fallback_seo],
18 | repo,
19 | )
20 | end
21 |
22 | def initialize(
23 | site_name,
24 | title_suffix,
25 | twitter_account,
26 | facebook_page_url,
27 | fallback_seo,
28 | repo
29 | )
30 | @site_name = site_name
31 | @title_suffix = title_suffix
32 | @twitter_account = twitter_account
33 | @facebook_page_url = facebook_page_url
34 | @fallback_seo = fallback_seo
35 | @repo = repo
36 | end
37 |
38 | def fallback_seo
39 | @fallback_seo && Seo.parse(@fallback_seo, @repo)
40 | end
41 |
42 | def to_hash(*args)
43 | {
44 | site_name: site_name,
45 | title_suffix: title_suffix,
46 | twitter_account: twitter_account,
47 | facebook_page_url: facebook_page_url,
48 | fallback_seo: fallback_seo && fallback_seo.to_hash(*args),
49 | }
50 | end
51 | end
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 |
2 | AllCops:
3 | TargetRubyVersion: 2.5
4 | NewCops: enable
5 |
6 | Exclude:
7 | - "dato.gemspec"
8 |
9 | Style/SafeNavigation:
10 | Enabled: false
11 |
12 | Style/Documentation:
13 | Enabled: false
14 |
15 | Style/GuardClause:
16 | Enabled: false
17 |
18 | Naming/PredicateName:
19 | Enabled: false
20 |
21 | Style/MissingRespondToMissing:
22 | Enabled: false
23 |
24 | Style/EvalWithLocation:
25 | Enabled: false
26 |
27 | Metrics/BlockNesting:
28 | Enabled: false
29 |
30 | Metrics/MethodLength:
31 | Enabled: false
32 |
33 | Metrics/BlockLength:
34 | Enabled: false
35 |
36 | Metrics/ClassLength:
37 | Enabled: false
38 |
39 | Metrics/ModuleLength:
40 | Enabled: false
41 |
42 | Metrics/CyclomaticComplexity:
43 | Enabled: false
44 |
45 | Metrics/AbcSize:
46 | Enabled: false
47 |
48 | Metrics/PerceivedComplexity:
49 | Enabled: false
50 |
51 | Layout/LineLength:
52 | Exclude:
53 | - "spec/**/*"
54 | - "lib/dato/site/repo/*"
55 | - "lib/dato/account/repo/*"
56 | - "lib/dato/account/repo/*"
57 | - "dato.gemspec"
58 |
59 | Metrics/ParameterLists:
60 | Exclude:
61 | - "lib/dato/local/field_type/*"
62 |
63 | Style/TrailingCommaInArrayLiteral:
64 | EnforcedStyleForMultiline: comma
65 |
66 | Style/TrailingCommaInArguments:
67 | EnforcedStyleForMultiline: comma
68 |
69 | Style/TrailingCommaInHashLiteral:
70 | EnforcedStyleForMultiline: comma
71 |
72 | Style/AndOr:
73 | EnforcedStyle: conditionals
74 |
75 | Style/StringLiterals:
76 | EnforcedStyle: double_quotes
77 |
78 | Style/StringLiteralsInInterpolation:
79 | EnforcedStyle: double_quotes
80 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/color.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Color
7 | attr_reader :red, :green, :blue
8 |
9 | def self.parse(value, _repo)
10 | value && new(
11 | value[:red],
12 | value[:green],
13 | value[:blue],
14 | value[:alpha],
15 | )
16 | end
17 |
18 | def initialize(red, green, blue, alpha)
19 | @red = red
20 | @green = green
21 | @blue = blue
22 | @alpha = alpha
23 | end
24 |
25 | def rgb
26 | if @alpha == 255
27 | "rgb(#{red}, #{green}, #{blue})"
28 | else
29 | "rgba(#{red}, #{green}, #{blue}, #{alpha})"
30 | end
31 | end
32 |
33 | def alpha
34 | @alpha / 255.0
35 | end
36 |
37 | def hex
38 | r = red.to_s(16)
39 | g = green.to_s(16)
40 | b = blue.to_s(16)
41 | a = (alpha * 255).to_i.to_s(16)
42 |
43 | r = "0#{r}" if r.length == 1
44 | g = "0#{g}" if g.length == 1
45 | b = "0#{b}" if b.length == 1
46 | a = "0#{a}" if a.length == 1
47 |
48 | hex = "##{r}#{g}#{b}"
49 |
50 | hex += a if a != "ff"
51 |
52 | hex
53 | end
54 |
55 | def to_hash(*_args)
56 | {
57 | red: red,
58 | green: green,
59 | blue: blue,
60 | rgb: rgb,
61 | hex: hex,
62 | }
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/dato/json_schema_relationships.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | class JsonSchemaRelationships
5 | attr_reader :schema
6 |
7 | def initialize(schema)
8 | @schema = schema
9 | end
10 |
11 | def relationships
12 | return {} if !schema || !schema.properties["data"]
13 |
14 | entity = if schema.properties["data"].type.first == "array"
15 | schema.properties["data"].items
16 | else
17 | schema.properties["data"]
18 | end
19 |
20 | return {} if !entity || !entity.properties["relationships"] || !entity.properties["relationships"]
21 |
22 | relationships = entity.properties["relationships"].properties
23 |
24 | relationships.each_with_object({}) do |(relationship, schema), acc|
25 | is_collection = schema.properties["data"].type.first == "array"
26 |
27 | types = if is_collection
28 | [type(schema.properties["data"].items)]
29 | elsif schema.properties["data"].type.first == "object"
30 | [type(schema.properties["data"])]
31 | else
32 | schema.properties["data"].any_of.map do |option|
33 | type(option)
34 | end.compact
35 | end
36 |
37 | acc[relationship.to_sym] = {
38 | collection: is_collection,
39 | types: types,
40 | }
41 | end
42 | end
43 |
44 | def type(definition)
45 | definition.properties["type"].pattern.source.gsub(/(^\^|\$$)/, "") if definition.properties["type"]
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/dato/utils/meta_tags/base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "forwardable"
4 | require "dato/local/field_type/seo"
5 |
6 | module Dato
7 | module Utils
8 | module MetaTags
9 | class Base
10 | attr_reader :site, :item
11 |
12 | def initialize(item, site)
13 | @item = item
14 | @site = site
15 | end
16 |
17 | def seo_field_with_fallback(attribute, alternative)
18 | fallback_seo = site.global_seo && site.global_seo.fallback_seo
19 |
20 | seo_field = item &&
21 | item.fields.detect { |f| f.field_type == "seo" }
22 |
23 | item_seo_value = seo_field &&
24 | item[seo_field.api_key] &&
25 | item[seo_field.api_key].send(attribute)
26 |
27 | fallback_seo_value = fallback_seo &&
28 | fallback_seo.send(attribute)
29 |
30 | item_seo_value.presence || alternative.presence || fallback_seo_value
31 | end
32 |
33 | def tag(tag_name, attributes)
34 | { tag_name: tag_name, attributes: attributes }
35 | end
36 |
37 | def meta_tag(name, content)
38 | tag("meta", name: name, content: content)
39 | end
40 |
41 | def og_tag(property, content)
42 | tag("meta", property: property, content: content)
43 | end
44 |
45 | def card_tag(name, content)
46 | meta_tag(name, content)
47 | end
48 |
49 | def content_tag(tag_name, content)
50 | { tag_name: tag_name, content: content }
51 | end
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/dato/json_api_deserializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/json_schema_relationships"
4 |
5 | module Dato
6 | class JsonApiDeserializer
7 | attr_reader :schema
8 |
9 | def initialize(schema)
10 | @schema = schema
11 | end
12 |
13 | def deserialize(data)
14 | return nil unless data
15 |
16 | data = data[:data]
17 |
18 | if data.is_a? Array
19 | data.map { |resource| deserialize_resource(resource) }
20 | else
21 | deserialize_resource(data)
22 | end
23 | end
24 |
25 | def deserialize_resource(data)
26 | result = { id: data[:id] }
27 | result[:meta] = data[:meta] if data[:meta]
28 | result.merge!(data[:attributes]) if data[:attributes]
29 |
30 | if data[:relationships]
31 | relationships.each do |relationship, meta|
32 | next unless data[:relationships][relationship]
33 |
34 | rel_data = data[:relationships][relationship][:data]
35 |
36 | result[relationship] = if meta[:types].length > 1
37 | rel_data
38 | elsif !rel_data
39 | nil
40 | elsif meta[:collection]
41 | rel_data.map { |ref| ref[:id] }
42 | else
43 | rel_data[:id]
44 | end
45 | end
46 | end
47 |
48 | result.with_indifferent_access
49 | end
50 |
51 | def relationships
52 | @relationships ||= JsonSchemaRelationships.new(schema).relationships
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/upload_id.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "imgix"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | class UploadId
9 | attr_reader :path, :format, :size, :width, :height
10 |
11 | def self.parse(upload_id, repo)
12 | if upload_id
13 | upload = repo.entities_repo.find_entity("upload", upload_id)
14 |
15 | if upload
16 | new(
17 | upload.path,
18 | upload.format,
19 | upload.size,
20 | upload.width,
21 | upload.height,
22 | repo.site.entity.imgix_host,
23 | )
24 | end
25 | end
26 | end
27 |
28 | def initialize(
29 | path,
30 | format,
31 | size,
32 | width,
33 | height,
34 | imgix_host
35 | )
36 | @path = path
37 | @format = format
38 | @size = size
39 | @imgix_host = imgix_host
40 | @width = width
41 | @height = height
42 | end
43 |
44 | def file
45 | Imgix::Client.new(
46 | domain: @imgix_host,
47 | secure: true,
48 | include_library_param: false,
49 | ).path(path)
50 | end
51 |
52 | def url(opts = {})
53 | file.to_url(opts)
54 | end
55 |
56 | def to_hash(*_args)
57 | {
58 | format: format,
59 | size: size,
60 | width: width,
61 | height: height,
62 | url: url,
63 | }
64 | end
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/dato/local/entities_repo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/local/json_api_entity"
4 |
5 | module Dato
6 | module Local
7 | class EntitiesRepo
8 | attr_reader :entities
9 |
10 | def initialize(*payloads)
11 | @entities = {}
12 | upsert_entities(*payloads)
13 | end
14 |
15 | def find_entities_of_type(type)
16 | entities.fetch(type, {}).values
17 | end
18 |
19 | def find_entity(type, id)
20 | entities.fetch(type, {}).fetch(id, nil)
21 | end
22 |
23 | def destroy_entities(type, ids)
24 | ids.each do |id|
25 | entities.fetch(type, {}).delete(id)
26 | end
27 | end
28 |
29 | def destroy_item_type(id)
30 | entities.fetch("item", {}).delete_if { |_item_id, item| item.item_type.id == id }
31 | entities.fetch("item_type", {}).delete(id)
32 | end
33 |
34 | def upsert_entities(*payloads)
35 | payloads.each do |payload|
36 | EntitiesRepo.payload_entities(payload).each do |entity_payload|
37 | object = JsonApiEntity.new(entity_payload, self)
38 | @entities[object.type] ||= {}
39 | @entities[object.type][object.id] = object
40 | end
41 | end
42 | end
43 |
44 | def self.payload_entities(payload)
45 | acc = []
46 |
47 | if payload[:data]
48 | acc = if payload[:data].is_a? Array
49 | acc + payload[:data]
50 | else
51 | acc + [payload[:data]]
52 | end
53 | end
54 |
55 | acc += payload[:included] if payload[:included]
56 |
57 | acc
58 | end
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/structured_text.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class StructuredText
7 | def self.parse(value, repo)
8 | new(value, repo)
9 | end
10 |
11 | def initialize(value, repo)
12 | @value = value
13 | @repo = repo
14 | end
15 |
16 | attr_reader :value
17 |
18 | def blocks
19 | find_all_nodes("block").map do |node|
20 | @repo.find(node["item"])
21 | end.uniq
22 | end
23 |
24 | def links
25 | find_all_nodes(%w[inlineItem itemLink]).map do |node|
26 | @repo.find(node["item"])
27 | end.uniq
28 | end
29 |
30 | def find_all_nodes(types)
31 | return [] if value.nil?
32 |
33 | types = Array(types)
34 | result = []
35 |
36 | visit(value["document"]) do |node|
37 | result << node if node.is_a?(Hash) && types.include?(node["type"])
38 | end
39 |
40 | result
41 | end
42 |
43 | def visit(node, &block)
44 | if node.is_a?(Hash) && node["children"].is_a?(Array)
45 | node["children"].each do |child|
46 | visit(child, &block)
47 | end
48 | end
49 |
50 | block.call(node)
51 | end
52 |
53 | def to_hash(max_depth = 3, current_depth = 0)
54 | {
55 | value: value,
56 | links: links.map { |item| item.to_hash(max_depth, current_depth) },
57 | blocks: blocks.map { |item| item.to_hash(max_depth, current_depth) },
58 | }
59 | end
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/dato/dump/ssg_detector_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Dump
7 | RSpec.describe SsgDetector do
8 | subject(:detector) { described_class.new(path) }
9 |
10 | describe ".detect" do
11 | context "package.json-based SSGs" do
12 | let(:path) { "./spec/fixtures/ssg/hexo" }
13 |
14 | it "detects it" do
15 | expect(detector.detect).to eq "hexo"
16 | end
17 | end
18 |
19 | context "Gemfile-based SSGs" do
20 | let(:path) { "./spec/fixtures/ssg/middleman" }
21 |
22 | it "detects it" do
23 | expect(detector.detect).to eq "middleman"
24 | end
25 | end
26 |
27 | context "Hugo SSGs" do
28 | context "with config.toml" do
29 | let(:path) { "./spec/fixtures/ssg/hugo/toml" }
30 |
31 | it "detects it" do
32 | expect(detector.detect).to eq "hugo"
33 | end
34 | end
35 |
36 | context "with config.yaml" do
37 | let(:path) { "./spec/fixtures/ssg/hugo/yaml" }
38 |
39 | it "detects it" do
40 | expect(detector.detect).to eq "hugo"
41 | end
42 | end
43 |
44 | context "with config.json" do
45 | let(:path) { "./spec/fixtures/ssg/hugo/json" }
46 |
47 | it "detects it" do
48 | expect(detector.detect).to eq "hugo"
49 | end
50 | end
51 | end
52 |
53 | context "otherwise" do
54 | let(:path) { "." }
55 |
56 | it 'fall backs to "unknown"' do
57 | expect(detector.detect).to eq "unknown"
58 | end
59 | end
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "simplecov"
4 | require "coveralls"
5 |
6 | ENV["SITE_API_BASE_URL"] ||= "https://site-api.datocms.com"
7 | ENV["ACCOUNT_API_BASE_URL"] ||= "https://account-api.datocms.com"
8 |
9 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
10 | SimpleCov::Formatter::HTMLFormatter,
11 | Coveralls::SimpleCov::Formatter,
12 | ])
13 |
14 | SimpleCov.start
15 |
16 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
17 |
18 | Dir["spec/support/**/*.rb"].each { |f| require_relative "../#{f}" }
19 |
20 | require "pry"
21 | require "vcr"
22 | require "i18n"
23 | require "i18n/backend/fallbacks"
24 | require "webmock/rspec"
25 |
26 | I18n.enforce_available_locales = false
27 | I18n.available_locales = %i[it en ru]
28 | I18n::Backend::Simple.include I18n::Backend::Fallbacks
29 | I18n.fallbacks[:ru] = [:"es-ES"]
30 |
31 | VCR.configure do |config|
32 | config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
33 | config.hook_into :webmock
34 | config.preserve_exact_body_bytes do |http_message|
35 | http_message.body.encoding.name == "ASCII-8BIT" ||
36 | !http_message.body.valid_encoding?
37 | end
38 | config.configure_rspec_metadata!
39 |
40 | config.register_request_matcher :modified_body do |request1, request2|
41 | if URI(request1.uri).path == "/account" &&
42 | URI(request2.uri).path == "/account" &&
43 | request1.method == request2.method
44 |
45 | true
46 | else
47 | request1.body == request2.body
48 | end
49 | end
50 |
51 | config.default_cassette_options = {
52 | match_requests_on: %i[method uri query modified_body],
53 | }
54 | end
55 |
56 | require "dato"
57 |
--------------------------------------------------------------------------------
/lib/dato/local/site.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "forwardable"
4 | require "dato/utils/locale_value"
5 |
6 | module Dato
7 | module Local
8 | class Site
9 | extend Forwardable
10 |
11 | attr_reader :entity
12 |
13 | def_delegators :entity, :id, :name, :locales, :domain,
14 | :internal_domain, :no_index, :frontend_url
15 |
16 | def initialize(entity, items_repo)
17 | @entity = entity
18 | @items_repo = items_repo
19 | end
20 |
21 | def global_seo
22 | read_attribute(:global_seo, FieldType::GlobalSeo, locales.size > 1)
23 | end
24 |
25 | def theme
26 | read_attribute(:theme, FieldType::Theme, false)
27 | end
28 |
29 | def favicon
30 | read_attribute(:favicon, FieldType::UploadId, false)
31 | end
32 |
33 | def to_s
34 | "#"
35 | end
36 | alias inspect to_s
37 |
38 | def favicon_meta_tags(theme_color = nil)
39 | Utils::FaviconTagsBuilder.new(self, theme_color).meta_tags
40 | end
41 |
42 | def to_hash
43 | attributes = %i[
44 | id name locales theme domain internal_domain
45 | no_index global_seo favicon frontend_url
46 | ]
47 |
48 | attributes.each_with_object({}) do |attribute, result|
49 | value = send(attribute)
50 | result[attribute] = if value.respond_to?(:to_hash)
51 | value.to_hash
52 | else
53 | value
54 | end
55 | end
56 | end
57 |
58 | private
59 |
60 | def read_attribute(method, type_klass, localized)
61 | value = if localized
62 | obj = entity[method] || {}
63 |
64 | Utils::LocaleValue.find(obj)
65 | else
66 | entity[method]
67 | end
68 |
69 | type_klass.parse(value, @items_repo)
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/dato.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # frozen_string_literal: true
3 | lib = File.expand_path('../lib', __FILE__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require 'dato/version'
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = 'dato'
9 | spec.version = Dato::VERSION
10 | spec.authors = ['Stefano Verna']
11 | spec.email = ['s.verna@cantierecreativo.net']
12 |
13 | spec.summary = 'Ruby client for DatoCMS API'
14 | spec.description = 'Ruby client for DatoCMS API'
15 | spec.homepage = 'https://github.com/datocms/ruby-datocms-client'
16 | spec.license = 'MIT'
17 |
18 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|build|spec|features)/}) }
19 | spec.bindir = 'exe'
20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21 | spec.require_paths = ['lib']
22 |
23 | spec.add_development_dependency 'bundler'
24 | spec.add_development_dependency 'rake'
25 | spec.add_development_dependency 'rspec'
26 | spec.add_development_dependency 'rubyzip'
27 | spec.add_development_dependency 'simplecov', '~> 0.17.0'
28 | spec.add_development_dependency 'vcr'
29 | spec.add_development_dependency 'webmock'
30 | spec.add_development_dependency 'rubocop'
31 | spec.add_development_dependency 'coveralls'
32 | spec.add_development_dependency 'pry'
33 | spec.add_development_dependency 'pry-byebug'
34 | spec.add_development_dependency 'front_matter_parser'
35 |
36 | spec.add_runtime_dependency 'faraday', ['>= 0.9.0']
37 | spec.add_runtime_dependency 'faraday_middleware', ['>= 0.9.0']
38 | spec.add_runtime_dependency 'activesupport', ['>= 4.2.7']
39 | spec.add_runtime_dependency 'addressable'
40 | spec.add_runtime_dependency 'thor'
41 | spec.add_runtime_dependency 'imgix', ['~> 4']
42 | spec.add_runtime_dependency 'toml'
43 | spec.add_runtime_dependency 'cacert'
44 | spec.add_runtime_dependency 'dotenv'
45 | spec.add_runtime_dependency 'pusher-client'
46 | spec.add_runtime_dependency 'listen'
47 | spec.add_runtime_dependency 'dato_json_schema'
48 | spec.add_runtime_dependency 'mime-types'
49 | end
50 |
--------------------------------------------------------------------------------
/lib/dato/local/json_api_entity.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/local/json_api_meta"
4 |
5 | module Dato
6 | module Local
7 | class JsonApiEntity
8 | attr_reader :payload, :data_source
9 |
10 | def initialize(payload, data_source)
11 | @payload = payload
12 | @data_source = data_source
13 | end
14 |
15 | def id
16 | @payload[:id]
17 | end
18 |
19 | def type
20 | @payload[:type]
21 | end
22 |
23 | def meta
24 | @meta ||= JsonApiMeta.new(@payload[:meta])
25 | end
26 |
27 | def ==(other)
28 | if other.is_a? JsonApiEntity
29 | id == other.id && type == other.type
30 | else
31 | false
32 | end
33 | end
34 |
35 | def to_s
36 | "#"
37 | end
38 | alias inspect to_s
39 |
40 | def [](key)
41 | attributes[key.to_sym]
42 | end
43 |
44 | def respond_to_missing?(method, include_private = false)
45 | if attributes.key?(method) || relationships.key?(method)
46 | true
47 | else
48 | super
49 | end
50 | end
51 |
52 | private
53 |
54 | def attributes
55 | @payload.fetch(:attributes, {})
56 | end
57 |
58 | def relationships
59 | @payload.fetch(:relationships, {})
60 | end
61 |
62 | def dereference_linkage(linkage)
63 | case linkage
64 | when Array
65 | linkage.map do |item|
66 | data_source.find_entity(item[:type], item[:id])
67 | end
68 | when Hash
69 | data_source.find_entity(linkage[:type], linkage[:id])
70 | end
71 | end
72 |
73 | def method_missing(method, *arguments, &block)
74 | return super unless arguments.empty?
75 |
76 | if attributes.key?(method)
77 | attributes[method]
78 | elsif relationships.key?(method)
79 | dereference_linkage(relationships[method][:data])
80 | else
81 | super
82 | end
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/dato/upload/create_upload_path.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "mime/types"
4 | require "tempfile"
5 | require "addressable"
6 | require "net/http"
7 |
8 | module Dato
9 | module Upload
10 | class CreateUploadPath
11 | attr_reader :client, :source
12 |
13 | def initialize(client, source)
14 | @client = client
15 | @source = source
16 | end
17 |
18 | def file
19 | @file ||= if http_source?
20 | uri = Addressable::URI.parse(source)
21 | ext = ::File.extname(uri.path).downcase
22 | tempfile = Tempfile.new(["file", ext])
23 | tempfile.binmode
24 | tempfile.write(download_file(source))
25 | tempfile.rewind
26 | tempfile
27 | else
28 | ::File.new(::File.expand_path(source))
29 | end
30 | end
31 |
32 | def http_source?
33 | uri = Addressable::URI.parse(source)
34 | uri.scheme == "http" || uri.scheme == "https"
35 | rescue Addressable::URI::InvalidURIError
36 | false
37 | end
38 |
39 | def filename
40 | if http_source?
41 | ::File.basename(source)
42 | else
43 | ::File.basename(file.path)
44 | end
45 | end
46 |
47 | def upload_path
48 | upload_request = client.upload_request.create(filename: filename)
49 | uri = URI.parse(upload_request[:url])
50 |
51 | mime_type = MIME::Types.of(filename).first
52 |
53 | request = Net::HTTP::Put.new(uri)
54 | request.add_field("Content-Type", mime_type.to_s) if mime_type
55 | request.body = file.read
56 |
57 | http = Net::HTTP.new(uri.host, uri.port)
58 | http.use_ssl = true
59 |
60 | http.request(request)
61 |
62 | upload_request[:id]
63 | end
64 |
65 | def download_file(url)
66 | connection = Faraday.new do |c|
67 | c.response :raise_error
68 | c.use FaradayMiddleware::FollowRedirects
69 | c.adapter :net_http
70 | end
71 | connection.get(url).body
72 | rescue Faraday::Error => e
73 | puts "Error during upload of #{url}: #{e.message}"
74 | raise e
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/dato/utils/favicon_tags_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Utils
5 | class FaviconTagsBuilder
6 | attr_reader :theme_color, :site
7 |
8 | APPLE_TOUCH_ICON_SIZES = [57, 60, 72, 76, 114, 120, 144, 152, 180].freeze
9 | ICON_SIZES = [16, 32, 96, 192].freeze
10 | WINDOWS_SIZES = [[70, 70], [150, 150], [310, 310], [310, 150]].freeze
11 |
12 | def initialize(site, theme_color)
13 | @site = site
14 | @theme_color = theme_color
15 | end
16 |
17 | def meta_tags
18 | [
19 | build_icon_tags,
20 | build_apple_icon_tags,
21 | build_windows_tags,
22 | build_color_tags,
23 | build_app_name_tag,
24 | ].flatten.compact
25 | end
26 |
27 | def build_apple_icon_tags
28 | return unless site.favicon
29 |
30 | APPLE_TOUCH_ICON_SIZES.map do |size|
31 | link_tag(
32 | "apple-touch-icon",
33 | url(size),
34 | sizes: "#{size}x#{size}",
35 | )
36 | end
37 | end
38 |
39 | def build_icon_tags
40 | return unless site.favicon
41 |
42 | ICON_SIZES.map do |size|
43 | link_tag(
44 | "icon",
45 | url(size),
46 | sizes: "#{size}x#{size}",
47 | type: "image/#{site.favicon.format}",
48 | )
49 | end
50 | end
51 |
52 | def build_windows_tags
53 | return unless site.favicon
54 |
55 | WINDOWS_SIZES.map do |(w, h)|
56 | meta_tag("msapplication-square#{w}x#{h}logo", url(w, h))
57 | end
58 | end
59 |
60 | def build_app_name_tag
61 | meta_tag("application-name", site.name)
62 | end
63 |
64 | def build_color_tags
65 | return unless theme_color
66 |
67 | [
68 | meta_tag("theme-color", theme_color),
69 | meta_tag("msapplication-TileColor", theme_color),
70 | ]
71 | end
72 |
73 | def url(width, height = width)
74 | site.favicon.url(w: width, h: height)
75 | end
76 |
77 | def meta_tag(name, value)
78 | { tag_name: "meta", attributes: { name: name, content: value } }
79 | end
80 |
81 | def link_tag(rel, href, attrs = {})
82 | { tag_name: "link", attributes: attrs.merge(rel: rel, href: href) }
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, and in the interest of
4 | fostering an open and welcoming community, we pledge to respect all people who
5 | contribute through reporting issues, posting feature requests, updating
6 | documentation, submitting pull requests or patches, and other activities.
7 |
8 | We are committed to making participation in this project a harassment-free
9 | experience for everyone, regardless of level of experience, gender, gender
10 | identity and expression, sexual orientation, disability, personal appearance,
11 | body size, race, ethnicity, age, religion, or nationality.
12 |
13 | Examples of unacceptable behavior by participants include:
14 |
15 | * The use of sexualized language or imagery
16 | * Personal attacks
17 | * Trolling or insulting/derogatory comments
18 | * Public or private harassment
19 | * Publishing other's private information, such as physical or electronic
20 | addresses, without explicit permission
21 | * Other unethical or unprofessional conduct
22 |
23 | Project maintainers have the right and responsibility to remove, edit, or
24 | reject comments, commits, code, wiki edits, issues, and other contributions
25 | that are not aligned to this Code of Conduct, or to ban temporarily or
26 | permanently any contributor for other behaviors that they deem inappropriate,
27 | threatening, offensive, or harmful.
28 |
29 | By adopting this Code of Conduct, project maintainers commit themselves to
30 | fairly and consistently applying these principles to every aspect of managing
31 | this project. Project maintainers who do not follow or enforce the Code of
32 | Conduct may be permanently removed from the project team.
33 |
34 | This code of conduct applies both within project spaces and in public spaces
35 | when an individual is representing the project or its community.
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
38 | reported by contacting a project maintainer at s.verna@cantierecreativo.net. All
39 | complaints will be reviewed and investigated and will result in a response that
40 | is deemed necessary and appropriate to the circumstances. Maintainers are
41 | obligated to maintain confidentiality with regard to the reporter of an
42 | incident.
43 |
44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45 | version 1.3.0, available at
46 | [http://contributor-covenant.org/version/1/3/0/][version]
47 |
48 | [homepage]: http://contributor-covenant.org
49 | [version]: http://contributor-covenant.org/version/1/3/0/
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/twitter_card_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe TwitterCard do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | let(:result) { builder.build }
15 | let(:card_value) { result[:attributes][:content] }
16 |
17 | context "with no fallback seo" do
18 | context "with no item" do
19 | it "returns no tags" do
20 | expect(card_value).to eq("summary")
21 | end
22 | end
23 |
24 | context "with item" do
25 | let(:item) { items_repo.articles.first }
26 |
27 | context "with no title" do
28 | context "no SEO" do
29 | it "returns no tags" do
30 | expect(card_value).to eq("summary")
31 | end
32 | end
33 |
34 | context "with SEO" do
35 | let(:seo) do
36 | { twitter_card: "foobar" }
37 | end
38 |
39 | it "returns seo title" do
40 | expect(card_value).to eq("foobar")
41 | end
42 | end
43 | end
44 | end
45 | end
46 |
47 | context "with fallback seo" do
48 | let(:global_seo) do
49 | {
50 | fallback_seo: {
51 | twitter_card: "default_summary",
52 | },
53 | }
54 | end
55 |
56 | context "with no item" do
57 | it "returns fallback title" do
58 | expect(card_value).to eq("default_summary")
59 | end
60 | end
61 |
62 | context "with item" do
63 | let(:item) { items_repo.articles.first }
64 |
65 | context "no SEO" do
66 | it "returns fallback title" do
67 | expect(card_value).to eq("default_summary")
68 | end
69 | end
70 |
71 | context "with SEO" do
72 | let(:seo) do
73 | { twitter_card: "item_summary" }
74 | end
75 |
76 | it "returns seo title" do
77 | expect(card_value).to eq("item_summary")
78 | end
79 | end
80 | end
81 | end
82 | end
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/video.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dato
4 | module Local
5 | module FieldType
6 | class Video
7 | attr_reader :url, :thumbnail_url, :title, :width, :height, :provider, :provider_url, :provider_uid
8 |
9 | def self.parse(value, _repo)
10 | value && new(
11 | value[:url],
12 | value[:thumbnail_url],
13 | value[:title],
14 | value[:width],
15 | value[:height],
16 | value[:provider],
17 | value[:provider_url],
18 | value[:provider_uid],
19 | )
20 | end
21 |
22 | def initialize(
23 | url,
24 | thumbnail_url,
25 | title,
26 | width,
27 | height,
28 | provider,
29 | provider_url,
30 | provider_uid
31 | )
32 | @url = url
33 | @thumbnail_url = thumbnail_url
34 | @title = title
35 | @width = width
36 | @height = height
37 | @provider = provider
38 | @provider_url = provider_url
39 | @provider_uid = provider_uid
40 | end
41 |
42 | def iframe_embed(width = self.width, height = self.height)
43 | # rubocop:disable Layout/LineLength
44 | case provider
45 | when "youtube"
46 | %()
47 | when "vimeo"
48 | %()
49 | when "facebook"
50 | %()
51 | end
52 | # rubocop:enable Layout/LineLength
53 | end
54 |
55 | def to_hash(*_args)
56 | {
57 | url: url,
58 | thumbnail_url: thumbnail_url,
59 | title: title,
60 | width: width,
61 | height: height,
62 | provider: provider,
63 | provider_url: provider_url,
64 | provider_uid: provider_uid,
65 | }
66 | end
67 | end
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/dato/cli.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "thor"
4 | require "dato/dump/runner"
5 | require "dato/dump/ssg_detector"
6 | require "listen"
7 | module Dato
8 | class Cli < Thor
9 | package_name "DatoCMS"
10 |
11 | desc "dump", "dumps DatoCMS content into local files"
12 | option :config, default: "dato.config.rb"
13 | option :token, default: ENV["DATO_API_TOKEN"], required: true
14 | option :environment, type: :string, required: false
15 | option :preview, default: false, type: :boolean
16 | option :watch, default: false, type: :boolean
17 |
18 | def dump
19 | config_file = File.expand_path(options[:config])
20 | watch_mode = options[:watch]
21 | preview_mode = options[:preview]
22 |
23 | client = Dato::Site::Client.new(
24 | options[:token],
25 | environment: options[:environment],
26 | extra_headers: {
27 | "X-Reason" => "dump",
28 | "X-SSG" => Dump::SsgDetector.new(Dir.pwd).detect,
29 | },
30 | )
31 | loader = Dato::Local::Loader.new(client, preview_mode)
32 | puts "Fetching content from DatoCMS..."
33 | loader.load
34 |
35 | if watch_mode
36 | semaphore = Mutex.new
37 |
38 | thread_safe_dump(semaphore, config_file, client, preview_mode, loader)
39 |
40 | loader.watch do
41 | thread_safe_dump(semaphore, config_file, client, preview_mode, loader)
42 | end
43 |
44 | watch_config_file(config_file) do
45 | thread_safe_dump(semaphore, config_file, client, preview_mode, loader)
46 | end
47 |
48 | sleep
49 | else
50 | Dump::Runner.new(config_file, client, preview_mode, loader).run
51 | end
52 | end
53 |
54 | desc "check", "checks the presence of a DatoCMS token"
55 | def check
56 | exit 0 if ENV["DATO_API_TOKEN"]
57 |
58 | say "Site token is not specified!"
59 | token = ask "Please paste your DatoCMS site read-only API token:\n>"
60 |
61 | if !token || token.empty?
62 | puts "Missing token"
63 | exit 1
64 | end
65 |
66 | File.open(".env", "a") do |file|
67 | file.puts "DATO_API_TOKEN=#{token}"
68 | end
69 |
70 | say "Token added to .env file."
71 |
72 | exit 0
73 | end
74 |
75 | no_tasks do
76 | def watch_config_file(config_file, &block)
77 | Listen.to(
78 | File.dirname(config_file),
79 | only: /#{Regexp.quote(File.basename(config_file))}/,
80 | &block
81 | ).start
82 | end
83 |
84 | def thread_safe_dump(semaphore, config_file, client, preview_mode, loader)
85 | semaphore.synchronize do
86 | Dump::Runner.new(config_file, client, preview_mode, loader).run
87 | end
88 | end
89 | end
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/spec/dato/local/items_repo_integration_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | RSpec.describe ItemsRepo, :vcr do
8 | include_context "with a new site"
9 |
10 | subject(:repo) do
11 | loader = Loader.new(client)
12 | loader.load
13 | loader.items_repo
14 | end
15 |
16 | describe ".to_hash" do
17 | it "dump everything you might need" do
18 | expect(repo.available_locales).to eq %i[en it]
19 |
20 | serialized_site = repo.site.to_hash
21 | expect(serialized_site[:name]).to eq "Integration new test site"
22 | expect(serialized_site[:locales]).to eq %w[en it]
23 |
24 | article_type = repo.item_types.find { |it| it[:api_key] == "article" }
25 | serialized_article = repo.items_of_type(article_type).first.to_hash
26 | expect(serialized_article[:item_type]).to eq "article"
27 | expect(serialized_article[:updated_at]).to be_present
28 | expect(serialized_article[:created_at]).to be_present
29 | expect(serialized_article[:title]).to eq "First post"
30 | expect(serialized_article[:slug]).to eq "first-post"
31 | expect(serialized_article[:image][:format]).to eq "png"
32 | expect(serialized_article[:image][:alt]).to eq "My first post"
33 | expect(serialized_article[:image][:title]).to eq "First post"
34 | expect(serialized_article[:image][:size]).to eq 119_271
35 | expect(serialized_article[:image][:height]).to eq 621
36 | expect(serialized_article[:image][:width]).to eq 2553
37 | expect(serialized_article[:image][:url]).to be_present
38 | expect(serialized_article[:image][:video]).to be_nil
39 | expect(serialized_article[:file][:format]).to eq "txt"
40 | expect(serialized_article[:file][:size]).to eq 10
41 | expect(serialized_article[:file][:url]).to be_present
42 | expect(serialized_article[:file][:video]).to be_nil
43 | expect(serialized_article[:content][:value]).to be_an(Hash)
44 | expect(serialized_article[:content][:blocks].first[:title]).to eq "Foo"
45 | expect(serialized_article[:content][:links].first[:name]).to eq "Mark Smith"
46 | end
47 | end
48 |
49 | context "multi language" do
50 | it "returns localized data correctly" do
51 | I18n.with_locale(:it) do
52 | expect(repo.articles.last.title).to eq "Primo post"
53 | expect(repo.articles.last.to_hash[:title]).to eq "Primo post"
54 | end
55 |
56 | I18n.with_locale(:en) do
57 | expect(repo.articles.last.title).to eq "First post"
58 | expect(repo.articles.last.to_hash[:title]).to eq "First post"
59 | end
60 | end
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/dato/upload/file_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Upload
7 | describe File, :vcr do
8 | let(:account_client) do
9 | generate_account_client!
10 | end
11 |
12 | let(:site) do
13 | account_client.sites.create(name: "Test site")
14 | end
15 |
16 | before { site }
17 |
18 | let(:site_client) do
19 | Dato::Site::Client.new(
20 | site[:readwrite_token],
21 | base_url: ENV.fetch("SITE_API_BASE_URL"),
22 | )
23 | end
24 |
25 | subject(:upload) do
26 | described_class.new(site_client, source).upload
27 | end
28 |
29 | context "with a url" do
30 | let(:source) { "https://s3.claudiaraddi.net/slideshows/original/4/Sito2.jpg" }
31 |
32 | it "downloads locally and then uploads the file" do
33 | expect(upload).not_to be_nil
34 | expect(site_client.uploads.find(upload[:upload_id])[:format]).to eq "jpg"
35 | end
36 | context "with a 404 url" do
37 | let(:source) { "https://google.it/NonExistentImage.png" }
38 |
39 | it "raise an exception" do
40 | expect { upload }.to raise_error(Faraday::ResourceNotFound)
41 | end
42 | end
43 | end
44 |
45 | context "with a local file" do
46 | let(:source) { "./spec/fixtures/image.jpg" }
47 |
48 | it "uploads the file" do
49 | expect(upload).not_to be_nil
50 | expect(site_client.uploads.find(upload[:upload_id])[:format]).to eq "jpg"
51 | end
52 |
53 | context "jpg without extension" do
54 | let(:source) { "./spec/fixtures/image" }
55 |
56 | it "uploads the file" do
57 | expect(upload).not_to be_nil
58 | expect(site_client.uploads.find(upload[:upload_id])[:format]).to eq "jpeg"
59 | end
60 | end
61 |
62 | context "gif image" do
63 | let(:source) { "./spec/fixtures/image.gif" }
64 | it "uploads the file" do
65 | expect(upload).not_to be_nil
66 | expect(site_client.uploads.find(upload[:upload_id])[:format]).to eq "gif"
67 | end
68 | end
69 |
70 | context "png image" do
71 | let(:source) { "./spec/fixtures/image.png" }
72 | it "uploads the file" do
73 | expect(upload).not_to be_nil
74 | expect(site_client.uploads.find(upload[:upload_id])[:format]).to eq "png"
75 | end
76 | end
77 |
78 | context "no image file" do
79 | let(:source) { "./spec/fixtures/file.txt" }
80 | it "returns format error" do
81 | expect(upload).not_to be_nil
82 | expect(site_client.uploads.find(upload[:upload_id])[:format]).to eq "txt"
83 | end
84 | end
85 | end
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/lib/dato/dump/ssg_detector.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "toml"
4 | require "json"
5 | require "yaml"
6 |
7 | module Dato
8 | module Dump
9 | class SsgDetector
10 | attr_reader :path
11 |
12 | RUBY = %w[middleman jekyll nanoc].freeze
13 |
14 | NODE = %w[brunch assemble ember-cli hexo metalsmith react-scripts
15 | roots docpad wintersmith gatsby harp grunt gulp].freeze
16 |
17 | PYTHON = %w[mkdocs pelican cactus].freeze
18 |
19 | HUGO = [
20 | {
21 | file: "config.toml",
22 | loader: ->(content) { TOML::Parser.new(content).parsed },
23 | },
24 | {
25 | file: "config.yaml",
26 | loader: ->(content) { YAML.safe_load(content) },
27 | },
28 | {
29 | file: "config.json",
30 | loader: ->(content) { JSON.parse(content) },
31 | },
32 | ].freeze
33 |
34 | def initialize(path)
35 | @path = path
36 | end
37 |
38 | def detect
39 | ruby_generator ||
40 | node_generator ||
41 | python_generator ||
42 | hugo ||
43 | "unknown"
44 | end
45 |
46 | private
47 |
48 | def ruby_generator
49 | gemfile_path = File.join(path, "Gemfile")
50 | return unless File.exist?(gemfile_path)
51 |
52 | gemfile = File.read(gemfile_path)
53 |
54 | RUBY.find do |generator|
55 | gemfile =~ /('#{generator}'|"#{generator}")/
56 | end
57 | end
58 |
59 | def node_generator
60 | package_path = File.join(path, "package.json")
61 | return unless File.exist?(package_path)
62 |
63 | package = JSON.parse(File.read(package_path))
64 |
65 | deps = package.fetch("dependencies", {})
66 | dev_deps = package.fetch("devDependencies", {})
67 | all_deps = deps.merge(dev_deps)
68 |
69 | NODE.find do |generator|
70 | all_deps.key? generator
71 | end
72 | rescue JSON::ParserError
73 | nil
74 | end
75 |
76 | def python_generator
77 | requirements_path = File.join(path, "requirements.txt")
78 | return unless File.exist?(requirements_path)
79 |
80 | requirements = File.read(requirements_path)
81 |
82 | PYTHON.find do |generator|
83 | requirements =~ /^#{generator}(==)?/
84 | end
85 | end
86 |
87 | def hugo
88 | HUGO.any? do |option|
89 | config_path = File.join(path, option[:file])
90 | if File.exist?(config_path)
91 | config = option[:loader].call(File.read(config_path))
92 | config.key? "baseurl"
93 | end
94 | end && "hugo"
95 | rescue JSON::ParserError
96 | nil
97 | end
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/spec/dato/dump/runner_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "front_matter_parser"
5 |
6 | module Dato
7 | module Dump
8 | RSpec.describe Runner, :vcr do
9 | include_context "with a new site"
10 |
11 | subject(:runner) do
12 | described_class.new(config_path, client, false, loader, destination_path)
13 | end
14 |
15 | let(:config_path) { "./spec/fixtures/config.rb" }
16 |
17 | let(:destination_path) do
18 | Dir.mktmpdir
19 | end
20 |
21 | let(:loader) { Dato::Local::Loader.new(client) }
22 |
23 | describe ".run" do
24 | before do
25 | loader.load
26 | runner.run
27 | end
28 |
29 | it "generates directories and files" do
30 | toml_file = TOML.load(File.read(File.join(destination_path, "foobar.toml")))
31 | expect(toml_file["sitename"]).to eq "Integration new test site"
32 |
33 | yaml_file = YAML.safe_load(File.read(File.join(destination_path, "site.yml")))
34 | expect(yaml_file["name"]).to eq "Integration new test site"
35 | expect(yaml_file["locales"]).to eq %w[en it]
36 |
37 | loader = FrontMatterParser::Loader::Yaml.new(allowlist_classes: [Time])
38 |
39 | article_file = FrontMatterParser::Parser.new(:md, loader: loader).call(
40 | File.read(File.join(destination_path, "posts", "first-post.md")),
41 | )
42 | expect(article_file.front_matter["item_type"]).to eq "article"
43 | expect(article_file.front_matter["updated_at"]).to be_present
44 | expect(article_file.front_matter["created_at"]).to be_present
45 | expect(article_file.front_matter["title"]).to eq "First post"
46 | expect(article_file.front_matter["slug"]).to eq "first-post"
47 | expect(article_file.front_matter["image"]["format"]).to eq "png"
48 | expect(article_file.front_matter["image"]["size"]).to eq 119_271
49 | expect(article_file.front_matter["image"]["height"]).to eq 621
50 | expect(article_file.front_matter["image"]["width"]).to eq 2553
51 | expect(article_file.front_matter["image"]["url"]).to be_present
52 | expect(article_file.front_matter["image"]["colors"]).to be_present
53 | expect(article_file.front_matter["image"]["tags"]).to eq []
54 | expect(article_file.front_matter["image"]["smart_tags"]).to include("logo")
55 | expect(article_file.front_matter["image"]["blurhash"]).to be_present
56 | expect(article_file.front_matter["image"]["focal_point"]).to eq("x" => 0.1, "y" => 0.1)
57 | expect(article_file.front_matter["file"]["format"]).to eq "txt"
58 | expect(article_file.front_matter["file"]["size"]).to eq 10
59 | expect(article_file.front_matter["file"]["url"]).to be_present
60 | expect(article_file.front_matter["content"]["value"]).to be_an(Hash)
61 | expect(article_file.front_matter["content"]["blocks"].first["title"]).to eq "Foo"
62 | expect(article_file.front_matter["content"]["links"].first["name"]).to eq "Mark Smith"
63 |
64 | expect(article_file.content).to eq "First post"
65 | end
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/description_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe Description do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | let(:result) { builder.build }
15 | let(:description_value) { result[0][:attributes][:content] }
16 | let(:og_value) { result[1][:attributes][:content] }
17 | let(:card_value) { result[2][:attributes][:content] }
18 |
19 | context "with no fallback seo" do
20 | context "with no item" do
21 | it "returns no tags" do
22 | expect(result).to be_nil
23 | end
24 | end
25 |
26 | context "with item" do
27 | let(:item) { items_repo.articles.first }
28 |
29 | context "no SEO" do
30 | it "returns no tags" do
31 | expect(result).to be_nil
32 | end
33 | end
34 |
35 | context "with SEO" do
36 | let(:seo) do
37 | { description: "SEO description" }
38 | end
39 |
40 | it "returns seo description" do
41 | expect(description_value).to eq("SEO description")
42 | expect(og_value).to eq("SEO description")
43 | expect(card_value).to eq("SEO description")
44 | end
45 | end
46 | end
47 | end
48 |
49 | context "with fallback seo" do
50 | let(:global_seo) do
51 | {
52 | fallback_seo: {
53 | description: "Default description",
54 | },
55 | }
56 | end
57 |
58 | context "with no item" do
59 | it "returns fallback description" do
60 | expect(description_value).to eq("Default description")
61 | expect(og_value).to eq("Default description")
62 | expect(card_value).to eq("Default description")
63 | end
64 | end
65 |
66 | context "with item" do
67 | let(:item) { items_repo.articles.first }
68 |
69 | context "no SEO" do
70 | it "returns fallback description" do
71 | expect(description_value).to eq("Default description")
72 | expect(og_value).to eq("Default description")
73 | expect(card_value).to eq("Default description")
74 | end
75 | end
76 |
77 | context "with SEO" do
78 | let(:seo) do
79 | { description: "SEO description" }
80 | end
81 |
82 | it "returns seo description" do
83 | expect(description_value).to eq("SEO description")
84 | expect(og_value).to eq("SEO description")
85 | expect(card_value).to eq("SEO description")
86 | end
87 | end
88 | end
89 | end
90 | end
91 | end
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.7.16
2 |
3 | Fixes bug when launching `dato dump --watch` together with the `environment` option.
4 |
5 | # 0.7.13
6 |
7 | Add option to pass a project's environment:
8 |
9 | ```ruby
10 | require "dato"
11 | client = Dato::Site::Client.new("YOUR-API-KEY", environment: 'sandbox-foobar')
12 | ```
13 |
14 | # 0.7.12
15 |
16 | Introduces `Dato::Utils::BuildModularBlock` class to help creating modular blocks.
17 |
18 | An example usage can be:
19 |
20 | ```ruby
21 | description = [
22 | Dato::Utils::BuildModularBlock.build(
23 | item_type: "1235",
24 | name: "Best dog in the world",
25 | year: "2020",
26 | picture: picture
27 | )
28 | ]
29 |
30 | record = client.items.create(
31 | item_type: "1234", # model ID
32 | name: "Gigio",
33 | description: description
34 | )
35 | ```
36 |
37 | Find more info [on the documentation](https://www.datocms.com/docs/content-management-api/resources/item/create).
38 |
39 | # 0.7.11 (not released)
40 |
41 | * Updated specs cassettes after `appeareance -> appearance` typo fix
42 | * Some style changes
43 |
44 | # 0.7.10
45 |
46 | * Fixed SEO title retrieval. Now it fallbacks to default SEO title is item title is blank
47 |
48 | # 0.7.9
49 |
50 | * Added new attributes to uploads
51 |
52 | # 0.7.0
53 |
54 | * Real-time events are now much more granular and the gem avoids downloading all the content every time a change occurs
55 |
56 | # 0.6.18
57 |
58 | * Fixed regression where you could no longer access items' item type
59 |
60 | # 0.6.15
61 |
62 | * Handle `429 Too Many Requests` responses from API
63 |
64 | # 0.6.12
65 |
66 | * Allow empty responses from server
67 |
68 | # 0.6.10
69 |
70 | * Introduced `.meta` on `Dato::Local::Item` to fetch all meta information about the records
71 |
72 | # 0.6.5
73 |
74 | * The `.seo_meta_tags` method now generates fallback titles based on the model field title
75 |
76 | # v0.6.2
77 |
78 | Moved `json_schema` as runtime dependency
79 |
80 | # v0.6.1
81 |
82 | The big change is that the methods the client makes available are generated at runtime based on the [JSON Schema of our CMA](https://www.datocms.com/content-management-api/). This means any new API endpoint — or changes to existing ones — will instantly be reflected to the client, without the need to upgrade to the latest client version.
83 |
84 | We also added a new `deserialize_response` option to every call, that you can use if you want to retrieve the exact payload the DatoCMS returns:
85 |
86 | ```ruby
87 | require "dato"
88 | client = Dato::Site::Client.new("YOUR-API-KEY")
89 |
90 | # `deserialize_response` is true by default:
91 | access_token = client.access_tokens.create(name: "New token", role: "34")
92 |
93 | # {
94 | # "id" => "312",
95 | # "hardcoded_type" => nil,
96 | # "name" => "New token",
97 | # "token" => "XXXX",
98 | # "role" => "34"
99 | # }
100 |
101 | # if `deserialize_response` is false, this will be the result
102 | access_token = client.access_tokens.create({ name: "New token", role: "34" }, deserialize_response: false)
103 |
104 | # {
105 | # "data": {
106 | # "type": "access_token",
107 | # "id": "312",
108 | # "attributes": {
109 | # "name": "New token",
110 | # "token": "XXXX",
111 | # "hardcoded_type": nil
112 | # },
113 | # "relationships": {
114 | # "role": {
115 | # "data": {
116 | # "type": "role",
117 | # "id": "34"
118 | # }
119 | # }
120 | # }
121 | # }
122 | # }
123 | ```
124 |
125 | In our doc pages we also added some examples for the super-handy `all_pages` option which was already present since v0.3.29:
126 |
127 | ```ruby
128 | # if you want to fetch all the pages with just one call:
129 | client.items.all({ "filter[type]" => "44" }, all_pages: true)
130 | ```
131 |
--------------------------------------------------------------------------------
/spec/dato/local/json_api_entity_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | RSpec.describe JsonApiEntity do
8 | subject(:object) { described_class.new(payload, data_source) }
9 | let(:payload) do
10 | {
11 | id: "peter",
12 | type: "person",
13 | attributes: {
14 | first_name: "Peter",
15 | last_name: "Griffin",
16 | },
17 | relationships: {
18 | children: {
19 | data: [
20 | { type: "person", id: "stewie" },
21 | ],
22 | },
23 | mother: {
24 | data: { type: "person", id: "thelma" },
25 | },
26 | father: {
27 | data: nil,
28 | },
29 | },
30 | }
31 | end
32 |
33 | let(:data_source) do
34 | instance_double("Dato::Local::DataSource")
35 | end
36 |
37 | describe "#id" do
38 | it "returns the object ID" do
39 | expect(object.id).to eq "peter"
40 | end
41 | end
42 |
43 | describe "#type" do
44 | it "returns the object type" do
45 | expect(object.type).to eq "person"
46 | end
47 | end
48 |
49 | describe "attributes" do
50 | it "returns the attribute if it exists" do
51 | expect(object.respond_to?(:first_name)).to be_truthy
52 | expect(object.first_name).to eq "Peter"
53 | end
54 |
55 | it "returns NoMethodError if it doesnt" do
56 | expect(object.respond_to?(:foo_bar)).to be_falsy
57 | expect { object.foo_bar }.to raise_error NoMethodError
58 | end
59 | end
60 |
61 | describe "[]" do
62 | it "returns the attribute if it exists" do
63 | expect(object[:first_name]).to eq "Peter"
64 | end
65 |
66 | it "returns nil if it doesnt" do
67 | expect(object[:foo_bar]).to be_nil
68 | end
69 | end
70 |
71 | describe "==" do
72 | context "same id and type" do
73 | let(:other) do
74 | described_class.new({ id: "peter", type: "person" }, data_source)
75 | end
76 |
77 | it "returns true" do
78 | expect(object == other).to be_truthy
79 | end
80 | end
81 |
82 | context "different id and type" do
83 | let(:other) do
84 | described_class.new({ id: "stewie", type: "person" }, data_source)
85 | end
86 |
87 | it "returns false" do
88 | expect(object == other).to be_falsy
89 | end
90 | end
91 |
92 | context "different class" do
93 | let(:other) do
94 | 12
95 | end
96 |
97 | it "returns false" do
98 | expect(object == other).to be_falsy
99 | end
100 | end
101 | end
102 |
103 | describe "links" do
104 | let(:stewie) { instance_double("Dato::Local::JsonApiObject") }
105 | let(:thelma) { instance_double("Dato::Local::JsonApiObject") }
106 |
107 | before do
108 | allow(data_source).to receive(:find_entity).with("person", "stewie") { stewie }
109 | allow(data_source).to receive(:find_entity).with("person", "thelma") { thelma }
110 | end
111 |
112 | context "multiple linkages" do
113 | it "returns the array of JsonApiObjects" do
114 | expect(object.children).to eq [stewie]
115 | end
116 | end
117 |
118 | context "single linkage" do
119 | it "returns the JsonApiObject" do
120 | expect(object.mother).to eq thelma
121 | expect(object.father).to eq nil
122 | end
123 | end
124 |
125 | it "returns NoMethodError if it does not exist" do
126 | expect { object.xxx }.to raise_error NoMethodError
127 | end
128 | end
129 | end
130 | end
131 | end
132 |
--------------------------------------------------------------------------------
/spec/dato/utils/favicon_tags_builder_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | describe FaviconTagsBuilder do
8 | include_context "items repo"
9 |
10 | subject(:builder) { described_class.new(site, "#ff0000") }
11 |
12 | describe "#meta_tags" do
13 | context "with no favicon" do
14 | let(:favicon) { nil }
15 |
16 | it "returns an array of tags" do
17 | expect(builder.meta_tags).to eq [
18 | { tag_name: "meta", attributes: { name: "theme-color", content: "#ff0000" } },
19 | { tag_name: "meta", attributes: { name: "msapplication-TileColor", content: "#ff0000" } },
20 | { tag_name: "meta", attributes: { name: "application-name", content: "XXX" } },
21 | ]
22 | end
23 | end
24 |
25 | context "with favicon" do
26 | let(:favicon) { "666" }
27 |
28 | it "returns an array of tags" do
29 | expect(builder.meta_tags).to eq [
30 | { tag_name: "link", attributes: { sizes: "16x16", type: "image/png", rel: "icon", href: "https://www.datocms-assets.com/seo.png?w=16&h=16" } },
31 | { tag_name: "link", attributes: { sizes: "32x32", type: "image/png", rel: "icon", href: "https://www.datocms-assets.com/seo.png?w=32&h=32" } },
32 | { tag_name: "link", attributes: { sizes: "96x96", type: "image/png", rel: "icon", href: "https://www.datocms-assets.com/seo.png?w=96&h=96" } },
33 | { tag_name: "link", attributes: { sizes: "192x192", type: "image/png", rel: "icon", href: "https://www.datocms-assets.com/seo.png?w=192&h=192" } },
34 | { tag_name: "link", attributes: { sizes: "57x57", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=57&h=57" } },
35 | { tag_name: "link", attributes: { sizes: "60x60", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=60&h=60" } },
36 | { tag_name: "link", attributes: { sizes: "72x72", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=72&h=72" } },
37 | { tag_name: "link", attributes: { sizes: "76x76", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=76&h=76" } },
38 | { tag_name: "link", attributes: { sizes: "114x114", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=114&h=114" } },
39 | { tag_name: "link", attributes: { sizes: "120x120", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=120&h=120" } },
40 | { tag_name: "link", attributes: { sizes: "144x144", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=144&h=144" } },
41 | { tag_name: "link", attributes: { sizes: "152x152", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=152&h=152" } },
42 | { tag_name: "link", attributes: { sizes: "180x180", rel: "apple-touch-icon", href: "https://www.datocms-assets.com/seo.png?w=180&h=180" } },
43 | { tag_name: "meta", attributes: { name: "msapplication-square70x70logo", content: "https://www.datocms-assets.com/seo.png?w=70&h=70" } },
44 | { tag_name: "meta", attributes: { name: "msapplication-square150x150logo", content: "https://www.datocms-assets.com/seo.png?w=150&h=150" } },
45 | { tag_name: "meta", attributes: { name: "msapplication-square310x310logo", content: "https://www.datocms-assets.com/seo.png?w=310&h=310" } },
46 | { tag_name: "meta", attributes: { name: "msapplication-square310x150logo", content: "https://www.datocms-assets.com/seo.png?w=310&h=150" } },
47 | { tag_name: "meta", attributes: { name: "theme-color", content: "#ff0000" } },
48 | { tag_name: "meta", attributes: { name: "msapplication-TileColor", content: "#ff0000" } },
49 | { tag_name: "meta", attributes: { name: "application-name", content: "XXX" } },
50 | ]
51 | end
52 | end
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/dato/repo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/json_api_serializer"
4 | require "dato/json_api_deserializer"
5 | require "dato/paginator"
6 |
7 | module Dato
8 | class Repo
9 | attr_reader :client, :type, :schema
10 |
11 | IDENTITY_REGEXP = /\{\(.*?definitions%2F(.*?)%2Fdefinitions%2Fidentity\)}/.freeze
12 |
13 | METHOD_NAMES = {
14 | "instances" => :all,
15 | "self" => :find,
16 | }.freeze
17 |
18 | def initialize(client, type, schema)
19 | @client = client
20 | @type = type
21 | @schema = schema
22 | end
23 |
24 | def respond_to_missing?(method, include_private = false)
25 | respond_to_missing = schema.links.any? do |link|
26 | METHOD_NAMES.fetch(link.rel, link.rel).to_sym == method.to_sym
27 | end
28 |
29 | respond_to_missing || super
30 | end
31 |
32 | private
33 |
34 | def method_missing(method, *args, &block)
35 | link = schema.links.find do |ilink|
36 | METHOD_NAMES.fetch(ilink.rel, ilink.rel).to_sym == method.to_sym
37 | end
38 |
39 | return super unless link
40 |
41 | min_arguments_count = [
42 | link.href.scan(IDENTITY_REGEXP).size,
43 | link.schema && link.method != :get ? 1 : 0,
44 | ].reduce(0, :+)
45 |
46 | (args.size >= min_arguments_count) ||
47 | raise(ArgumentError, "wrong number of arguments (given #{args.size}, expected #{min_arguments_count})")
48 |
49 | placeholders = []
50 |
51 | url = link["href"].gsub(IDENTITY_REGEXP) do |_stuff|
52 | placeholder = args.shift.to_s
53 | placeholders << placeholder
54 | placeholder
55 | end
56 |
57 | body = nil
58 | query_string = nil
59 |
60 | if %i[post put].include?(link.method)
61 | body = link.schema ? args.shift : {}
62 | query_string = args.shift || {}
63 |
64 | elsif %i[get delete].include?(link.method)
65 | query_string = args.shift || {}
66 | end
67 |
68 | options = args.any? ? args.shift.symbolize_keys : {}
69 |
70 | if link.schema && %i[post put].include?(link.method) && options.fetch(:serialize_response, true)
71 | body = JsonApiSerializer.new(link: link).serialize(
72 | body,
73 | link.method == :post ? nil : placeholders.last,
74 | )
75 | end
76 |
77 | response = if %i[post put].include?(link.method)
78 | client.send(link.method, url, body, query_string)
79 | elsif link.method == :delete
80 | client.delete(url, query_string)
81 | elsif link.method == :get
82 | if options.fetch(:all_pages, false)
83 | Paginator.new(client, url, query_string).response
84 | else
85 | client.get(url, query_string)
86 | end
87 | end
88 |
89 | if response && response[:data] && response[:data].is_a?(Hash) && response[:data][:type] == "job"
90 | job_result = nil
91 |
92 | until job_result
93 | begin
94 | sleep(1)
95 | job_result = client.job_result.find(response[:data][:id])
96 | rescue ApiError => e
97 | raise e if e.response[:status] != 404
98 | end
99 | end
100 |
101 | if job_result[:status] < 200 || job_result[:status] >= 300
102 | error = ApiError.new(
103 | status: job_result[:status],
104 | body: JSON.dump(job_result[:payload]),
105 | )
106 |
107 | puts "===="
108 | puts error.message
109 | puts "===="
110 |
111 | raise error
112 | end
113 |
114 | if options.fetch(:deserialize_response, true)
115 | JsonApiDeserializer.new(link.job_schema).deserialize(job_result[:payload])
116 | else
117 | job_result.payload
118 | end
119 | elsif options.fetch(:deserialize_response, true)
120 | JsonApiDeserializer.new(link.target_schema).deserialize(response)
121 | else
122 | response
123 | end
124 | end
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/lib/dato/json_api_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/json_schema_relationships"
4 | require "dato/json_schema_type"
5 |
6 | module Dato
7 | class JsonApiSerializer
8 | attr_reader :link, :type
9 |
10 | def initialize(type: nil, link: nil)
11 | @link = link
12 | @type = type || type_from_schema
13 | end
14 |
15 | def serialize(resource, id = nil)
16 | resource = resource.with_indifferent_access
17 | data = {}
18 |
19 | data[:id] = id || resource[:id] if id || resource[:id]
20 |
21 | resource.delete(:meta) if resource.key?(:meta)
22 |
23 | data[:type] = type
24 |
25 | if link.schema &&
26 | link.schema.properties["data"] &&
27 | link.schema.properties["data"].properties.keys.include?("attributes")
28 |
29 | serialized_resource_attributes = serialized_attributes(resource)
30 | data[:attributes] = serialized_resource_attributes
31 | end
32 |
33 | serialized_relationships = serialized_relationships(resource)
34 |
35 | data[:relationships] = serialized_relationships if serialized_relationships
36 |
37 | { data: data }
38 | end
39 |
40 | def serialized_attributes(resource)
41 | result = {}
42 |
43 | attributes(resource).each do |attribute|
44 | if resource.key? attribute
45 | result[attribute] = resource[attribute]
46 | elsif required_attributes.include? attribute
47 | throw "Required attribute: #{attribute}"
48 | end
49 | end
50 |
51 | result
52 | end
53 |
54 | def serialized_relationships(resource)
55 | result = {}
56 |
57 | relationships.each do |relationship, meta|
58 | if resource.key? relationship
59 | value = resource[relationship]
60 |
61 | data = if value
62 | if meta[:types].length > 1
63 | if meta[:collection]
64 | value.map(&:symbolize_keys)
65 | else
66 | value.symbolize_keys
67 | end
68 | else
69 | meta_type = meta[:types].first
70 | if meta[:collection]
71 | value.map do |id|
72 | {
73 | type: meta_type,
74 | id: id.to_s,
75 | }
76 | end
77 | else
78 | {
79 | type: meta_type,
80 | id: value.to_s,
81 | }
82 | end
83 | end
84 | end
85 |
86 | result[relationship] = { data: data }
87 | elsif required_relationships.include?(relationship)
88 | throw "Required attribute: #{relationship}"
89 | end
90 | end
91 |
92 | result.empty? ? nil : result
93 | end
94 |
95 | def attributes(resource)
96 | if type == "item"
97 | return resource.keys.reject do |key|
98 | %i[
99 | item_type
100 | id
101 | created_at
102 | updated_at
103 | creator
104 | ].include?(key.to_sym)
105 | end
106 | end
107 |
108 | link_attributes["properties"].keys.map(&:to_sym)
109 | end
110 |
111 | def required_attributes
112 | return [] if type == "item"
113 |
114 | (link_attributes.required || []).map(&:to_sym)
115 | end
116 |
117 | def relationships
118 | @relationships ||= JsonSchemaRelationships.new(link.schema).relationships
119 | end
120 |
121 | def required_relationships
122 | if link.schema.properties["data"].required.include?("relationships")
123 | (link_relationships.required || []).map(&:to_sym)
124 | else
125 | []
126 | end
127 | end
128 |
129 | def link_attributes
130 | link.schema.properties["data"].properties["attributes"]
131 | end
132 |
133 | def link_relationships
134 | link.schema.properties["data"].properties["relationships"]
135 | end
136 |
137 | def type_from_schema
138 | Dato::JsonSchemaType.new(link.schema).call
139 | end
140 | end
141 | end
142 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/image_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe Image do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | let(:result) { builder.build }
15 | let(:og_value) { result[0][:attributes][:content] }
16 | let(:card_value) { result[1][:attributes][:content] }
17 |
18 | context "with no fallback seo" do
19 | context "with no item" do
20 | it "returns no tags" do
21 | expect(result).to be_nil
22 | end
23 | end
24 |
25 | context "with item" do
26 | let(:item) { items_repo.articles.first }
27 |
28 | context "with no image" do
29 | context "no SEO" do
30 | it "returns no tags" do
31 | expect(result).to be_nil
32 | end
33 | end
34 |
35 | context "with SEO" do
36 | let(:seo) do
37 | {
38 | image: "666",
39 | }
40 | end
41 |
42 | it "returns seo image" do
43 | expect(og_value).to include("seo.png")
44 | expect(card_value).to include("seo.png")
45 | end
46 | end
47 | end
48 |
49 | context "with image" do
50 | let(:item_image) { "111" }
51 |
52 | context "no SEO" do
53 | it "returns item image" do
54 | expect(og_value).to include("image.png")
55 | expect(card_value).to include("image.png")
56 | end
57 | end
58 |
59 | context "with SEO" do
60 | let(:seo) do
61 | {
62 | image: "666",
63 | }
64 | end
65 |
66 | it "returns SEO image" do
67 | expect(og_value).to include("seo.png")
68 | expect(card_value).to include("seo.png")
69 | end
70 | end
71 | end
72 | end
73 |
74 | context "with fallback seo" do
75 | let(:global_seo) do
76 | {
77 | fallback_seo: {
78 | image: "999",
79 | },
80 | }
81 | end
82 |
83 | context "with no item" do
84 | it "returns fallback image" do
85 | expect(og_value).to include("fallback_seo.png")
86 | expect(card_value).to include("fallback_seo.png")
87 | end
88 | end
89 |
90 | context "with item" do
91 | let(:item) { items_repo.articles.first }
92 |
93 | context "with no image" do
94 | context "no SEO" do
95 | it "returns fallback image" do
96 | expect(og_value).to include("fallback_seo.png")
97 | expect(card_value).to include("fallback_seo.png")
98 | end
99 | end
100 |
101 | context "with SEO" do
102 | let(:seo) do
103 | {
104 | image: "666",
105 | }
106 | end
107 |
108 | it "returns seo image" do
109 | expect(og_value).to include("seo.png")
110 | expect(card_value).to include("seo.png")
111 | end
112 | end
113 | end
114 |
115 | context "with image" do
116 | let(:item_image) { "111" }
117 |
118 | context "no SEO" do
119 | it "returns item image" do
120 | expect(og_value).to include("image.png")
121 | expect(card_value).to include("image.png")
122 | end
123 | end
124 |
125 | context "with SEO" do
126 | let(:seo) do
127 | {
128 | image: "666",
129 | }
130 | end
131 |
132 | it "returns SEO image" do
133 | expect(og_value).to include("seo.png")
134 | expect(card_value).to include("seo.png")
135 | end
136 | end
137 | end
138 | end
139 | end
140 | end
141 | end
142 | end
143 | end
144 | end
145 | end
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 👉 [Visit the DatoCMS homepage](https://www.datocms.com) or see [What is DatoCMS?](#what-is-datocms)
5 |
6 |
7 | # DatoCMS Ruby Client
8 |
9 | [](https://coveralls.io/github/datocms/ruby-datocms-client?branch=master) [](https://travis-ci.org/datocms/ruby-datocms-client) [](https://badge.fury.io/rb/dato)
10 |
11 | CLI tool for DatoCMS (https://www.datocms.com).
12 |
13 | ## How to integrate DatoCMS with Jekyll
14 |
15 | Please head over the [Jekyll section of our documentation](https://www.datocms.com/docs/jekyll/) to learn everything you need to get started.
16 |
17 | ## How to integrate DatoCMS with Middleman
18 |
19 | For Middleman we have created a nice Middleman extension called [middleman-dato](https://github.com/datocms/middleman-dato). Please visit the [Middleman section of our documentation](https://docs.datocms.com/middleman/overview.html) to learn everything you need to get started.
20 |
21 | ## API Client
22 |
23 | This gem also exposes an API client, useful ie. to import existing content in your DatoCMS administrative area. Read our [documentation](https://www.datocms.com/content-management-api/) for detailed info.
24 |
25 | ## Development
26 |
27 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28 |
29 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30 |
31 | ## Contributing
32 |
33 | Bug reports and pull requests are welcome on GitHub at https://github.com/datocms/ruby-datocms-client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
34 |
35 | ## License
36 |
37 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
38 |
39 |
40 |
41 | -----------------
42 | # What is DatoCMS?
43 |
44 |
45 | [DatoCMS](https://www.datocms.com/) is the REST & GraphQL Headless CMS for the modern web.
46 |
47 | Trusted by over 25,000 enterprise businesses, agency partners, and individuals across the world, DatoCMS users create online content at scale from a central hub and distribute it via API. We ❤️ our [developers](https://www.datocms.com/team/best-cms-for-developers), [content editors](https://www.datocms.com/team/content-creators) and [marketers](https://www.datocms.com/team/cms-digital-marketing)!
48 |
49 | **Quick links:**
50 |
51 | - ⚡️ Get started with a [free DatoCMS account](https://dashboard.datocms.com/signup)
52 | - 🔖 Go through the [docs](https://www.datocms.com/docs)
53 | - ⚙️ Get [support from us and the community](https://community.datocms.com/)
54 | - 🆕 Stay up to date on new features and fixes on the [changelog](https://www.datocms.com/product-updates)
55 |
56 | **Our featured repos:**
57 | - [datocms/react-datocms](https://github.com/datocms/react-datocms): React helper components for images, Structured Text rendering, and more
58 | - [datocms/js-rest-api-clients](https://github.com/datocms/js-rest-api-clients): Node and browser JavaScript clients for updating and administering your content. For frontend fetches, we recommend using our [GraphQL Content Delivery API](https://www.datocms.com/docs/content-delivery-api) instead.
59 | - [datocms/cli](https://github.com/datocms/cli): Command-line interface that includes our [Contentful importer](https://github.com/datocms/cli/tree/main/packages/cli-plugin-contentful) and [Wordpress importer](https://github.com/datocms/cli/tree/main/packages/cli-plugin-wordpress)
60 | - [datocms/plugins](https://github.com/datocms/plugins): Example plugins we've made that extend the editor/admin dashboard
61 | - [datocms/gatsby-source-datocms](https://github.com/datocms/gatsby-source-datocms): Our Gatsby source plugin to pull data from DatoCMS
62 | - Frontend examples in different frameworks: [Next.js](https://github.com/datocms/nextjs-demo), [Vue](https://github.com/datocms/vue-datocms) and [Nuxt](https://github.com/datocms/nuxtjs-demo), [Svelte](https://github.com/datocms/datocms-svelte) and [SvelteKit](https://github.com/datocms/sveltekit-demo), [Astro](https://github.com/datocms/datocms-astro-blog-demo), [Remix](https://github.com/datocms/remix-example). See [all our starter templates](https://www.datocms.com/marketplace/starters).
63 |
64 | Or see [all our public repos](https://github.com/orgs/datocms/repositories?q=&type=public&language=&sort=stargazers)
65 |
66 |
--------------------------------------------------------------------------------
/lib/dato/local/item.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "forwardable"
4 | require "dato/utils/locale_value"
5 |
6 | Dir["#{File.dirname(__FILE__)}/field_type/*.rb"].sort.each do |file|
7 | require file
8 | end
9 |
10 | module Dato
11 | module Local
12 | class Item
13 | extend Forwardable
14 |
15 | attr_reader :entity
16 |
17 | def_delegators :entity, :id, :meta
18 |
19 | def initialize(entity, items_repo)
20 | @entity = entity
21 | @items_repo = items_repo
22 | end
23 |
24 | def ==(other)
25 | other.is_a?(Item) && other.id == id
26 | end
27 |
28 | def seo_meta_tags
29 | Utils::SeoTagsBuilder.new(self, @items_repo.site).meta_tags
30 | end
31 |
32 | def singleton?
33 | item_type.singleton
34 | end
35 | alias single_instance? singleton?
36 |
37 | def item_type
38 | @item_type ||= entity.item_type
39 | end
40 |
41 | def fields
42 | @fields ||= item_type.fields.sort_by(&:position)
43 | end
44 |
45 | def attributes
46 | fields.each_with_object(
47 | ActiveSupport::HashWithIndifferentAccess.new,
48 | ) do |field, acc|
49 | acc[field.api_key.to_sym] = read_attribute(field.api_key, field)
50 | end
51 | end
52 |
53 | def position
54 | entity.position
55 | end
56 |
57 | def parent
58 | @items_repo.find(entity.parent_id) if item_type.tree && entity.parent_id
59 | end
60 |
61 | def children
62 | @items_repo.children_of(id).sort_by(&:position) if item_type.tree
63 | end
64 |
65 | def updated_at
66 | Time.parse(meta.updated_at).utc
67 | end
68 |
69 | def created_at
70 | Time.parse(meta.created_at).utc
71 | end
72 |
73 | def to_s
74 | api_key = item_type.api_key
75 | "#- "
76 | end
77 | alias inspect to_s
78 |
79 | def [](key)
80 | attributes[key.to_sym]
81 | end
82 |
83 | def to_hash(max_depth = 3, current_depth = 0)
84 | return id if current_depth >= max_depth
85 |
86 | base = {
87 | id: id,
88 | item_type: item_type.api_key,
89 | updated_at: updated_at,
90 | created_at: created_at,
91 | }
92 |
93 | base[:position] = position if item_type.sortable
94 |
95 | if item_type.tree
96 | base[:position] = position
97 | base[:children] = children.map do |child|
98 | child.to_hash(
99 | max_depth,
100 | current_depth + 1,
101 | )
102 | end
103 | end
104 |
105 | fields.each_with_object(base) do |field, result|
106 | value = send(field.api_key)
107 |
108 | result[field.api_key.to_sym] = if value.respond_to?(:to_hash)
109 | value.to_hash(
110 | max_depth,
111 | current_depth + 1,
112 | )
113 | else
114 | value
115 | end
116 | end
117 | end
118 |
119 | private
120 |
121 | def read_attribute(method, field)
122 | field_type = field.field_type
123 | type_klass_name = "::Dato::Local::FieldType::#{field_type.camelize}"
124 | type_klass = type_klass_name.safe_constantize
125 |
126 | value = if field.localized
127 | obj = entity[method] || {}
128 | Utils::LocaleValue.find(obj)
129 | else
130 | entity[method]
131 | end
132 |
133 | if type_klass
134 | type_klass.parse(value, @items_repo)
135 | else
136 | warning = [
137 | "Warning: unrecognized field of type `#{field_type}`",
138 | "for item `#{item_type.api_key}` and",
139 | "field `#{method}`: returning a simple Hash instead.",
140 | "Please upgrade to the latest version of the `dato` gem!",
141 | ]
142 | puts warning.join(" ")
143 |
144 | value
145 | end
146 | end
147 |
148 | def method_missing(method, *arguments, &block)
149 | field = fields.find { |f| f.api_key.to_sym == method }
150 | if field && arguments.empty?
151 | read_attribute(method, field)
152 | else
153 | super
154 | end
155 | rescue NoMethodError => e
156 | if e.name == method
157 | message = []
158 | message << "Undefined method `#{method}`"
159 | message << "Available fields for a `#{item_type.api_key}` item:"
160 | message += fields.map do |f|
161 | "* .#{f.api_key}"
162 | end
163 | raise NoMethodError, message.join("\n")
164 | else
165 | raise e
166 | end
167 | end
168 |
169 | def respond_to_missing?(method, include_private = false)
170 | field = fields.find { |f| f.api_key.to_sym == method }
171 | if field
172 | true
173 | else
174 | super
175 | end
176 | end
177 | end
178 | end
179 | end
180 |
--------------------------------------------------------------------------------
/lib/dato/api_client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "faraday"
4 | require "faraday_middleware"
5 | require "json"
6 | require "json_schema"
7 |
8 | require "dato/version"
9 | require "dato/repo"
10 |
11 | require "dato/api_error"
12 |
13 | require "cacert"
14 |
15 | module Dato
16 | module ApiClient
17 | def self.included(base)
18 | base.extend ClassMethods
19 |
20 | base.class_eval do
21 | attr_reader :token, :environment, :base_url, :schema, :extra_headers
22 | end
23 | end
24 |
25 | module ClassMethods
26 | def json_schema(subdomain)
27 | define_method(:initialize) do |token, options = {}|
28 | @token = token
29 | @base_url = options[:base_url] || "https://#{subdomain}.datocms.com"
30 | @environment = options[:environment]
31 | @extra_headers = options[:extra_headers] || {}
32 | end
33 |
34 | define_singleton_method(:subdomain) do
35 | subdomain
36 | end
37 | end
38 | end
39 |
40 | def respond_to_missing?(method, include_private = false)
41 | json_schema.definitions.each do |type, obj|
42 | is_collection = obj.links.select { |x| x.rel == "instances" }.any?
43 | namespace = is_collection ? type.pluralize : type
44 | return true if method.to_s == namespace
45 | end
46 |
47 | super
48 | end
49 |
50 | def method_missing(method, *args, &block)
51 | json_schema.definitions.each do |type, obj|
52 | is_collection = obj.links.select { |x| x.rel == "instances" }.any?
53 | namespace = is_collection ? type.pluralize : type
54 |
55 | next unless method.to_s == namespace
56 |
57 | instance_variable_set(
58 | "@#{namespace}",
59 | instance_variable_get("@#{namespace}") ||
60 | Dato::Repo.new(self, type, obj),
61 | )
62 |
63 | return instance_variable_get("@#{namespace}")
64 | end
65 |
66 | super
67 | end
68 |
69 | def json_schema
70 | @json_schema ||= begin
71 | response = Faraday.get(
72 | # "http://#{subdomain}.lvh.me:3001/docs/#{subdomain}-hyperschema.json"
73 | "#{base_url}/docs/#{self.class.subdomain}-hyperschema.json",
74 | )
75 |
76 | schema = JsonSchema.parse!(JSON.parse(response.body))
77 | schema.expand_references!
78 |
79 | schema
80 | end
81 | end
82 |
83 | def put(absolute_path, body = {}, params = {})
84 | request(:put, absolute_path, body, params)
85 | end
86 |
87 | def post(absolute_path, body = {}, params = {})
88 | request(:post, absolute_path, body, params)
89 | end
90 |
91 | def get(absolute_path, params = {})
92 | request(:get, absolute_path, nil, params)
93 | end
94 |
95 | def delete(absolute_path, params = {})
96 | request(:delete, absolute_path, nil, params)
97 | end
98 |
99 | def request(*args)
100 | method, absolute_path, body, params = args
101 |
102 | response = connection.send(method, absolute_path, body) do |c|
103 | c.params = params if params
104 | end
105 |
106 | response.body.with_indifferent_access if response.body.is_a?(Hash)
107 | rescue Faraday::SSLError => e
108 | raise e if ENV["SSL_CERT_FILE"] == Cacert.pem
109 |
110 | Cacert.set_in_env
111 | request(*args)
112 | rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
113 | puts e.message
114 | raise e
115 | rescue Faraday::ClientError => e
116 | if e.response[:status] == 429
117 | to_wait = e.response[:headers]["x-ratelimit-reset"].to_i
118 | puts "Rate limit exceeded, waiting #{to_wait} seconds..."
119 | sleep(to_wait + 1)
120 | request(*args)
121 | elsif e.response[:status] == 422 && batch_data_validation?(e.response)
122 | puts "Validating items, waiting 1 second and retrying..."
123 | sleep(1)
124 | request(*args)
125 | else
126 | # puts body.inspect
127 | # puts '===='
128 | # puts error.message
129 | # puts '===='
130 | error = ApiError.new(e.response)
131 | raise error
132 | end
133 | end
134 |
135 | private
136 |
137 | def batch_data_validation?(response)
138 | body = begin
139 | JSON.parse(response[:body])
140 | rescue JSON::ParserError
141 | nil
142 | end
143 |
144 | return false unless body
145 | return false unless body["data"]
146 |
147 | body["data"].any? do |e|
148 | e["attributes"]["code"] == "BATCH_DATA_VALIDATION_IN_PROGRESS"
149 | end
150 | rescue StandardError
151 | false
152 | end
153 |
154 | def connection
155 | default_headers = {
156 | "Accept" => "application/json",
157 | "Content-Type" => "application/json",
158 | "Authorization" => "Bearer #{@token}",
159 | "User-Agent" => "ruby-client v#{Dato::VERSION}",
160 | "X-Api-Version" => "3",
161 | }
162 |
163 | default_headers.merge!("X-Environment" => environment) if environment
164 |
165 | options = {
166 | url: base_url,
167 | headers: default_headers.merge(extra_headers),
168 | }
169 |
170 | @connection ||= Faraday.new(options) do |c|
171 | c.request :json
172 | c.response :json, content_type: /\bjson$/
173 | c.response :raise_error
174 | c.use FaradayMiddleware::FollowRedirects
175 | c.adapter :net_http
176 | end
177 | end
178 | end
179 | end
180 |
--------------------------------------------------------------------------------
/spec/dato/utils/meta_tags/title_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Utils
7 | module MetaTags
8 | describe Title do
9 | include_context "items repo"
10 |
11 | subject(:builder) { described_class.new(item, site) }
12 |
13 | describe "#build" do
14 | let(:result) { builder.build }
15 | let(:title_tag) { result[0][:content] }
16 | let(:og_value) { result[1][:attributes][:content] }
17 | let(:card_value) { result[2][:attributes][:content] }
18 |
19 | context "with no fallback seo" do
20 | context "with no item" do
21 | it "returns no tags" do
22 | expect(result).to be_nil
23 | end
24 | end
25 |
26 | context "with item" do
27 | let(:item) { items_repo.articles.first }
28 |
29 | context "with no title" do
30 | context "no SEO" do
31 | it "returns no tags" do
32 | expect(result).to be_nil
33 | end
34 | end
35 |
36 | context "with SEO" do
37 | let(:seo) do
38 | { title: "SEO title" }
39 | end
40 |
41 | it "returns seo title" do
42 | expect(title_tag).to eq("SEO title")
43 | expect(og_value).to eq("SEO title")
44 | expect(card_value).to eq("SEO title")
45 | end
46 | end
47 | end
48 |
49 | context "with title" do
50 | let(:item_title) { "My title" }
51 |
52 | context "no SEO" do
53 | it "returns item title" do
54 | expect(title_tag).to eq("My title")
55 | expect(og_value).to eq("My title")
56 | expect(card_value).to eq("My title")
57 | end
58 | end
59 |
60 | context "with SEO" do
61 | let(:seo) do
62 | { title: "SEO title" }
63 | end
64 |
65 | it "returns SEO title" do
66 | expect(title_tag).to eq("SEO title")
67 | expect(og_value).to eq("SEO title")
68 | expect(card_value).to eq("SEO title")
69 | end
70 | end
71 | end
72 | end
73 | end
74 |
75 | context "with fallback seo" do
76 | let(:global_seo) do
77 | {
78 | title_suffix: title_suffix,
79 | fallback_seo: {
80 | title: "Default title",
81 | },
82 | }
83 | end
84 |
85 | context "with no item" do
86 | context "with title suffix" do
87 | let(:title_suffix) { " - My site" }
88 |
89 | it "returns fallback title" do
90 | expect(title_tag).to eq("Default title - My site")
91 | expect(og_value).to eq("Default title")
92 | expect(card_value).to eq("Default title")
93 | end
94 | end
95 |
96 | context "without title suffix" do
97 | it "returns fallback title" do
98 | expect(title_tag).to eq("Default title")
99 | expect(og_value).to eq("Default title")
100 | expect(card_value).to eq("Default title")
101 | end
102 | end
103 | end
104 |
105 | context "with item" do
106 | let(:item) { items_repo.articles.first }
107 |
108 | context "with no title" do
109 | context "no SEO" do
110 | it "returns fallback title" do
111 | expect(title_tag).to eq("Default title")
112 | expect(og_value).to eq("Default title")
113 | expect(card_value).to eq("Default title")
114 | end
115 | end
116 |
117 | context "with SEO" do
118 | let(:seo) do
119 | { title: "SEO title" }
120 | end
121 |
122 | it "returns seo title" do
123 | expect(title_tag).to eq("SEO title")
124 | expect(og_value).to eq("SEO title")
125 | expect(card_value).to eq("SEO title")
126 | end
127 | end
128 | end
129 |
130 | context "with title" do
131 | let(:item_title) { "My title" }
132 |
133 | context "no SEO" do
134 | it "returns item title" do
135 | expect(title_tag).to eq("My title")
136 | expect(og_value).to eq("My title")
137 | expect(card_value).to eq("My title")
138 | end
139 | end
140 |
141 | context "with SEO" do
142 | let(:seo) do
143 | { title: "SEO title" }
144 | end
145 |
146 | it "returns SEO title" do
147 | expect(title_tag).to eq("SEO title")
148 | expect(og_value).to eq("SEO title")
149 | expect(card_value).to eq("SEO title")
150 | end
151 | end
152 | end
153 |
154 | context "with blank title" do
155 | let(:item_title) { "" }
156 |
157 | context "no SEO" do
158 | it "returns fallback title" do
159 | expect(title_tag).to eq("Default title")
160 | expect(og_value).to eq("Default title")
161 | expect(card_value).to eq("Default title")
162 | end
163 | end
164 |
165 | context "with SEO" do
166 | let(:seo) do
167 | { title: "SEO title" }
168 | end
169 |
170 | it "returns SEO title" do
171 | expect(title_tag).to eq("SEO title")
172 | expect(og_value).to eq("SEO title")
173 | expect(card_value).to eq("SEO title")
174 | end
175 | end
176 | end
177 | end
178 | end
179 | end
180 | end
181 | end
182 | end
183 | end
184 |
--------------------------------------------------------------------------------
/lib/dato/local/loader.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "pusher-client"
4 |
5 | require "dato/local/entities_repo"
6 | require "dato/local/items_repo"
7 |
8 | module Dato
9 | module Local
10 | class Loader
11 | attr_reader :client, :entities_repo, :items_repo, :preview_mode
12 |
13 | PUSHER_API_KEY = "75e6ef0fe5d39f481626"
14 |
15 | # rubocop:disable Style/OptionalBooleanParameter
16 | def initialize(client, preview_mode = false)
17 | @client = client
18 | @preview_mode = preview_mode
19 | @entities_repo = EntitiesRepo.new
20 | @items_repo = ItemsRepo.new(@entities_repo)
21 | end
22 | # rubocop:enable Style/OptionalBooleanParameter
23 |
24 | def load
25 | threads = [
26 | Thread.new { Thread.current[:output] = site },
27 | Thread.new { Thread.current[:output] = all_items },
28 | Thread.new { Thread.current[:output] = all_uploads },
29 | ]
30 |
31 | results = threads.map do |t|
32 | t.join
33 | t[:output]
34 | end
35 |
36 | @entities_repo = EntitiesRepo.new(*results)
37 | @items_repo = ItemsRepo.new(@entities_repo)
38 | end
39 |
40 | def watch(&block)
41 | site_id = client.get("/site")["data"]["id"]
42 |
43 | return if pusher && pusher.connected
44 |
45 | channel_name = if client.environment
46 | "private-site-#{site_id}-environment-#{client.environment}"
47 | else
48 | "private-site-#{site_id}"
49 | end
50 |
51 | pusher.subscribe(channel_name)
52 |
53 | bind_on_site_upsert(&block)
54 | bind_on_item_destroy(&block)
55 | bind_on_item_upsert(&block)
56 | bind_on_item_type_upsert(&block)
57 | bind_on_item_type_destroy(&block)
58 | bind_on_upload_upsert(&block)
59 | bind_on_upload_destroy(&block)
60 |
61 | pusher.connect(true)
62 | end
63 |
64 | def stop_watch
65 | pusher.disconnect if pusher && pusher.connected
66 | end
67 |
68 | private
69 |
70 | def bind_on_site_upsert(&block)
71 | bind_on("site:upsert", block) do |_data|
72 | threads = [
73 | Thread.new { Thread.current[:output] = site },
74 | Thread.new { Thread.current[:output] = all_items },
75 | Thread.new { Thread.current[:output] = all_uploads },
76 | ]
77 |
78 | results = threads.map do |t|
79 | t.join
80 | t[:output]
81 | end
82 |
83 | @entities_repo = EntitiesRepo.new(*results)
84 | end
85 | end
86 |
87 | def bind_on_item_upsert(&block)
88 | event_type = preview_mode ? "preview_mode" : "published_mode"
89 |
90 | bind_on("item:#{event_type}:upsert", block) do |data|
91 | payload = client.items.all(
92 | {
93 | "filter[ids]" => data[:ids].join(","),
94 | version: item_version,
95 | },
96 | deserialize_response: false,
97 | all_pages: true,
98 | )
99 |
100 | @entities_repo.upsert_entities(payload)
101 | end
102 | end
103 |
104 | def bind_on_item_destroy(&block)
105 | event_type = preview_mode ? "preview_mode" : "published_mode"
106 |
107 | bind_on("item:#{event_type}:destroy", block) do |data|
108 | @entities_repo.destroy_entities("item", data[:ids])
109 | end
110 | end
111 |
112 | def bind_on_upload_upsert(&block)
113 | bind_on("upload:upsert", block) do |data|
114 | payload = client.uploads.all(
115 | {
116 | "filter[ids]" => data[:ids].join(","),
117 | },
118 | deserialize_response: false,
119 | all_pages: true,
120 | )
121 |
122 | @entities_repo.upsert_entities(payload)
123 | end
124 | end
125 |
126 | def bind_on_upload_destroy(&block)
127 | bind_on("upload:destroy", block) do |data|
128 | @entities_repo.destroy_entities("upload", data[:ids])
129 | end
130 | end
131 |
132 | def bind_on_item_type_upsert(&block)
133 | bind_on("item_type:upsert", block) do |data|
134 | data[:ids].each do |id|
135 | payload = client.item_types.find(id, {}, deserialize_response: false)
136 | @entities_repo.upsert_entities(payload)
137 |
138 | payload = client.items.all(
139 | { "filter[type]" => id },
140 | deserialize_response: false,
141 | all_pages: true,
142 | )
143 |
144 | @entities_repo.upsert_entities(payload)
145 | end
146 | end
147 | end
148 |
149 | def bind_on_item_type_destroy(&block)
150 | bind_on("item_type:destroy", block) do |data|
151 | data[:ids].each do |id|
152 | @entities_repo.destroy_item_type(id)
153 | end
154 | end
155 | end
156 |
157 | def bind_on(event_name, user_block, &block)
158 | pusher.bind(event_name) do |data|
159 | parsed_data = JSON.parse(data)
160 | block.call(parsed_data.deep_symbolize_keys)
161 | update_items_repo!
162 | user_block.call
163 | end
164 | end
165 |
166 | def update_items_repo!
167 | @items_repo = ItemsRepo.new(@entities_repo)
168 | end
169 |
170 | def pusher
171 | PusherClient.logger.level = Logger::WARN
172 |
173 | @pusher ||= PusherClient::Socket.new(
174 | PUSHER_API_KEY,
175 | secure: true,
176 | auth_method: method(:pusher_auth_method),
177 | )
178 | end
179 |
180 | def site
181 | client.get("/site", include: ["item_types", "item_types.fields"])
182 | end
183 |
184 | def all_items
185 | client.items.all(
186 | { version: item_version },
187 | deserialize_response: false,
188 | all_pages: true,
189 | )
190 | end
191 |
192 | def all_uploads
193 | client.uploads.all({},
194 | deserialize_response: false,
195 | all_pages: true)
196 | end
197 |
198 | def item_version
199 | if preview_mode
200 | "latest"
201 | else
202 | "published"
203 | end
204 | end
205 |
206 | def pusher_auth_method(socket_id, channel)
207 | client.pusher_token(socket_id, channel.name)["auth"]
208 | end
209 | end
210 | end
211 | end
212 |
--------------------------------------------------------------------------------
/lib/dato/local/items_repo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dato/local/item"
4 | require "dato/local/site"
5 |
6 | module Dato
7 | module Local
8 | class ItemsRepo
9 | attr_reader :entities_repo, :collections_by_type, :item_type_methods
10 |
11 | def initialize(entities_repo)
12 | @entities_repo = entities_repo
13 | @collections_by_type = {}
14 | @items_by_id = {}
15 | @items_by_parent_id = {}
16 | @item_type_methods = {}
17 |
18 | build_cache!
19 | end
20 |
21 | def find(id)
22 | @items_by_id[id.to_s]
23 | end
24 |
25 | def children_of(id)
26 | @items_by_parent_id.fetch(id.to_s, [])
27 | end
28 |
29 | def respond_to_missing?(method, include_private = false)
30 | if collections_by_type.key?(method)
31 | true
32 | else
33 | super
34 | end
35 | end
36 |
37 | def site
38 | Site.new(
39 | entities_repo.find_entities_of_type("site").first,
40 | self,
41 | )
42 | end
43 |
44 | def available_locales
45 | site.locales.map(&:to_sym)
46 | end
47 |
48 | def item_types
49 | entities_repo.find_entities_of_type("item_type")
50 | end
51 |
52 | def single_instance_item_types
53 | item_types.select(&:singleton)
54 | end
55 |
56 | def collection_item_types
57 | item_types.select do |item_type|
58 | !item_type.singleton && !item_type.modular_block
59 | end
60 | end
61 |
62 | def items_of_type(item_type)
63 | method = item_type_methods[item_type]
64 |
65 | if item_type.singleton
66 | Array(@collections_by_type[method])
67 | else
68 | @collections_by_type[method]
69 | end
70 | end
71 |
72 | private
73 |
74 | def build_cache!
75 | build_item_type_methods!
76 | build_collections_by_type!
77 | build_singletons_by_type!
78 | end
79 |
80 | def build_item_type_methods!
81 | @item_type_methods = {}
82 |
83 | singleton_keys = single_instance_item_types.map(&:api_key)
84 | collection_keys = collection_item_types.map(&:api_key)
85 | .map(&:pluralize)
86 |
87 | clashing_keys = singleton_keys & collection_keys
88 |
89 | item_types.each do |item_type|
90 | pluralized_api_key = item_type.api_key.pluralize
91 |
92 | method = if item_type.singleton
93 | item_type.api_key
94 | else
95 | pluralized_api_key
96 | end
97 |
98 | if clashing_keys.include?(pluralized_api_key)
99 | suffix = item_type.singleton ? "instance" : "collection"
100 | method = "#{method}_#{suffix}"
101 | end
102 |
103 | @item_type_methods[item_type] = method.to_sym
104 | end
105 | end
106 |
107 | def build_collections_by_type!
108 | item_types.each do |item_type|
109 | method = item_type_methods[item_type]
110 | @collections_by_type[method] = if item_type.singleton
111 | nil
112 | else
113 | ItemCollection.new
114 | end
115 | end
116 |
117 | item_entities.each do |item_entity|
118 | item = Item.new(item_entity, self)
119 | method = item_type_methods[item_entity.item_type]
120 |
121 | @collections_by_type[method].push item unless item_entity.item_type.singleton
122 |
123 | @items_by_id[item.id] = item
124 |
125 | if item_entity.respond_to?(:parent_id) && item_entity.parent_id
126 | @items_by_parent_id[item_entity.parent_id] ||= []
127 | @items_by_parent_id[item_entity.parent_id] << item
128 | end
129 | end
130 |
131 | item_types.each do |item_type|
132 | method = item_type_methods[item_type]
133 | if !item_type.singleton && item_type.sortable
134 | nil_items, valid_items = @collections_by_type[method].partition do |item|
135 | item.position.nil?
136 | end
137 | @collections_by_type[method] = valid_items.sort_by(&:position) + nil_items
138 | elsif item_type.ordering_field
139 | field = item_type.ordering_field.api_key
140 | nil_items, valid_items = @collections_by_type[method].partition do |item|
141 | item[field].nil?
142 | end
143 | @collections_by_type[method] = valid_items.sort_by { |item| item[field] } + nil_items
144 | @collections_by_type[method].reverse! if item_type.ordering_direction == "desc"
145 | end
146 | end
147 | end
148 |
149 | def build_singletons_by_type!
150 | item_types.each do |item_type|
151 | method = item_type_methods[item_type]
152 | next unless item_type.singleton
153 |
154 | item = (@items_by_id[item_type.singleton_item.id] if item_type.singleton_item)
155 |
156 | @collections_by_type[method] = item
157 | end
158 | end
159 |
160 | def item_entities
161 | entities_repo.find_entities_of_type("item")
162 | end
163 |
164 | def method_missing(method, *arguments, &block)
165 | if collections_by_type.key?(method) && arguments.empty?
166 | collections_by_type[method]
167 | else
168 | super
169 | end
170 | rescue NoMethodError
171 | message = []
172 | message << "Undefined method `#{method}`"
173 | message << "Available DatoCMS collections/items:"
174 | message += collections_by_type.map do |key, _value|
175 | "* .#{key}"
176 | end
177 | raise NoMethodError, message.join("\n")
178 | end
179 |
180 | class ItemCollection < Array
181 | def each(&block)
182 | if block && block.arity == 2
183 | each_with_object({}) do |item, acc|
184 | acc[item.id] = item
185 | end.each(&block)
186 | else
187 | super(&block)
188 | end
189 | end
190 |
191 | def [](id)
192 | if id.is_a? String
193 | find { |item| item.id == id }
194 | else
195 | super(id)
196 | end
197 | end
198 |
199 | def keys
200 | map(&:id)
201 | end
202 |
203 | def values
204 | to_a
205 | end
206 | end
207 | end
208 | end
209 | end
210 |
--------------------------------------------------------------------------------
/spec/dato/local/item_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | module Dato
6 | module Local
7 | RSpec.describe Item do
8 | subject(:item) do
9 | ItemsRepo.new(entities_repo).work_items.first
10 | end
11 |
12 | let(:entity) do
13 | entities_repo.find_entities_of_type("item").first
14 | end
15 |
16 | let(:entities_repo) do
17 | EntitiesRepo.new(entities)
18 | end
19 |
20 | let(:repo) do
21 | ItemsRepo.new(entities_repo)
22 | end
23 |
24 | let(:entities) do
25 | {
26 | data: [
27 | {
28 | id: "item",
29 | type: "item",
30 | attributes: item_attributes,
31 | meta: {
32 | updated_at: "2010-01-01T00:00",
33 | },
34 | relationships: {
35 | item_type: {
36 | data: {
37 | id: "item-type",
38 | type: "item_type",
39 | },
40 | },
41 | },
42 | },
43 | {
44 | id: "item-type",
45 | type: "item_type",
46 | attributes: {
47 | singleton: is_singleton,
48 | modular_block: false,
49 | sortable: true,
50 | api_key: "work_item",
51 | },
52 | relationships: {
53 | fields: {
54 | data: [
55 | { id: "title", type: "field" },
56 | { id: "body", type: "field" },
57 | ],
58 | },
59 | },
60 | },
61 | {
62 | id: "title",
63 | type: "field",
64 | attributes: {
65 | position: 1,
66 | api_key: title_api_key,
67 | localized: title_localized,
68 | field_type: title_field_type,
69 | appearance: {
70 | addons: [],
71 | editor: "single_line",
72 | parameters: { heading: true },
73 | },
74 | },
75 | },
76 | {
77 | id: "body",
78 | type: "field",
79 | attributes: {
80 | position: 2,
81 | api_key: "body",
82 | localized: false,
83 | field_type: "text",
84 | appearance: {
85 | addons: [],
86 | editor: "markdown",
87 | parameters: { toolbar: ["bold"] },
88 | },
89 | },
90 | },
91 | ],
92 | }
93 | end
94 |
95 | let(:is_singleton) { false }
96 | let(:title_localized) { false }
97 | let(:title_field_type) { "string" }
98 | let(:title_api_key) { "title" }
99 |
100 | let(:item_attributes) do
101 | {
102 | title_api_key.to_sym => "My titlè with àccents",
103 | body: "Hi there",
104 | position: 2,
105 | }
106 | end
107 |
108 | describe "#attributes" do
109 | it "returns an hash of the field values" do
110 | expected_attributes = {
111 | "title" => "My titlè with àccents",
112 | "body" => "Hi there",
113 | }
114 | expect(item.attributes).to eq expected_attributes
115 | end
116 | end
117 |
118 | describe "position" do
119 | it "returns the entity position field" do
120 | expect(item.position).to eq 2
121 | end
122 | end
123 |
124 | describe "updated_at" do
125 | it "returns the entity updated_at field" do
126 | expect(item.updated_at).to be_a Time
127 | end
128 | end
129 |
130 | describe "dynamic methods" do
131 | context "existing field" do
132 | it "returns the field value" do
133 | expect(item.respond_to?(:body)).to be_truthy
134 | expect(item.body).to eq "Hi there"
135 | expect(item[:body]).to eq "Hi there"
136 | expect(item["body"]).to eq "Hi there"
137 | end
138 |
139 | context "localized field" do
140 | let(:item_attributes) do
141 | super().merge(title: { it: "Foo", en: "Bar" })
142 | end
143 |
144 | let(:title_localized) { true }
145 |
146 | it "returns the value for the current locale" do
147 | I18n.with_locale(:it) do
148 | expect(item.title).to eq "Foo"
149 | end
150 | end
151 |
152 | context "non existing value" do
153 | it "raises nil" do
154 | I18n.with_locale(:ru) do
155 | expect(item.title).to eq nil
156 | end
157 | end
158 | end
159 |
160 | context "fallbacks" do
161 | let(:item_attributes) do
162 | super().merge(title: { ru: nil, "es-ES": "Bar" })
163 | end
164 |
165 | it "uses them" do
166 | I18n.with_locale(:ru) do
167 | expect(item.title).to eq "Bar"
168 | end
169 | end
170 | end
171 | end
172 | end
173 |
174 | context "non existing field" do
175 | it "raises NoMethodError" do
176 | expect(item.respond_to?(:qux)).to be_falsy
177 | expect { item.qux }.to raise_error NoMethodError
178 | end
179 | end
180 |
181 | context "non existing field type" do
182 | let(:title_field_type) { "rotfl" }
183 |
184 | it "returns the raw item value" do
185 | expect(item.title).to eq "My titlè with àccents"
186 | end
187 | end
188 |
189 | context "field with api_key = meta" do
190 | let(:title_api_key) { "meta" }
191 |
192 | it ".meta returns the meta info" do
193 | expect(item.meta).to be_a JsonApiMeta
194 | expect(item["meta"]).to eq "My titlè with àccents"
195 | expect(item[:meta]).to eq "My titlè with àccents"
196 | end
197 | end
198 | end
199 |
200 | context "meta" do
201 | it "returns raw info" do
202 | expect(item.meta.updated_at).to eq "2010-01-01T00:00"
203 | end
204 | end
205 |
206 | context "equality" do
207 | subject(:same_item) { described_class.new(entity, repo) }
208 |
209 | subject(:another_item) { described_class.new(another_entity, repo) }
210 | let(:another_entity) do
211 | double(
212 | "Dato::Local::JsonApiEntity(Item)",
213 | id: "15",
214 | )
215 | end
216 |
217 | it "two items are equal if their id is the same" do
218 | expect(item).to eq same_item
219 | end
220 |
221 | it "else they're not" do
222 | expect(item).not_to eq another_item
223 | expect(item).not_to eq "foobar"
224 | end
225 | end
226 | end
227 | end
228 | end
229 |
--------------------------------------------------------------------------------
/spec/support/shared_contexts/new_site.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_context "with a new site" do
4 | def b(type, *other)
5 | node = {
6 | "type" => type,
7 | }
8 |
9 | param = other.shift
10 |
11 | if param.is_a?(Hash)
12 | node.merge!(param)
13 | param = other.shift
14 | end
15 |
16 | node["children"] = param if param.is_a?(Array)
17 |
18 | node["value"] = param if param.is_a?(String)
19 |
20 | node.stringify_keys
21 | end
22 |
23 | let(:account_client) do
24 | generate_account_client!
25 | end
26 |
27 | let(:site) do
28 | account_client.sites.create(
29 | name: "Integration new test site",
30 | )
31 | end
32 |
33 | let(:client) do
34 | Dato::Site::Client.new(
35 | site[:readwrite_token],
36 | base_url: ENV.fetch("SITE_API_BASE_URL"),
37 | )
38 | end
39 |
40 | let(:item_type) do
41 | client.item_types.create(
42 | name: "Article",
43 | singleton: false,
44 | modular_block: false,
45 | sortable: false,
46 | tree: false,
47 | draft_mode_active: false,
48 | api_key: "article",
49 | ordering_direction: nil,
50 | ordering_field: nil,
51 | all_locales_required: true,
52 | title_field: nil,
53 | )
54 | end
55 |
56 | let(:author_type) do
57 | client.item_types.create(
58 | name: "Author",
59 | singleton: false,
60 | modular_block: false,
61 | sortable: false,
62 | tree: false,
63 | draft_mode_active: false,
64 | api_key: "author",
65 | ordering_direction: nil,
66 | ordering_field: nil,
67 | all_locales_required: true,
68 | title_field: nil,
69 | )
70 | end
71 |
72 | let(:author_field) do
73 | client.fields.create(
74 | author_type[:id],
75 | api_key: "name",
76 | field_type: "string",
77 | label: "Name",
78 | localized: false,
79 | hint: "",
80 | validators: { required: {} },
81 | )
82 | end
83 |
84 | let(:block_type) do
85 | client.item_types.create(
86 | name: "Block",
87 | singleton: false,
88 | modular_block: true,
89 | sortable: false,
90 | tree: false,
91 | draft_mode_active: false,
92 | api_key: "block",
93 | ordering_direction: nil,
94 | ordering_field: nil,
95 | all_locales_required: true,
96 | title_field: nil,
97 | )
98 | end
99 |
100 | let(:block_field) do
101 | client.fields.create(
102 | block_type[:id],
103 | api_key: "title",
104 | field_type: "string",
105 | label: "Title",
106 | localized: false,
107 | hint: "",
108 | validators: { required: {} },
109 | )
110 | end
111 |
112 | let(:structured_text_field) do
113 | client.fields.create(
114 | item_type[:id],
115 | api_key: "content",
116 | field_type: "structured_text",
117 | label: "Content",
118 | localized: false,
119 | hint: "",
120 | validators: {
121 | structured_text_blocks: { item_types: [block_type[:id]] },
122 | structured_text_links: { item_types: [author_type[:id]] },
123 | },
124 | )
125 | end
126 |
127 | let(:text_field) do
128 | client.fields.create(
129 | item_type[:id],
130 | api_key: "title",
131 | field_type: "string",
132 | label: "Title",
133 | localized: true,
134 | hint: "",
135 | validators: { required: {} },
136 | )
137 | end
138 |
139 | let(:slug_field) do
140 | client.fields.create(
141 | item_type[:id],
142 | api_key: "slug",
143 | field_type: "slug",
144 | label: "Slug",
145 | localized: false,
146 | hint: "",
147 | validators: {
148 | required: {},
149 | slug_title_field: {
150 | title_field_id: text_field[:id].to_s,
151 | },
152 | },
153 | )
154 | end
155 |
156 | let(:image_field) do
157 | client.fields.create(
158 | item_type[:id],
159 | api_key: "image",
160 | field_type: "file",
161 | label: "Image",
162 | localized: false,
163 | hint: "",
164 | validators: {
165 | required: {},
166 | extension: {
167 | predefined_list: "image",
168 | },
169 | },
170 | )
171 | end
172 |
173 | let(:file_field) do
174 | client.fields.create(
175 | item_type[:id],
176 | api_key: "file",
177 | field_type: "file",
178 | label: "File",
179 | localized: false,
180 | hint: "",
181 | validators: { required: {} },
182 | )
183 | end
184 |
185 | let(:image_id) { client.upload_image("https://www.datocms-assets.com/205/1549027974-logo.png")[:upload_id] }
186 | let(:file_id) { client.upload_file("./spec/fixtures/file.txt")[:upload_id] }
187 |
188 | let(:author) do
189 | client.items.create(
190 | item_type: author_type[:id],
191 | author_field[:api_key] => "Mark Smith",
192 | )
193 | end
194 |
195 | let(:item) do
196 | client.items.create(
197 | item_type: item_type[:id],
198 | text_field[:api_key] => {
199 | en: "First post",
200 | it: "Primo post",
201 | },
202 | slug_field[:api_key] => "first-post",
203 | image_field[:api_key] => {
204 | upload_id: image_id,
205 | alt: "My first post",
206 | title: "First post",
207 | custom_data: {},
208 | focal_point: {
209 | x: 0.1,
210 | y: 0.1,
211 | },
212 | },
213 | structured_text_field[:api_key] => {
214 | schema: "dast",
215 | document: b("root", [
216 | b("heading", { level: 1 }, [b("span", "This is the title!")]),
217 | b("paragraph", [
218 | b("span", "And "),
219 | b("span", { marks: ["strong"] }, "this"),
220 | b("span", " is an "),
221 | b("itemLink", { item: author[:id] }, [b("span", "author")]),
222 | ]),
223 | b("paragraph", [b("inlineItem", { item: author[:id] })]),
224 | b("block", {
225 | item: Dato::Utils::BuildModularBlock.build({
226 | item_type: block_type[:id],
227 | block_field[:api_key] => "Foo",
228 | }),
229 | }),
230 | ]),
231 | },
232 | file_field[:api_key] => {
233 | upload_id: file_id,
234 | alt: "My first file",
235 | title: "My first file",
236 | custom_data: {},
237 | },
238 | )
239 | end
240 |
241 | before do
242 | site
243 |
244 | client.site.update(
245 | client.site.find.merge(
246 | locales: %w[en it],
247 | theme: {
248 | logo: client.upload_image("./spec/fixtures/dato-logo.jpg")[:upload_id],
249 | primary_color: {
250 | red: 63,
251 | green: 63,
252 | blue: 63,
253 | alpha: 63,
254 | },
255 | dark_color: {
256 | red: 0,
257 | green: 0,
258 | blue: 0,
259 | alpha: 0,
260 | },
261 | light_color: {
262 | red: 127,
263 | green: 127,
264 | blue: 127,
265 | alpha: 127,
266 | },
267 | accent_color: {
268 | red: 255,
269 | green: 255,
270 | blue: 255,
271 | alpha: 255,
272 | },
273 | },
274 | ),
275 | )
276 |
277 | client.items.publish(item[:id])
278 | end
279 | end
280 |
--------------------------------------------------------------------------------
/lib/dato/local/field_type/file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "imgix"
4 |
5 | module Dato
6 | module Local
7 | module FieldType
8 | class File
9 | def self.parse(value, repo)
10 | if value
11 | v = value.with_indifferent_access
12 |
13 | upload = repo.entities_repo.find_entity("upload", v[:upload_id])
14 |
15 | if upload
16 | new(
17 | upload,
18 | v[:alt],
19 | v[:title],
20 | v[:custom_data],
21 | v[:focal_point],
22 | repo.site.entity.imgix_host,
23 | )
24 | end
25 | end
26 | end
27 |
28 | def initialize(
29 | upload,
30 | alt,
31 | title,
32 | custom_data,
33 | focal_point,
34 | imgix_host
35 | )
36 | @upload = upload
37 | @alt = alt
38 | @title = title
39 | @custom_data = custom_data
40 | @focal_point = focal_point
41 | @imgix_host = imgix_host
42 | end
43 |
44 | def id
45 | @upload.id
46 | end
47 |
48 | def path
49 | @upload.path
50 | end
51 |
52 | def format
53 | @upload.format
54 | end
55 |
56 | def size
57 | @upload.size
58 | end
59 |
60 | def width
61 | @upload.width
62 | end
63 |
64 | def height
65 | @upload.height
66 | end
67 |
68 | def author
69 | @upload.author
70 | end
71 |
72 | def notes
73 | @upload.notes
74 | end
75 |
76 | def copyright
77 | @upload.copyright
78 | end
79 |
80 | def filename
81 | @upload.filename
82 | end
83 |
84 | def basename
85 | @upload.basename
86 | end
87 |
88 | def alt
89 | default_metadata = @upload.default_field_metadata.deep_stringify_keys
90 | .fetch(I18n.locale.to_s, {})
91 | @alt || default_metadata["alt"]
92 | end
93 |
94 | def title
95 | default_metadata = @upload.default_field_metadata.deep_stringify_keys
96 | .fetch(I18n.locale.to_s, {})
97 | @title || default_metadata["title"]
98 | end
99 |
100 | def custom_data
101 | default_metadata = @upload.default_field_metadata.deep_stringify_keys
102 | .fetch(I18n.locale.to_s, {})
103 | @custom_data.merge(default_metadata.fetch("custom_data", {}))
104 | end
105 |
106 | def focal_point
107 | default_metadata = @upload.default_field_metadata.deep_stringify_keys
108 | .fetch(I18n.locale.to_s, {})
109 | @focal_point || default_metadata["focal_point"]
110 | end
111 |
112 | def tags
113 | @upload.tags
114 | end
115 |
116 | def smart_tags
117 | @upload.smart_tags
118 | end
119 |
120 | def is_image
121 | @upload.is_image
122 | end
123 |
124 | def exif_info
125 | @upload.exif_info
126 | end
127 |
128 | def mime_type
129 | @upload.mime_type
130 | end
131 |
132 | def colors
133 | @upload.colors.map { |color| Color.parse(color, nil) }
134 | end
135 |
136 | def blurhash
137 | @upload.blurhash
138 | end
139 |
140 | class VideoAttributes
141 | def initialize(upload)
142 | @upload = upload
143 | end
144 |
145 | def mux_playback_id
146 | @upload.mux_playback_id
147 | end
148 |
149 | def frame_rate
150 | @upload.frame_rate
151 | end
152 |
153 | def duration
154 | @upload.duration
155 | end
156 |
157 | def streaming_url
158 | "https://stream.mux.com/#{@upload.mux_playback_id}.m3u8"
159 | end
160 |
161 | def thumbnail_url(format = :jpg)
162 | if format == :gif
163 | "https://image.mux.com/#{@upload.mux_playback_id}/animated.gif"
164 | else
165 | "https://image.mux.com/#{@upload.mux_playback_id}/thumbnail.#{format}"
166 | end
167 | end
168 |
169 | def mp4_url(options = nil)
170 | @upload.mux_mp4_highest_res or
171 | return nil
172 |
173 | if options && options[:exact_res]
174 | if options[:exact_res] == :low
175 | raw_mp4_url("low")
176 | elsif options[:exact_res] == :medium
177 | raw_mp4_url("medium") if %w[medium high].include?(@upload.mux_mp4_highest_res)
178 | elsif @upload.mux_mp4_highest_res == :high
179 | raw_mp4_url("high")
180 | end
181 | elsif options && options[:res] == :low
182 | raw_mp4_url("low")
183 | elsif options && options[:res] == :medium
184 | if %w[low medium].include?(@upload.mux_mp4_highest_res)
185 | raw_mp4_url(@upload.mux_mp4_highest_res)
186 | else
187 | raw_mp4_url("medium")
188 | end
189 | else
190 | raw_mp4_url(@upload.mux_mp4_highest_res)
191 | end
192 | end
193 |
194 | def to_hash
195 | {
196 | mux_playback_id: mux_playback_id,
197 | frame_rate: frame_rate,
198 | duration: duration,
199 | streaming_url: streaming_url,
200 | thumbnail_url: thumbnail_url,
201 | mp4_url: mp4_url,
202 | }
203 | end
204 |
205 | private
206 |
207 | def raw_mp4_url(res)
208 | "https://stream.mux.com/#{@upload.mux_playback_id}/#{res}.mp4"
209 | end
210 | end
211 |
212 | def video
213 | VideoAttributes.new(@upload) if @upload.mux_playback_id
214 | end
215 |
216 | def file
217 | Imgix::Client.new(
218 | domain: @imgix_host,
219 | secure: true,
220 | include_library_param: false,
221 | ).path(path)
222 | end
223 |
224 | def url(query = {})
225 | query.deep_stringify_keys!
226 |
227 | if focal_point &&
228 | query["fit"] == "crop" &&
229 | (query["h"] || query["height"]) &&
230 | (query["w"] || query["width"]) &&
231 | [nil, "focalpoint"].include?(query["crop"]) &&
232 | query["fp-x"].nil? &&
233 | query["fp-y"].nil?
234 |
235 | query.merge!(
236 | "crop" => "focalpoint",
237 | "fp-x" => focal_point[:x],
238 | "fp-y" => focal_point[:y],
239 | )
240 | end
241 |
242 | file.to_url(query)
243 | end
244 |
245 | def lqip_data_url(opts = {})
246 | @imgix_host != "www.datocms-assets.com" and
247 | raise "#lqip_data_url can only be used with www.datocms-assets.com domain"
248 |
249 | response = Faraday.get(file.to_url(opts.merge(lqip: "blurhash")))
250 |
251 | "data:image/jpeg;base64,#{Base64.strict_encode64(response.body)}" if response.status == 200
252 | end
253 |
254 | def to_hash(*_args)
255 | {
256 | id: id,
257 | format: format,
258 | size: size,
259 | width: width,
260 | height: height,
261 | alt: alt,
262 | title: title,
263 | custom_data: custom_data,
264 | focal_point: focal_point,
265 | url: url,
266 | copyright: copyright,
267 | tags: tags,
268 | smart_tags: smart_tags,
269 | filename: filename,
270 | basename: basename,
271 | is_image: is_image,
272 | exif_info: exif_info,
273 | mime_type: mime_type,
274 | colors: colors.map(&:to_hash),
275 | blurhash: blurhash,
276 | video: video && video.to_hash,
277 | }
278 | end
279 | end
280 | end
281 | end
282 | end
283 |
--------------------------------------------------------------------------------
/spec/support/shared_contexts/meta_tags.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_context "items repo" do
4 | let(:item) { nil }
5 | let(:site) { items_repo.site }
6 | let(:items_repo) { Dato::Local::ItemsRepo.new(entities_repo) }
7 | let(:entities_repo) { Dato::Local::EntitiesRepo.new(payload) }
8 | let(:payload) do
9 | {
10 | data: [
11 | {
12 | id: "24038",
13 | type: "item",
14 | attributes: {
15 | is_valid: true,
16 | title: item_title,
17 | another_string: "Foo bar",
18 | seo_settings: seo,
19 | image: (
20 | if item_image
21 | {
22 | upload_id: item_image,
23 | alt: nil,
24 | title: nil,
25 | custom_data: {},
26 | }
27 | end
28 | ),
29 | },
30 | meta: {
31 | updated_at: "2016-12-07T09:14:22.046Z",
32 | },
33 | relationships: {
34 | item_type: {
35 | data: {
36 | id: "3781",
37 | type: "item_type",
38 | },
39 | },
40 | },
41 | },
42 | {
43 | id: "666",
44 | type: "upload",
45 | attributes: {
46 | path: "/seo.png",
47 | width: 500,
48 | height: 500,
49 | format: "png",
50 | size: 572_451,
51 | default_field_metadata: {
52 | en: {
53 | alt: "an alt",
54 | title: "a title",
55 | custom_data: {},
56 | },
57 | },
58 | },
59 | },
60 | {
61 | id: "999",
62 | type: "upload",
63 | attributes: {
64 | path: "/fallback_seo.png",
65 | width: 500,
66 | height: 500,
67 | format: "png",
68 | size: 543_210,
69 | default_field_metadata: {
70 | en: {
71 | alt: "another alt",
72 | title: "another title",
73 | custom_data: {},
74 | },
75 | },
76 | },
77 | },
78 | {
79 | id: "111",
80 | type: "upload",
81 | attributes: {
82 | path: "/simple_image.png",
83 | width: 500,
84 | height: 500,
85 | format: "png",
86 | size: 543_210,
87 | default_field_metadata: {
88 | en: {
89 | alt: "image alt",
90 | title: "image title",
91 | custom_data: {},
92 | },
93 | },
94 | },
95 | },
96 | {
97 | id: "681",
98 | type: "site",
99 | attributes: {
100 | name: "XXX",
101 | locales: ["en"],
102 | theme_hue: 190,
103 | domain: nil,
104 | internal_domain: "wispy-sun-3056.admin.datocms.com",
105 | global_seo: global_seo,
106 | favicon: favicon,
107 | no_index: no_index,
108 | ssg: nil,
109 | imgix_host: "www.datocms-assets.com",
110 | },
111 | relationships: {
112 | menu_items: {
113 | data: [
114 | {
115 | id: "4212",
116 | type: "menu_item",
117 | },
118 | ],
119 | },
120 | item_types: {
121 | data: [
122 | {
123 | id: "3781",
124 | type: "item_type",
125 | },
126 | ],
127 | },
128 | },
129 | },
130 | {
131 | id: "3781",
132 | type: "item_type",
133 | attributes: {
134 | name: "Article",
135 | singleton: false,
136 | modular_block: false,
137 | draft_mode_active: false,
138 | sortable: false,
139 | api_key: "article",
140 | ordering_direction: nil,
141 | },
142 | relationships: {
143 | fields: {
144 | data: [
145 | {
146 | id: "15088",
147 | type: "field",
148 | },
149 | {
150 | id: "15085",
151 | type: "field",
152 | },
153 | {
154 | id: "15086",
155 | type: "field",
156 | },
157 | {
158 | id: "15087",
159 | type: "field",
160 | },
161 | ],
162 | },
163 | singleton_item: {
164 | data: nil,
165 | },
166 | ordering_field: {
167 | data: nil,
168 | },
169 | title_field: {
170 | data: {
171 | id: "15085",
172 | type: "field",
173 | },
174 | },
175 | },
176 | },
177 | {
178 | id: "15088",
179 | type: "field",
180 | attributes: {
181 | label: "Image",
182 | field_type: "file",
183 | api_key: "image",
184 | hint: nil,
185 | localized: false,
186 | validators: {},
187 | position: 1,
188 | appearance: {
189 | editor: "file",
190 | parameters: {},
191 | },
192 | },
193 | relationships: {
194 | item_type: {
195 | data: {
196 | id: "3781",
197 | type: "item_type",
198 | },
199 | },
200 | },
201 | },
202 | {
203 | id: "15085",
204 | type: "field",
205 | attributes: {
206 | label: "Title",
207 | field_type: "string",
208 | api_key: "title",
209 | hint: nil,
210 | localized: false,
211 | validators: {
212 | required: {},
213 | },
214 | position: 2,
215 | appearance: {
216 | editor: "single_line",
217 | parameters: { heading: true },
218 | },
219 | },
220 | relationships: {
221 | item_type: {
222 | data: {
223 | id: "3781",
224 | type: "item_type",
225 | },
226 | },
227 | },
228 | },
229 | {
230 | id: "15086",
231 | type: "field",
232 | attributes: {
233 | label: "Another string",
234 | field_type: "string",
235 | api_key: "another_string",
236 | hint: nil,
237 | localized: false,
238 | validators: {},
239 | position: 3,
240 | appearance: {
241 | editor: "single_line",
242 | parameters: { heading: false },
243 | },
244 | },
245 | relationships: {
246 | item_type: {
247 | data: {
248 | id: "3781",
249 | type: "item_type",
250 | },
251 | },
252 | },
253 | },
254 | {
255 | id: "15087",
256 | type: "field",
257 | attributes: {
258 | label: "SEO settings",
259 | field_type: "seo",
260 | api_key: "seo_settings",
261 | hint: nil,
262 | localized: false,
263 | validators: {},
264 | position: 4,
265 | appearance: {
266 | editor: "seo",
267 | parameters: {},
268 | },
269 | },
270 | relationships: {
271 | item_type: {
272 | data: {
273 | id: "3781",
274 | type: "item_type",
275 | },
276 | },
277 | },
278 | },
279 | ],
280 | }
281 | end
282 | let(:item_title) { nil }
283 | let(:item_image) { nil }
284 | let(:favicon) { nil }
285 | let(:global_seo) { nil }
286 | let(:seo) { nil }
287 | let(:no_index) { false }
288 | let(:title_suffix) { nil }
289 | end
290 |
--------------------------------------------------------------------------------