sanity-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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