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,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
|