diff --git a/Gemfile b/Gemfile index 96d6f2e..11968ad 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" # Specify your gem's dependencies in spooky.gemspec diff --git a/README.md b/README.md index 89f60a9..9aea9c6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A simple Ruby wrapper for the [Ghost](https://ghost.org) Content API. -The Ghost Content API documentation can be found [here](https://ghost.org/docs/api/v3/content/#endpoints). +The Ghost Content API documentation can be found [here](https://ghost.org/docs/content-api/). ## Installation @@ -77,7 +77,7 @@ Both return a `Spooky::Post`. #### Get posts with a filter applied -Filtering accepts simple hashes as conditions or [NQL strings for more complex filters](https://ghost.org/docs/api/v3/content/#syntax-reference). +Filtering accepts simple hashes as conditions or [NQL strings for more complex filters](https://ghost.org/docs/api/content/#syntax-reference). ```ruby featured_posts, pagination = client.posts(filter: { featured: true }) @@ -125,4 +125,4 @@ The gem is available as open source under the terms of the [MIT License](http:// ## Credits -This gem was originally developed by [infinityrobot](https://github.com/infinityrobot). \ No newline at end of file +This gem was originally developed by [infinityrobot](https://github.com/infinityrobot). diff --git a/Rakefile b/Rakefile index c29cca0..41b8864 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/gem_tasks" require "rubocop/rake_task" require "rspec/core/rake_task" diff --git a/bin/console b/bin/console index 364599e..9d8ef9b 100755 --- a/bin/console +++ b/bin/console @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" require "spooky" diff --git a/lib/spooky.rb b/lib/spooky.rb index 63a13ca..2deba6c 100644 --- a/lib/spooky.rb +++ b/lib/spooky.rb @@ -1,7 +1,12 @@ +# frozen_string_literal: true + require "spooky/version" -require "spooky/is_resource" +require "active_model" require "spooky/client" -require "spooky/post" +require "spooky/base" +require "spooky/count" require "spooky/author" require "spooky/tag" +require "spooky/post" +require "spooky/page" diff --git a/lib/spooky/author.rb b/lib/spooky/author.rb index e4c99b5..b7a33f0 100644 --- a/lib/spooky/author.rb +++ b/lib/spooky/author.rb @@ -1,21 +1,41 @@ +# frozen_string_literal: true + module Spooky - class Author - ATTRIBUTES = [ - "bio", - "cover_image", - "facebook", - "id", - "location", - "meta_description", - "meta_title", - "name", - "profile_image", - "slug", - "twitter", - "url", - "website" - ].freeze + class Author < Base + attribute :slug, :string + attribute :id, :string + attribute :name, :string + attribute :profile_image, :string + attribute :cover_image, :string + attribute :bio, :string + attribute :website, :string + attribute :location, :string + attribute :facebook, :string + attribute :twitter, :string + attribute :meta_title, :string + attribute :meta_description, :string + attribute :url, :string + attribute :threads, :string + attribute :bluesky, :string + attribute :mastodon, :string + attribute :tiktok, :string + attribute :instagram, :string + attribute :linkedin, :string + attribute :youtube, :string + attribute :comments, :boolean + + attr_reader :count + + def count=(attributes) + @count = Count.new(attributes) + end + + def to_s + name.to_s + end - include IsResource + def to_param + slug.to_s + end end end diff --git a/lib/spooky/base.rb b/lib/spooky/base.rb new file mode 100644 index 0000000..2da71d8 --- /dev/null +++ b/lib/spooky/base.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Spooky + class Base + include ActiveModel::Model + include ActiveModel::Attributes + + def attribute_writer_missing(name, value) + # Typically means they added something to the API. + # Most likely a new social network. + puts "Unexpected attribute for #{self.class.name}: \"#{name}\": \"#{value}\"" + end + + def persisted? + id.present? + end + end +end diff --git a/lib/spooky/client.rb b/lib/spooky/client.rb index 281d286..1ef5adc 100644 --- a/lib/spooky/client.rb +++ b/lib/spooky/client.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "http" require "active_support/core_ext/object/blank" require "active_support/core_ext/string/inflections" @@ -9,7 +11,7 @@ class Client def initialize(attrs = {}) @api_url = ENV["GHOST_API_URL"] || attrs[:api_url] @api_key = ENV["GHOST_CONTENT_API_KEY"] || attrs[:api_key] - @endpoint = "#{@api_url}/ghost/api/v3/content" + @endpoint = "#{@api_url}/ghost/api/content" end def fetch_json(resource, options = {}) @@ -37,13 +39,33 @@ def fetch(resource, options = {}) response.present? && [response.map { |attrs| resource_class.send(:new, attrs) }, pagination] end - def posts(tags: false, authors: false, filter: false, page: false, limit: false) - inc = [] - inc << "tags" if tags - inc << "authors" if authors + def pages(include: [], filter: false, page: false, limit: false) + options = {} + options[:include] = include unless include.empty? + + options = apply_filter(options, filter) + options = apply_pagination(options, { page: page, limit: limit }) + + fetch("pages", options) + end + def page_by(id: nil, slug: nil) options = {} - options[:include] = inc if inc.present? + + if id.present? + response, = fetch("pages/#{id}", options) + response.present? && response.first + elsif slug.present? + response, = fetch("pages/slug/#{slug}", options) + response.present? && response.first + else + false + end + end + + def posts(include: [], filter: false, page: false, limit: false) + options = {} + options[:include] = include unless include.empty? options = apply_filter(options, filter) options = apply_pagination(options, { page: page, limit: limit }) @@ -51,19 +73,65 @@ def posts(tags: false, authors: false, filter: false, page: false, limit: false) fetch("posts", options) end - def post_by(id: nil, slug: nil, tags: false, authors: false) - inc = [] - inc << "tags" if tags - inc << "authors" if authors + def post_by(id: nil, slug: nil, include: []) + options = {} + options[:include] = include unless include.empty? + + if id.present? + response, = fetch("posts/#{id}", options) + response.present? && response.first + elsif slug.present? + response, = fetch("posts/slug/#{slug}", options) + response.present? && response.first + else + false + end + end + + def authors(include: [], filter: false, page: false, limit: false) + options = {} + options[:include] = include unless include.empty? + + options = apply_filter(options, filter) + options = apply_pagination(options, { page: page, limit: limit }) + + fetch("authors", options) + end + + def author_by(id: nil, slug: nil, include: []) + options = {} + options[:include] = include unless include.empty? + + if id.present? + response, = fetch("authors/#{id}", options) + response.present? && response.first + elsif slug.present? + response, = fetch("authors/slug/#{slug}", options) + response.present? && response.first + else + false + end + end + + def tags(include: [], filter: false, page: false, limit: false) + options = {} + options[:include] = include unless include.empty? + + options = apply_filter(options, filter) + options = apply_pagination(options, { page: page, limit: limit }) + + fetch("tags", options) + end + def tag_by(id: nil, slug: nil, include: []) options = {} - options[:include] = inc if inc.present? + options[:include] = include unless include.empty? if id.present? - response, _ = fetch("posts/#{id}", options) + response, = fetch("tags/#{id}", options) response.present? && response.first elsif slug.present? - response, _ = fetch("posts/slug/#{slug}", options) + response, = fetch("tags/slug/#{slug}", options) response.present? && response.first else false @@ -74,11 +142,11 @@ def post_by(id: nil, slug: nil, tags: false, authors: false) def apply_filter(options, filter) if filter.present? - if filter.is_a?(Hash) - options[:filter] = filter.map { |k, v| "#{k}:#{v}" }.join("+") - else - options[:filter] = filter - end + options[:filter] = if filter.is_a?(Hash) + filter.map { |k, v| "#{k}:#{v}" }.join("+") + else + filter + end end options diff --git a/lib/spooky/count.rb b/lib/spooky/count.rb new file mode 100644 index 0000000..a0c6aeb --- /dev/null +++ b/lib/spooky/count.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Spooky + class Count < Base + attribute :posts, :integer + end +end diff --git a/lib/spooky/is_resource.rb b/lib/spooky/is_resource.rb deleted file mode 100644 index 159f039..0000000 --- a/lib/spooky/is_resource.rb +++ /dev/null @@ -1,26 +0,0 @@ -module IsResource - def self.included(base) - base.class_eval do - attr_reader(*const_get("ATTRIBUTES")) - - def initialize(attrs = {}) - self.class.const_get("ATTRIBUTES").each do |attribute| - instance_variable_set("@#{attribute}", attrs[attribute]) - end - - parse_datetimes(attrs) - parse_attributes(attrs) - end - - def parse_datetimes(attrs) - ["created_at", "updated_at", "published_at"].each do |date_attr| - instance_variable_set("@#{date_attr}", DateTime.iso8601(attrs[date_attr])) if attrs[date_attr].present? - end - end - - def parse_attributes(attrs) - # Abstract method, should be overridden in child if needed. - end - end - end -end diff --git a/lib/spooky/page.rb b/lib/spooky/page.rb new file mode 100644 index 0000000..83e9f19 --- /dev/null +++ b/lib/spooky/page.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Spooky + class Page < Post + # Per docs, "Pages are structured identically to posts."" + end +end diff --git a/lib/spooky/post.rb b/lib/spooky/post.rb index b389f85..1a61171 100644 --- a/lib/spooky/post.rb +++ b/lib/spooky/post.rb @@ -1,58 +1,84 @@ +# frozen_string_literal: true + module Spooky - class Post - ATTRIBUTES = [ - "authors", - "canonical_url", - "codeinjection_foot", - "codeinjection_head", - "comment_id", - "created_at", - "custom_excerpt", - "custom_template", - "email_subject", - "excerpt", - "feature_image", - "featured", - "html", - "id", - "meta_description", - "meta_title", - "og_description", - "og_image", - "og_title", - "primary_author", - "primary_tag", - "published_at", - "reading_time", - "send_email_when_published", - "slug", - "tags", - "title", - "twitter_description", - "twitter_image", - "twitter_title", - "updated_at", - "url", - "uuid", - "visibility" - ].freeze - - include IsResource - - def parse_attributes(attrs) - author = attrs["primary_author"] - @primary_author = author.present? && Spooky::Author.new(author) - - @authors = (attrs["authors"] || []).map do |author| - Spooky::Author.new(author) + class Post < Base + attribute :slug, :string + attribute :id, :immutable_string + attribute :uuid, :immutable_string + attribute :title, :string + attribute :html, :string + attribute :plaintext, :string + attribute :comment_id, :string + attribute :feature_image, :string + attribute :feature_image_alt, :string + attribute :feature_image_caption, :string + attribute :featured, :boolean + attribute :visibility, :string + attribute :created_at, :datetime + attribute :updated_at, :datetime + attribute :published_at, :datetime + attribute :custom_excerpt, :string + attribute :codeinjection_head, :string + attribute :codeinjection_foot, :string + attribute :custom_template, :string + attribute :canonical_url, :string + attribute :url, :string + attribute :excerpt, :string + attribute :reading_time, :integer + attribute :access, :boolean + attribute :og_image, :string + attribute :og_title, :string + attribute :og_description, :string + attribute :twitter_image, :string + attribute :twitter_title, :string + attribute :twitter_description, :string + attribute :meta_title, :string + attribute :meta_description, :string + attribute :email_subject, :string + attribute :send_email_when_published, :boolean + attribute :frontmatter, :string + attribute :show_title_and_feature_image, :boolean + attribute :visibility, :string + attribute :meta_description, :string + attribute :meta_title, :string + attribute :comments, :boolean + + attr_reader :primary_author, :primary_tag + + def authors + @authors ||= [] + end + + def authors=(attributes) + @authors = attributes.map do |author_attrs| + Author.new(author_attrs) end + end - tag = attrs["primary_tag"] - @primary_tag = tag.present? && Spooky::Tag.new(tag) + def tags + @tags ||= [] + end - @tags = (attrs["tags"] || []).map do |tag| - Spooky::Tag.new(tag) + def tags=(attributes) + @tags = attributes.map do |tag_attrs| + Tag.new(tag_attrs) end end + + def primary_author=(attributes) + @primary_author = Author.new(attributes) + end + + def primary_tag=(attributes) + @primary_tag = Tag.new(attributes) + end + + def to_s + title.to_s + end + + def to_param + slug.to_s + end end end diff --git a/lib/spooky/tag.rb b/lib/spooky/tag.rb index 14b7242..69fe202 100644 --- a/lib/spooky/tag.rb +++ b/lib/spooky/tag.rb @@ -1,17 +1,39 @@ +# frozen_string_literal: true + module Spooky - class Tag - ATTRIBUTES = [ - "description", - "feature_image", - "id", - "meta_description", - "meta_title", - "name", - "slug", - "url", - "visibility" - ].freeze + class Tag < Base + attribute :slug, :string + attribute :id, :immutable_string + attribute :name, :string + attribute :description, :string + attribute :feature_image, :string + attribute :visibility, :string + attribute :meta_title, :string + attribute :meta_description, :string + attribute :og_image, :string + attribute :og_title, :string + attribute :og_description, :string + attribute :twitter_image, :string + attribute :twitter_title, :string + attribute :twitter_description, :string + attribute :codeinjection_head, :string + attribute :codeinjection_foot, :string + attribute :canonical_url, :string + attribute :accent_color, :string + attribute :url, :string + + attr_reader :count + + def count=(attributes) + @count = Count.new(attributes) + end + + def to_s + name.to_s + end - include IsResource + def to_param + slug.to_s + end end end diff --git a/lib/spooky/version.rb b/lib/spooky/version.rb index 2ebebdb..1615384 100644 --- a/lib/spooky/version.rb +++ b/lib/spooky/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Spooky - VERSION = "1.1.0".freeze + VERSION = "2.0.0" end diff --git a/spec/authors_spec.rb b/spec/authors_spec.rb new file mode 100644 index 0000000..6f427a4 --- /dev/null +++ b/spec/authors_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "spec_helper" +require "fixtures" + +describe Spooky::Author do + let(:client) { Spooky::Client.new } + + it "returns an array of Authors" do + response = double("A parsed response", parse: FIXTURES[:authors]) + allow(HTTP).to receive(:get).and_return(response) + + authors, = client.authors + + expect(authors.all? { |p| p.is_a?(Spooky::Author) }).to be(true) + end + + it "returns the pagination information with a browse request" do + response = double("A parsed response", parse: FIXTURES[:authors]) + allow(HTTP).to receive(:get).and_return(response) + + _, pagination = client.authors + + expect(pagination).to be_present + expect(pagination.keys).to include("page", "limit", "pages", "total") + end + + it "returns a requested author by ID" do + response = double("A parsed response", parse: FIXTURES[:authors]) + allow(HTTP).to receive(:get).and_return(response) + + # This doesn't actually query by the ID. We are testing attribute creation here. + author = client.author_by(id: "1") + + expect(author.id).to eq("1") + expect(author.name).to eq("Georges Gabereau") + expect(author.slug).to eq("georges") + end + + it "returns an array of Authors with post count" do + response = double("A parsed response", parse: FIXTURES[:authors_with_count]) + allow(HTTP).to receive(:get).and_return(response) + + authors, = client.authors(include: "count.posts") + + expect(authors.first.id).to eq("1") + expect(authors.first.name).to eq("Georges Gabereau") + expect(authors.first.slug).to eq("georges") + expect(authors.first.count.posts).to eq(5) + end +end diff --git a/spec/client_spec.rb b/spec/client_spec.rb index fcac03a..b476f47 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" ENV["GHOST_API_URL"] = "https://spec.test" @@ -11,34 +13,79 @@ allow(HTTP).to receive(:get).and_return(response) end - it "gets all posts" do - expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/v3/content/posts/", { params: { key: "abc123" } }) - client.posts - end + describe "posts" do + it "gets all posts" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/posts/", { params: { key: "abc123" } }) + client.posts + end - it "gets all posts with tags and authors" do - expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/v3/content/posts/", { params: { key: "abc123", include: ["tags", "authors"] } }) - client.posts(tags: true, authors: true) - end + it "gets all posts with tags and authors" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/posts/", + { params: { key: "abc123", include: %w[tags authors] } }) + client.posts(include: %w[tags authors]) + end - it "gets a post by id" do - expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/v3/content/posts/99/", { params: { key: "abc123" } }) - client.post_by(id: 99) - end + it "gets a post by id" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/posts/99/", + { params: { key: "abc123" } }) + client.post_by(id: 99) + end + + it "gets all posts with tags and authors" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/posts/", + { params: { key: "abc123", include: %w[tags authors] } }) + client.posts(include: %w[tags authors]) + end - it "gets a post by slug" do - expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/v3/content/posts/slug/this-is-a-slug/", { params: { key: "abc123" } }) - client.post_by(slug: 'this-is-a-slug') + it "gets featured posts with hash filter" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/posts/", + { params: { key: "abc123", filter: "featured:true" } }) + client.posts(filter: { featured: true }) + end + + it "applies a string filter to the request" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/posts/", + { params: { key: "abc123", filter: "title:Welcome" } }) + client.posts(filter: "title:Welcome") + end end - it "gets featured posts with hash filter" do - expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/v3/content/posts/", { params: { key: "abc123", filter: "featured:true" } }) - client.posts(filter: { featured: true }) + describe "pages" do + it "gets all pages" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/pages/", { params: { key: "abc123" } }) + client.pages + end + + it "gets a page by id" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/pages/1/", + { params: { key: "abc123" } }) + client.page_by(id: 1) + end end - it "applies a string filter to the request" do - expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/v3/content/posts/", { params: { key: "abc123", filter: "title:Welcome" } }) - client.posts(filter: "title:Welcome") + describe "authors" do + it "gets all authors" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/authors/", { params: { key: "abc123" } }) + client.authors + end + + it "gets a author by id" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/authors/1/", + { params: { key: "abc123" } }) + client.author_by(id: 1) + end end + describe "tags" do + it "gets all tags" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/tags/", { params: { key: "abc123" } }) + client.tags + end + + it "gets a tag by id" do + expect(HTTP).to receive(:get).with("https://spec.test/ghost/api/content/tags/5f397fe0a185384852d1f144/", + { params: { key: "abc123" } }) + client.tag_by(id: "5f397fe0a185384852d1f144") + end + end end diff --git a/spec/fixtures.rb b/spec/fixtures.rb index 78b7237..7b7675a 100644 --- a/spec/fixtures.rb +++ b/spec/fixtures.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + FIXTURES = { simple_posts: JSON.parse( <<~'POST' @@ -254,7 +256,7 @@ POST ), posts_with_authors: JSON.parse( - <<~'POST' + <<~POST { "posts": [ { @@ -335,5 +337,359 @@ } } POST + ), + + pages: JSON.parse( + <<~PAGES + { + "pages": [ + { + "id": "67ed823ddf3cfc00077dbd50", + "uuid": "b84cd61f-2b0d-4851-9bd7-463d55205ba6", + "title": "About this site", + "slug": "about", + "html": "
About this site…
", + "comment_id": "67ed823ddf3cfc00077dbd50", + "feature_image": null, + "featured": false, + "visibility": "public", + "created_at": "2025-04-02T14:30:21.000-04:00", + "updated_at": "2025-04-03T10:16:44.000-04:00", + "published_at": "2025-04-02T14:30:21.000-04:00", + "custom_excerpt": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "custom_template": null, + "canonical_url": null, + "show_title_and_feature_image": true, + "url": "https://blog.spec.test/about/", + "excerpt": "This is our about page", + "reading_time": 1, + "access": true, + "comments": false, + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null, + "meta_title": null, + "meta_description": null, + "frontmatter": null, + "feature_image_alt": null, + "feature_image_caption": null + } + ], + "meta": { + "pagination": { + "page": 1, + "limit": 15, + "pages": 1, + "total": 1, + "next": null, + "prev": null + } + } + } + PAGES + ), + + pages_with_authors_and_tags: JSON.parse( + <<~PAGES + { + "pages": [ + { + "id": "67ed823ddf3cfc00077dbd50", + "uuid": "b84cd61f-2b0d-4851-9bd7-463d55205ba6", + "title": "About this site", + "slug": "about", + "html": "About this site…
", + "comment_id": "67ed823ddf3cfc00077dbd50", + "feature_image": null, + "featured": false, + "visibility": "public", + "created_at": "2025-04-02T14:30:21.000-04:00", + "updated_at": "2025-04-03T10:16:44.000-04:00", + "published_at": "2025-04-02T14:30:21.000-04:00", + "custom_excerpt": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "custom_template": null, + "canonical_url": null, + "show_title_and_feature_image": true, + "authors": [ + { + "id": "1", + "name": "Georges Gabereau", + "slug": "georges", + "profile_image": null, + "cover_image": null, + "bio": null, + "website": null, + "location": null, + "facebook": null, + "twitter": null, + "meta_title": null, + "meta_description": null, + "url": "https://blog.spec.test/author/georges/" + } + ], + "primary_author": { + "id": "1", + "name": "Georges Gabereau", + "slug": "georges", + "profile_image": null, + "cover_image": null, + "bio": null, + "website": null, + "location": null, + "facebook": null, + "twitter": null, + "meta_title": null, + "meta_description": null, + "url": "https://blog.spec.test/author/georges/" + }, + "tags": [ + { + "id": "5f397fe0a185384852d1f144", + "name": "Getting Started", + "slug": "getting-started", + "description": null, + "feature_image": null, + "visibility": "public", + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null, + "meta_title": null, + "meta_description": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "canonical_url": null, + "accent_color": null, + "url": "https://blog.spec.test/tag/getting-started/" + } + ], + "primary_tag": { + "id": "5f397fe0a185384852d1f144", + "name": "Getting Started", + "slug": "getting-started", + "description": null, + "feature_image": null, + "visibility": "public", + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null, + "meta_title": null, + "meta_description": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "canonical_url": null, + "accent_color": null, + "url": "https://blog.spec.test/tag/getting-started/" + }, + "url": "https://blog.spec.test/about/", + "excerpt": "This is our about page", + "reading_time": 1, + "access": true, + "comments": false, + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null, + "meta_title": null, + "meta_description": null, + "frontmatter": null, + "feature_image_alt": null, + "feature_image_caption": null + } + ], + "meta": { + "pagination": { + "page": 1, + "limit": 15, + "pages": 1, + "total": 1, + "next": null, + "prev": null + } + } + } + PAGES + ), + + authors: JSON.parse( + <<~AUTHORS + { + "authors": [ + { + "id": "1", + "name": "Georges Gabereau", + "slug": "georges", + "profile_image": null, + "cover_image": null, + "bio": null, + "website": null, + "location": null, + "facebook": null, + "twitter": null, + "meta_title": null, + "meta_description": null, + "threads": null, + "bluesky": null, + "mastodon": null, + "tiktok": null, + "youtube": null, + "instagram": null, + "linkedin": null, + "url": "https://blog.spec.test/author/georges/" + } + ], + "meta": { + "pagination": { + "page": 1, + "limit": 15, + "pages": 1, + "total": 5, + "next": null, + "prev": null + } + } + } + AUTHORS + ), + + authors_with_count: JSON.parse( + <<~AUTHORS + { + "authors": [ + { + "id": "1", + "name": "Georges Gabereau", + "slug": "georges", + "profile_image": null, + "cover_image": null, + "bio": null, + "website": null, + "location": null, + "facebook": null, + "twitter": null, + "meta_title": null, + "meta_description": null, + "threads": null, + "bluesky": null, + "mastodon": null, + "tiktok": null, + "youtube": null, + "instagram": null, + "linkedin": null, + "count": { + "posts": 5 + }, + "url": "https://blog.spec.test/author/georges/" + } + ], + "meta": { + "pagination": { + "page": 1, + "limit": 15, + "pages": 1, + "total": 5, + "next": null, + "prev": null + } + } + } + AUTHORS + ), + + tags: JSON.parse( + <<~TAGS + { + "tags": [ + { + "id": "5f397fe0a185384852d1f144", + "name": "Getting Started", + "slug": "getting-started", + "description": null, + "feature_image": null, + "visibility": "public", + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null, + "meta_title": null, + "meta_description": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "canonical_url": null, + "accent_color": null, + "url": "https://blog.spec.test.ghost.io/tag/getting-started/" + } + ], + "meta": { + "pagination": { + "page": 1, + "limit": 15, + "pages": 1, + "total": 1, + "next": null, + "prev": null + } + } + } + TAGS + ), + + tags_with_count: JSON.parse( + <<~TAGS + { + "tags": [ + { + "id": "5f397fe0a185384852d1f144", + "name": "Getting Started", + "slug": "getting-started", + "description": null, + "feature_image": null, + "visibility": "public", + "og_image": null, + "og_title": null, + "og_description": null, + "twitter_image": null, + "twitter_title": null, + "twitter_description": null, + "meta_title": null, + "meta_description": null, + "codeinjection_head": null, + "codeinjection_foot": null, + "canonical_url": null, + "accent_color": null, + "count": { + "posts": 5 + }, + "url": "https://blog.spec.test.ghost.io/tag/getting-started/" + } + ], + "meta": { + "pagination": { + "page": 1, + "limit": 15, + "pages": 1, + "total": 1, + "next": null, + "prev": null + } + } + } + TAGS ) }.freeze diff --git a/spec/pages_spec.rb b/spec/pages_spec.rb new file mode 100644 index 0000000..b6fc697 --- /dev/null +++ b/spec/pages_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "spec_helper" +require "fixtures" + +describe Spooky::Page do + let(:client) { Spooky::Client.new } + + it "returns an array of Pages" do + response = double("A parsed response", parse: FIXTURES[:pages]) + allow(HTTP).to receive(:get).and_return(response) + + pages, = client.pages + + expect(pages.all? { |p| p.is_a?(Spooky::Page) }).to be(true) + end + + it "returns the pagination information with a browse request" do + response = double("A parsed response", parse: FIXTURES[:pages]) + allow(HTTP).to receive(:get).and_return(response) + + _, pagination = client.pages + + expect(pagination).to be_present + expect(pagination.keys).to include("page", "limit", "pages", "total") + end + + it "converts nested tags to Spooky::Tag" do + response = double("A parsed response", parse: FIXTURES[:pages_with_authors_and_tags]) + allow(HTTP).to receive(:get).and_return(response) + + pages, = client.pages(include: %w[tags]) + page = pages.first + + expect(page.primary_tag.is_a?(Spooky::Tag)).to be(true) + expect(page.tags.all? { |p| p.is_a?(Spooky::Tag) }).to be(true) + end + + it "converts nested authors to Spooky::Author" do + response = double("A parsed response", parse: FIXTURES[:pages_with_authors_and_tags]) + allow(HTTP).to receive(:get).and_return(response) + + pages, = client.pages(include: %w[authors]) + page = pages.first + + expect(page.primary_author.is_a?(Spooky::Author)).to be(true) + expect(page.authors.all? { |p| p.is_a?(Spooky::Author) }).to be(true) + end + + it "returns a requested page by ID" do + response = double("A parsed response", parse: FIXTURES[:pages]) + allow(HTTP).to receive(:get).and_return(response) + + # This doesn't actually query by the ID. We are testing attribute creation here. + page = client.page_by(id: "5f397fe1a185384852d1f1a5") + + expect(page.id).to eq("67ed823ddf3cfc00077dbd50") + expect(page.title).to eq("About this site") + expect(page.slug).to eq("about") + end +end diff --git a/spec/posts_spec.rb b/spec/posts_spec.rb index e02e221..44bfd5a 100644 --- a/spec/posts_spec.rb +++ b/spec/posts_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "fixtures" @@ -8,7 +10,7 @@ response = double("A parsed response", parse: FIXTURES[:simple_posts]) allow(HTTP).to receive(:get).and_return(response) - posts, _ = client.posts + posts, = client.posts expect(posts.all? { |p| p.is_a?(Spooky::Post) }).to be(true) end @@ -27,7 +29,7 @@ response = double("A parsed response", parse: FIXTURES[:posts_with_tags]) allow(HTTP).to receive(:get).and_return(response) - posts, _ = client.posts(tags: true) + posts, = client.posts(include: %w[tags]) post = posts.first expect(post.primary_tag.is_a?(Spooky::Tag)).to be(true) @@ -38,7 +40,7 @@ response = double("A parsed response", parse: FIXTURES[:posts_with_authors]) allow(HTTP).to receive(:get).and_return(response) - posts, _ = client.posts(authors: true) + posts, = client.posts(include: %w[authors]) post = posts.first expect(post.primary_author.is_a?(Spooky::Author)).to be(true) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index abcfbb1..746e16a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + ENV["GHOST_API_URL"] = "https://spec.test" ENV["GHOST_CONTENT_API_KEY"] = "abc123" -$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) require "spooky" diff --git a/spec/spooky_spec.rb b/spec/spooky_spec.rb index 868915a..0142531 100644 --- a/spec/spooky_spec.rb +++ b/spec/spooky_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" describe Spooky do diff --git a/spec/tags_spec.rb b/spec/tags_spec.rb new file mode 100644 index 0000000..ac2d50f --- /dev/null +++ b/spec/tags_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "spec_helper" +require "fixtures" + +describe Spooky::Tag do + let(:client) { Spooky::Client.new } + + it "returns an array of Tags" do + response = double("A parsed response", parse: FIXTURES[:tags]) + allow(HTTP).to receive(:get).and_return(response) + + tags, = client.tags + + expect(tags.all? { |p| p.is_a?(Spooky::Tag) }).to be(true) + end + + it "returns the pagination information with a browse request" do + response = double("A parsed response", parse: FIXTURES[:tags]) + allow(HTTP).to receive(:get).and_return(response) + + _, pagination = client.tags + + expect(pagination).to be_present + expect(pagination.keys).to include("page", "limit", "pages", "total") + end + + it "returns a requested tag by ID" do + response = double("A parsed response", parse: FIXTURES[:tags]) + allow(HTTP).to receive(:get).and_return(response) + + # This doesn't actually query by the ID. We are testing attribute creation here. + tag = client.tag_by(id: "5f397fe0a185384852d1f144") + + expect(tag.id).to eq("5f397fe0a185384852d1f144") + expect(tag.name).to eq("Getting Started") + expect(tag.slug).to eq("getting-started") + end + + it "returns an array of Tags with post count" do + response = double("A parsed response", parse: FIXTURES[:tags_with_count]) + allow(HTTP).to receive(:get).and_return(response) + + tags, = client.tags(include: "count.posts") + + expect(tags.first.id).to eq("5f397fe0a185384852d1f144") + expect(tags.first.name).to eq("Getting Started") + expect(tags.first.slug).to eq("getting-started") + expect(tags.first.count.posts).to eq(5) + end +end diff --git a/spooky.gemspec b/spooky.gemspec index e613fb5..b1949cc 100644 --- a/spooky.gemspec +++ b/spooky.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "spooky/version" @@ -19,12 +21,14 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_runtime_dependency "activesupport", "~> 6.0" - spec.add_runtime_dependency "http", "~> 4.0" + spec.add_dependency "activemodel", ">= 6.0" + spec.add_dependency "activesupport", ">= 6.0" + spec.add_dependency "http", ">= 4.0" spec.add_development_dependency "bundler", "~> 2.0" spec.add_development_dependency "dotenv", "~> 2.7" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "rspec", "~> 3.9" spec.add_development_dependency "rubocop", "~> 0.89.0" + spec.metadata["rubygems_mfa_required"] = "true" end