sanity-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +9 -0
- data/.standard.yml +2 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +203 -0
- data/Rakefile +24 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/bin/standardrb +29 -0
- data/lib/sanity.rb +21 -0
- data/lib/sanity/attributable.rb +66 -0
- data/lib/sanity/configuration.rb +44 -0
- data/lib/sanity/groq/filter.rb +111 -0
- data/lib/sanity/groq/order.rb +39 -0
- data/lib/sanity/groq/select.rb +37 -0
- data/lib/sanity/groq/slice.rb +41 -0
- data/lib/sanity/groqify.rb +49 -0
- data/lib/sanity/http.rb +24 -0
- data/lib/sanity/http/create.rb +9 -0
- data/lib/sanity/http/create_if_not_exists.rb +9 -0
- data/lib/sanity/http/create_or_replace.rb +9 -0
- data/lib/sanity/http/delete.rb +9 -0
- data/lib/sanity/http/find.rb +24 -0
- data/lib/sanity/http/mutation.rb +108 -0
- data/lib/sanity/http/patch.rb +9 -0
- data/lib/sanity/http/query.rb +74 -0
- data/lib/sanity/http/results.rb +26 -0
- data/lib/sanity/http/where.rb +59 -0
- data/lib/sanity/mutatable.rb +56 -0
- data/lib/sanity/queryable.rb +50 -0
- data/lib/sanity/refinements.rb +5 -0
- data/lib/sanity/refinements/arrays.rb +30 -0
- data/lib/sanity/refinements/hashes.rb +20 -0
- data/lib/sanity/refinements/strings.rb +38 -0
- data/lib/sanity/resource.rb +30 -0
- data/lib/sanity/resources.rb +4 -0
- data/lib/sanity/resources/asset.rb +7 -0
- data/lib/sanity/resources/document.rb +25 -0
- data/lib/sanity/version.rb +5 -0
- data/sanity.gemspec +29 -0
- metadata +86 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
|
5
|
+
using Sanity::Refinements::Strings
|
6
|
+
|
7
|
+
module Sanity
|
8
|
+
module Http
|
9
|
+
module Query
|
10
|
+
class << self
|
11
|
+
def included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
base.extend(Forwardable)
|
14
|
+
base.delegate(%i[project_id api_version dataset token] => :"Sanity.config")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def call(**args)
|
20
|
+
new(**args).call
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :resource_klass, :result_wrapper
|
25
|
+
|
26
|
+
# @todo Add query support
|
27
|
+
def initialize(**args)
|
28
|
+
@resource_klass = args.delete(:resource_klass)
|
29
|
+
@result_wrapper = args.delete(:result_wrapper) || Sanity::Http::Results
|
30
|
+
end
|
31
|
+
|
32
|
+
# @todo Add query support
|
33
|
+
def call
|
34
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
35
|
+
|
36
|
+
http.use_ssl = uri.scheme == "https"
|
37
|
+
|
38
|
+
request = Module.const_get("Net::HTTP::#{method.to_s.classify}").new(uri, headers)
|
39
|
+
|
40
|
+
request.body = request_body
|
41
|
+
|
42
|
+
http.request(request).then do |result|
|
43
|
+
data = JSON.parse(result.body)
|
44
|
+
|
45
|
+
block_given? ? yield(result_wrapper.call(data)) : result_wrapper.call(data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def request_body
|
52
|
+
end
|
53
|
+
|
54
|
+
def method
|
55
|
+
:get
|
56
|
+
end
|
57
|
+
|
58
|
+
def base_url
|
59
|
+
"https://#{project_id}.api.sanity.io/#{api_version}/#{api_endpoint}/#{dataset}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def headers
|
63
|
+
{
|
64
|
+
"Content-Type": "application/json",
|
65
|
+
Authorization: "Bearer #{token}"
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def uri
|
70
|
+
URI(base_url)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
module Http
|
5
|
+
class Results
|
6
|
+
class << self
|
7
|
+
def call(result)
|
8
|
+
new(result).call
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :raw_result
|
13
|
+
|
14
|
+
def initialize(result)
|
15
|
+
@raw_result = result
|
16
|
+
end
|
17
|
+
|
18
|
+
# TODO: parse the JSON and return what the user asked for
|
19
|
+
# whether that just be the response, the document ids, or the
|
20
|
+
# the document object(s) the user mutated
|
21
|
+
def call
|
22
|
+
raw_result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Hash#except! was added in Ruby 3
|
4
|
+
using Sanity::Refinements::Hashes if RUBY_VERSION.to_i < 3.0
|
5
|
+
|
6
|
+
module Sanity
|
7
|
+
module Http
|
8
|
+
class Where
|
9
|
+
include Sanity::Http::Query
|
10
|
+
delegate where_api_endpoint: :resource_klass
|
11
|
+
alias_method :api_endpoint, :where_api_endpoint
|
12
|
+
|
13
|
+
attr_reader :groq, :use_post, :groq_attributes, :variables
|
14
|
+
|
15
|
+
def initialize(**args)
|
16
|
+
super
|
17
|
+
@groq = args.delete(:groq) || ""
|
18
|
+
@variables = args.delete(:variables) || {}
|
19
|
+
@use_post = args.delete(:use_post) || false
|
20
|
+
|
21
|
+
@groq_attributes = args.except(:groq, :use_post, :resource_klass, :result_wrapper)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def method
|
27
|
+
use_post ? :post : :get
|
28
|
+
end
|
29
|
+
|
30
|
+
def uri
|
31
|
+
super.tap do |obj|
|
32
|
+
obj.query = URI.encode_www_form(query_and_variables) unless use_post
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def query_and_variables
|
37
|
+
if use_post
|
38
|
+
{params: variables}
|
39
|
+
else
|
40
|
+
{}.tap do |hash|
|
41
|
+
variables.each do |key, value|
|
42
|
+
hash["$#{key}"] = "\"#{value}\""
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end.merge(query: groq_query)
|
46
|
+
end
|
47
|
+
|
48
|
+
def groq_query
|
49
|
+
groq.empty? ? Sanity::Groqify.call(**groq_attributes) : groq
|
50
|
+
end
|
51
|
+
|
52
|
+
def request_body
|
53
|
+
return unless use_post
|
54
|
+
|
55
|
+
query_and_variables.to_json
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
# Mutatable is responsible for setting the appropriate class methods
|
5
|
+
# that invoke Sanity::Http's mutatable classes
|
6
|
+
#
|
7
|
+
# The mutatable marco can limit what queries are accessible to the
|
8
|
+
# mutatable object
|
9
|
+
#
|
10
|
+
# @example provides default class methods
|
11
|
+
# mutatable
|
12
|
+
#
|
13
|
+
# @example only add the `.create` method
|
14
|
+
# mutatable only: %i(create)
|
15
|
+
#
|
16
|
+
# @example only add the `.create_or_replace`& `#create_or_replace` methods
|
17
|
+
# mutatable only: %i(create_or_replace)
|
18
|
+
#
|
19
|
+
using Sanity::Refinements::Strings
|
20
|
+
|
21
|
+
module Mutatable
|
22
|
+
class << self
|
23
|
+
def included(base)
|
24
|
+
base.extend(ClassMethods)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
DEFAULT_KLASS_MUTATIONS = %i[create create_or_replace create_if_not_exists patch delete].freeze
|
30
|
+
DEFAULT_INSTANCE_MUTATIONS = %i[create create_or_replace create_if_not_exists delete].freeze
|
31
|
+
ALL_MUTATIONS = DEFAULT_KLASS_MUTATIONS | DEFAULT_INSTANCE_MUTATIONS
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def mutatable(**options)
|
36
|
+
options.fetch(:only, ALL_MUTATIONS).each do |mutation|
|
37
|
+
if DEFAULT_KLASS_MUTATIONS.include? mutation.to_sym
|
38
|
+
define_singleton_method(mutation) do |**args|
|
39
|
+
Module.const_get("Sanity::Http::#{mutation.to_s.classify}").call(**args.merge(resource_klass: self))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
if DEFAULT_INSTANCE_MUTATIONS.include? mutation.to_sym
|
44
|
+
define_method(mutation) do |**args|
|
45
|
+
Module.const_get("Sanity::Http::#{mutation.to_s.classify}").call(
|
46
|
+
**args.merge(params: attributes, resource_klass: self.class)
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
define_singleton_method("mutatable_api_endpoint") { options.fetch(:api_endpoint, "") }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
# Queryable is responsible for setting the appropriate class methods
|
5
|
+
# that invoke Sanity::Http's query classes
|
6
|
+
#
|
7
|
+
# The queryable marco can limit what queries are accessible to the
|
8
|
+
# queryable object
|
9
|
+
#
|
10
|
+
# @example provides default class methods
|
11
|
+
# queryable
|
12
|
+
#
|
13
|
+
# @example only add the `.where` method
|
14
|
+
# queryable only: %i(where)
|
15
|
+
#
|
16
|
+
# @example only add the `.find` method
|
17
|
+
# queryable only: %i(find)
|
18
|
+
#
|
19
|
+
using Sanity::Refinements::Strings
|
20
|
+
|
21
|
+
module Queryable
|
22
|
+
class << self
|
23
|
+
def included(base)
|
24
|
+
base.extend(ClassMethods)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
DEFAULT_KLASS_QUERIES = %i[find where].freeze
|
30
|
+
|
31
|
+
# See https://www.sanity.io/docs/http-query & https://www.sanity.io/docs/http-doc
|
32
|
+
QUERY_ENDPOINTS = {
|
33
|
+
find: "data/doc",
|
34
|
+
where: "data/query"
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# @private
|
40
|
+
def queryable(**options)
|
41
|
+
options.fetch(:only, DEFAULT_KLASS_QUERIES).each do |query|
|
42
|
+
define_singleton_method(query) do |**args|
|
43
|
+
Module.const_get("Sanity::Http::#{query.to_s.classify}").call(**args.merge(resource_klass: self))
|
44
|
+
end
|
45
|
+
define_singleton_method("#{query}_api_endpoint") { QUERY_ENDPOINTS[query] }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
module Refinements
|
5
|
+
# Array refinements based on ActiveSupport methods.
|
6
|
+
#
|
7
|
+
# Using refinements as to:
|
8
|
+
# 1) not pollute the global namespace
|
9
|
+
# 2) not conflict with ActiveSupport in a Rails based project
|
10
|
+
#
|
11
|
+
# These methods are defined in the way as needed in this gem. They
|
12
|
+
# are not meant to replace the more robust ActiveSupport methods.
|
13
|
+
#
|
14
|
+
# Defining these here, removes the need for adding ActiveSupport
|
15
|
+
# as a dependency
|
16
|
+
module Arrays
|
17
|
+
refine Array.singleton_class do
|
18
|
+
def wrap(object)
|
19
|
+
if object.nil?
|
20
|
+
[]
|
21
|
+
elsif object.respond_to?(:to_ary)
|
22
|
+
object.to_ary || [object]
|
23
|
+
else
|
24
|
+
[object]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
module Refinements
|
5
|
+
module Hashes
|
6
|
+
refine Hash do
|
7
|
+
# Defined in Ruby >= 3
|
8
|
+
def except!(*keys)
|
9
|
+
keys.each { |key| delete(key) }
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
# Defined in Ruby >= 3
|
14
|
+
def except(*keys)
|
15
|
+
dup.except!(*keys)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
module Refinements
|
5
|
+
# String refinements based on ActiveSupport methods.
|
6
|
+
#
|
7
|
+
# Using refinements as to:
|
8
|
+
# 1) not pollute the global namespace
|
9
|
+
# 2) not conflict with ActiveSupport in a Rails based project
|
10
|
+
#
|
11
|
+
# These methods are defined in the way as needed in this gem. They
|
12
|
+
# are not meant to replace the more robust ActiveSupport methods.
|
13
|
+
#
|
14
|
+
# Defining these here, removes the need for adding ActiveSupport
|
15
|
+
# as a dependency
|
16
|
+
module Strings
|
17
|
+
refine String do
|
18
|
+
def camelize_lower
|
19
|
+
split("_")[0..].each_with_index.map do |val, idx|
|
20
|
+
idx != 0 ? val.capitalize : val
|
21
|
+
end.join
|
22
|
+
end
|
23
|
+
|
24
|
+
def classify
|
25
|
+
split("_").map(&:capitalize).join
|
26
|
+
end
|
27
|
+
|
28
|
+
def demodulize
|
29
|
+
split("::")[-1]
|
30
|
+
end
|
31
|
+
|
32
|
+
def underscore
|
33
|
+
split(/(?=[A-Z])/).map(&:downcase).join("_")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
# Sanity::Resource is the base class used by
|
5
|
+
# the sanity resources defined within this gem.
|
6
|
+
#
|
7
|
+
# Out of the box it includes the following mixins:
|
8
|
+
# Sanity::Attributable
|
9
|
+
# Sanity::Mutatable
|
10
|
+
# Sanity::Queryable
|
11
|
+
#
|
12
|
+
# Sanity::Document and Sanity::Asset both inherit
|
13
|
+
# from Sanity::Resource
|
14
|
+
#
|
15
|
+
# Any PORO in your project could become a
|
16
|
+
# Sanity::Resource via inheritance
|
17
|
+
#
|
18
|
+
# @example inherit from Sanity::Resource
|
19
|
+
# class User < Sanity::Resource
|
20
|
+
# attribute :first_name, default: ""
|
21
|
+
# queryable
|
22
|
+
# mutatable
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
class Resource
|
26
|
+
include Sanity::Attributable
|
27
|
+
include Sanity::Mutatable
|
28
|
+
include Sanity::Queryable
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
# Sanity::Document is the core resource for interacting
|
5
|
+
# with Sanity's HTTP API. This class provides out of
|
6
|
+
# the box query and mutation methods for interacting
|
7
|
+
# with the API.
|
8
|
+
#
|
9
|
+
# @example create a new document object in memory
|
10
|
+
# Sanity::Document.new(_id: 1, _type: "post")
|
11
|
+
#
|
12
|
+
# @example invoke the api operations to create a document
|
13
|
+
# Sanity::Document.create(params: {_type: "post", title: "A new blog post"})
|
14
|
+
#
|
15
|
+
# @example invoke the api operations to delete a document
|
16
|
+
# Sanity::Document.delete(params: {id: "1234"})
|
17
|
+
#
|
18
|
+
class Document < Sanity::Resource
|
19
|
+
attribute :_id, default: ""
|
20
|
+
attribute :_type, default: ""
|
21
|
+
# See https://www.sanity.io/docs/http-mutations#ac77879076d4
|
22
|
+
mutatable api_endpoint: "data/mutate"
|
23
|
+
queryable
|
24
|
+
end
|
25
|
+
end
|