sanity-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|