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,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
class Configuration
|
5
|
+
# @return [String] Sanity's project id
|
6
|
+
attr_accessor :project_id
|
7
|
+
|
8
|
+
# @return [String] Sanity's dataset
|
9
|
+
attr_accessor :dataset
|
10
|
+
|
11
|
+
# @return [String] Sanity's api version
|
12
|
+
attr_accessor :api_version
|
13
|
+
|
14
|
+
# @return [String] Sanity's api token
|
15
|
+
attr_accessor :token
|
16
|
+
|
17
|
+
# @return [Boolean] whether to use Sanity's cdn api
|
18
|
+
attr_accessor :use_cdn
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@project_id = ""
|
22
|
+
@dataset = ""
|
23
|
+
@api_version = ""
|
24
|
+
@token = ""
|
25
|
+
@use_cdn = false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.configuration
|
30
|
+
@configuration ||= Configuration.new
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
alias_method :config, :configuration
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.configuration=(config)
|
38
|
+
@configuration = config
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.configure
|
42
|
+
yield configuration
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,111 @@
|
|
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 Groq
|
8
|
+
class Filter
|
9
|
+
class << self
|
10
|
+
def call(**args)
|
11
|
+
new(**args).call
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
START_PAREN = "("
|
16
|
+
END_PAREN = ")"
|
17
|
+
|
18
|
+
COMPARISON_OPERATORS = {
|
19
|
+
is: "==",
|
20
|
+
not: "!=",
|
21
|
+
gt: ">",
|
22
|
+
gt_eq: ">=",
|
23
|
+
lt: "<",
|
24
|
+
lt_eq: "<=",
|
25
|
+
match: "match"
|
26
|
+
}
|
27
|
+
|
28
|
+
LOGICAL_OPERATORS = {
|
29
|
+
and: "&&",
|
30
|
+
or: "||"
|
31
|
+
}
|
32
|
+
|
33
|
+
RESERVED = COMPARISON_OPERATORS.keys | LOGICAL_OPERATORS.keys
|
34
|
+
|
35
|
+
attr_reader :args, :filter_value
|
36
|
+
|
37
|
+
def initialize(**args)
|
38
|
+
@args = args.except(*Sanity::Groqify::RESERVED - RESERVED)
|
39
|
+
@filter_value = +""
|
40
|
+
end
|
41
|
+
|
42
|
+
def call
|
43
|
+
iterate
|
44
|
+
filter_value.strip
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def cast_value(val)
|
50
|
+
val.is_a?(Integer) ? val : "'#{val}'"
|
51
|
+
end
|
52
|
+
|
53
|
+
def default_multi_filter
|
54
|
+
filter_value.length.positive? ? " #{LOGICAL_OPERATORS[:and]}" : ""
|
55
|
+
end
|
56
|
+
|
57
|
+
def equal
|
58
|
+
COMPARISON_OPERATORS[:is]
|
59
|
+
end
|
60
|
+
|
61
|
+
def filter(key: nil)
|
62
|
+
key ? " #{multi_filter(key)}" : default_multi_filter.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def iterate(arg = args, nested_key: nil)
|
66
|
+
arg.each do |key, val|
|
67
|
+
if val.is_a?(String) || val.is_a?(Integer)
|
68
|
+
filter_value << "#{filter(key: nested_key)} #{key} #{equal} #{cast_value(val)}"
|
69
|
+
elsif val.is_a?(Array) && !val[0].is_a?(Hash)
|
70
|
+
filter_value << "#{key} in #{val.map(&:to_s)}"
|
71
|
+
elsif LOGICAL_OPERATORS.key?(key)
|
72
|
+
if val.is_a?(Array)
|
73
|
+
val.each { |hsh| iterate(hsh, nested_key: key) }
|
74
|
+
elsif LOGICAL_OPERATORS.key?(val.keys[0])
|
75
|
+
filter_value << " #{LOGICAL_OPERATORS[key]} #{START_PAREN}"
|
76
|
+
|
77
|
+
val.values[0].each_with_index do |(vkey, vval), idx|
|
78
|
+
operator = logical_operator(val.keys[0], index: idx)
|
79
|
+
filter_value << "#{operator} " unless operator.empty?
|
80
|
+
|
81
|
+
if vkey.is_a?(Hash)
|
82
|
+
vkey.each do |vvkey, vvval|
|
83
|
+
filter_value << "#{vvkey} #{equal} #{cast_value(vvval)}"
|
84
|
+
end
|
85
|
+
else
|
86
|
+
filter_value << "#{vkey} #{equal} #{cast_value(vval)}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
filter_value << END_PAREN
|
91
|
+
else
|
92
|
+
iterate(val, nested_key: key)
|
93
|
+
end
|
94
|
+
elsif COMPARISON_OPERATORS.key?(val.keys[0])
|
95
|
+
val.each do |vkey, vval|
|
96
|
+
filter_value << "#{filter(key: nested_key)} #{key} #{COMPARISON_OPERATORS[vkey]} #{cast_value(vval)}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def logical_operator(key, index: 0)
|
103
|
+
index.positive? ? " #{LOGICAL_OPERATORS[key]}" : ""
|
104
|
+
end
|
105
|
+
|
106
|
+
def multi_filter(key)
|
107
|
+
filter_value.length.positive? ? LOGICAL_OPERATORS[key] : ""
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
module Groq
|
5
|
+
class Order
|
6
|
+
class << self
|
7
|
+
def call(**args)
|
8
|
+
new(**args).call
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
RESERVED = %i[order]
|
13
|
+
|
14
|
+
attr_reader :order, :val
|
15
|
+
|
16
|
+
def initialize(**args)
|
17
|
+
args.slice(*RESERVED).then do |opts|
|
18
|
+
@order = opts[:order]
|
19
|
+
end
|
20
|
+
|
21
|
+
@val = +""
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
return unless order
|
26
|
+
|
27
|
+
raise ArgumentError, "order must be hash" unless order.is_a?(Hash)
|
28
|
+
|
29
|
+
order.to_a.each_with_index do |(key, sort), idx|
|
30
|
+
val << " | order(#{key} #{sort})".then do |str|
|
31
|
+
idx.positive? ? str : str.strip
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
val
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
using Sanity::Refinements::Arrays
|
4
|
+
|
5
|
+
module Sanity
|
6
|
+
module Groq
|
7
|
+
class Select
|
8
|
+
class << self
|
9
|
+
def call(**args)
|
10
|
+
new(**args).call
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
RESERVED = %i[select]
|
15
|
+
|
16
|
+
attr_reader :select, :val
|
17
|
+
|
18
|
+
def initialize(**args)
|
19
|
+
args.slice(*RESERVED).then do |opts|
|
20
|
+
@select = opts[:select]
|
21
|
+
end
|
22
|
+
|
23
|
+
@val = +""
|
24
|
+
end
|
25
|
+
|
26
|
+
def call
|
27
|
+
return unless select
|
28
|
+
|
29
|
+
Array.wrap(select).each_with_index do |x, idx|
|
30
|
+
val << "#{idx.positive? ? "," : ""} #{x}"
|
31
|
+
end
|
32
|
+
|
33
|
+
"{ #{val.strip} }"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
module Groq
|
5
|
+
class Slice
|
6
|
+
class << self
|
7
|
+
def call(**args)
|
8
|
+
new(**args).call
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
RESERVED = %i[limit offset]
|
13
|
+
ZERO_INDEX = 0
|
14
|
+
|
15
|
+
attr_reader :limit, :offset
|
16
|
+
|
17
|
+
def initialize(**args)
|
18
|
+
args.slice(*RESERVED).then do |opts|
|
19
|
+
@limit = opts[:limit]
|
20
|
+
@offset = opts[:offset]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
return "" unless limit
|
26
|
+
|
27
|
+
!offset ? zero_index_to_limit : offset_to_limit
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def offset_to_limit
|
33
|
+
"[#{offset}...#{limit + offset}]"
|
34
|
+
end
|
35
|
+
|
36
|
+
def zero_index_to_limit
|
37
|
+
"[#{ZERO_INDEX}...#{limit}]"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sanity/groq/filter"
|
4
|
+
require "sanity/groq/order"
|
5
|
+
require "sanity/groq/slice"
|
6
|
+
require "sanity/groq/select"
|
7
|
+
|
8
|
+
module Sanity
|
9
|
+
class Groqify
|
10
|
+
class << self
|
11
|
+
def call(**args)
|
12
|
+
new(**args).call
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
RESERVED = Sanity::Groq::Filter::RESERVED |
|
17
|
+
Sanity::Groq::Slice::RESERVED |
|
18
|
+
Sanity::Groq::Order::RESERVED |
|
19
|
+
Sanity::Groq::Select::RESERVED
|
20
|
+
|
21
|
+
attr_reader :args
|
22
|
+
|
23
|
+
def initialize(**args)
|
24
|
+
@args = args
|
25
|
+
end
|
26
|
+
|
27
|
+
def call
|
28
|
+
"*[#{filter}] #{order} #{slice} #{select}".strip
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def order
|
34
|
+
Sanity::Groq::Order.call(**args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def select
|
38
|
+
Sanity::Groq::Select.call(**args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def slice
|
42
|
+
Sanity::Groq::Slice.call(**args)
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter
|
46
|
+
Sanity::Groq::Filter.call(**args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/sanity/http.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "net/http"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
require "sanity/http/mutation"
|
8
|
+
require "sanity/http/query"
|
9
|
+
|
10
|
+
require "sanity/http/create"
|
11
|
+
require "sanity/http/create_if_not_exists"
|
12
|
+
require "sanity/http/create_or_replace"
|
13
|
+
require "sanity/http/delete"
|
14
|
+
require "sanity/http/patch"
|
15
|
+
|
16
|
+
require "sanity/http/find"
|
17
|
+
require "sanity/http/where"
|
18
|
+
|
19
|
+
require "sanity/http/results"
|
20
|
+
|
21
|
+
module Sanity
|
22
|
+
module Http
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sanity
|
4
|
+
module Http
|
5
|
+
class Find
|
6
|
+
include Sanity::Http::Query
|
7
|
+
delegate find_api_endpoint: :resource_klass
|
8
|
+
alias_method :api_endpoint, :find_api_endpoint
|
9
|
+
|
10
|
+
attr_reader :id
|
11
|
+
|
12
|
+
def initialize(**args)
|
13
|
+
super
|
14
|
+
@id = args.delete(:id)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def uri
|
20
|
+
URI("#{base_url}/#{id}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
using Sanity::Refinements::Strings
|
4
|
+
using Sanity::Refinements::Arrays
|
5
|
+
|
6
|
+
module Sanity
|
7
|
+
module Http
|
8
|
+
module Mutation
|
9
|
+
class << self
|
10
|
+
def included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
base.extend(Forwardable)
|
13
|
+
base.delegate(%i[project_id api_version dataset token] => :"Sanity.config")
|
14
|
+
base.delegate(mutatable_api_endpoint: :resource_klass)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def call(**args)
|
20
|
+
new(**args).call
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# See https://www.sanity.io/docs/http-mutations#visibility-937bc4250c79
|
25
|
+
ALLOWED_VISIBILITY = %i[sync async deferred]
|
26
|
+
|
27
|
+
# See https://www.sanity.io/docs/http-mutations#aa493b1c2524
|
28
|
+
REQUEST_KEY = "mutations"
|
29
|
+
|
30
|
+
# See https://www.sanity.io/docs/http-mutations#952b77deb110
|
31
|
+
DEFAULT_QUERY_PARAMS = {
|
32
|
+
return_ids: false,
|
33
|
+
return_documents: false,
|
34
|
+
visibility: :sync
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
attr_reader :options, :params, :resource_klass, :query_set, :result_wrapper
|
38
|
+
|
39
|
+
def initialize(**args)
|
40
|
+
@resource_klass = args.delete(:resource_klass)
|
41
|
+
@params = args.delete(:params)
|
42
|
+
@query_set = Set.new
|
43
|
+
@result_wrapper = args.delete(:result_wrapper) || Sanity::Http::Results
|
44
|
+
|
45
|
+
raise ArgumentError, "resource_klass must be defined" unless resource_klass
|
46
|
+
raise ArgumentError, "params argument is missing" unless params
|
47
|
+
|
48
|
+
(args.delete(:options) || {}).then do |opts|
|
49
|
+
DEFAULT_QUERY_PARAMS.keys.each do |qup|
|
50
|
+
query_set << [qup, opts.fetch(qup, DEFAULT_QUERY_PARAMS[qup])]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
raise ArgumentError, "visibility argument must be one of #{ALLOWED_VISIBILITY}" unless valid_invisibility?
|
54
|
+
end
|
55
|
+
|
56
|
+
def body_key
|
57
|
+
self.class.name.demodulize.underscore.camelize_lower
|
58
|
+
end
|
59
|
+
|
60
|
+
def call
|
61
|
+
Net::HTTP.post(uri, {"#{REQUEST_KEY}": body}.to_json, headers).then do |result|
|
62
|
+
block_given? ? yield(result_wrapper.call(result)) : result_wrapper.call(result)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def base_url
|
69
|
+
"https://#{project_id}.api.sanity.io/#{api_version}/#{mutatable_api_endpoint}/#{dataset}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def body
|
73
|
+
return Array.wrap({"#{body_key}": params}) if params.is_a?(Hash)
|
74
|
+
|
75
|
+
Array.wrap(params.map { |pam| {"#{body_key}": pam} })
|
76
|
+
end
|
77
|
+
|
78
|
+
def camelize_query_set
|
79
|
+
query_set.to_h.transform_keys do |key|
|
80
|
+
key.to_s.camelize_lower
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def headers
|
85
|
+
{
|
86
|
+
"Content-Type": "application/json",
|
87
|
+
Authorization: "Bearer #{token}"
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def query_params
|
92
|
+
camelize_query_set.map do |key, val|
|
93
|
+
"#{key}=#{val}"
|
94
|
+
end.join("&")
|
95
|
+
end
|
96
|
+
|
97
|
+
def uri
|
98
|
+
URI(base_url).tap do |obj|
|
99
|
+
obj.query = query_params
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def valid_invisibility?
|
104
|
+
ALLOWED_VISIBILITY.include? query_set.to_h[:visibility]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|