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.
@@ -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
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Http
5
+ class Create
6
+ include Sanity::Http::Mutation
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Http
5
+ class CreateIfNotExists
6
+ include Sanity::Http::Mutation
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Http
5
+ class CreateOrReplace
6
+ include Sanity::Http::Mutation
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sanity
4
+ module Http
5
+ class Delete
6
+ include Sanity::Http::Mutation
7
+ end
8
+ end
9
+ 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