strapi_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,95 @@
1
+ require "faraday"
2
+ require "json"
3
+
4
+ module StrapiRuby
5
+ class Client
6
+ attr_reader :connection
7
+
8
+ def initialize(options = {}, &block)
9
+ @connection = set_connection(options, &block)
10
+ end
11
+
12
+ def get(endpoint)
13
+ response = performs_request { @connection.get(endpoint) }
14
+ handle_response(response)
15
+ end
16
+
17
+ def post(endpoint, body)
18
+ response = performs_request { @connection.post(endpoint, build_data_payload(body)) }
19
+ handle_response(response)
20
+ end
21
+
22
+ def put(endpoint, body)
23
+ response = performs_request { @connection.put(endpoint, build_data_payload(body)) }
24
+ handle_response(response)
25
+ end
26
+
27
+ def delete(endpoint)
28
+ response = performs_request { @connection.delete(endpoint) }
29
+ handle_response(response)
30
+ end
31
+
32
+ private
33
+
34
+ def set_connection(options, &block)
35
+ url = options[:strapi_server_uri]
36
+ strapi_token = options[:strapi_token]
37
+
38
+ default_headers = { "Content-Type" => "application/json",
39
+ "Authorization" => "Bearer #{strapi_token}",
40
+ "User-Agent" => "StrapiRuby/#{StrapiRuby::VERSION}" }
41
+
42
+ Faraday.new(url: url) do |faraday|
43
+ faraday.request :url_encoded
44
+ faraday.adapter Faraday.default_adapter
45
+ block&.call(faraday)
46
+ faraday.headers = default_headers.merge(faraday.headers)
47
+ end
48
+ end
49
+
50
+ def performs_request
51
+ begin
52
+ yield
53
+ rescue Faraday::ConnectionFailed => e
54
+ raise ConnectionError, "#{ErrorMessage.connection_failed} #{e.message}"
55
+ rescue Faraday::TimeoutError => e
56
+ raise ConnectionError, "#{ErrorMessage.timeout} #{e.message}"
57
+ rescue StandardError => e
58
+ raise ConnectionError, "#{ErrorMessage.unexpected} #{e.message}"
59
+ end
60
+ end
61
+
62
+ def convert_json_to_open_struct(json)
63
+ JSON.parse(json, object_class: OpenStruct)
64
+ rescue JSON::ParserError => e
65
+ raise JSONParsingError, e.message
66
+ end
67
+
68
+ def build_data_payload(body)
69
+ { data: body }.to_json
70
+ end
71
+
72
+ # rubocop:disable Metrics/AbcSize
73
+ def handle_response(response)
74
+ body = convert_json_to_open_struct(response.body)
75
+ case response.status
76
+ when 200
77
+ body
78
+ when 400
79
+ raise BadRequestError.new(body.error.message, response.status)
80
+ when 401
81
+ raise UnauthorizedError.new(body.error.message, response.status)
82
+ when 403
83
+ raise ForbiddenError.new(body.error.message, response.status)
84
+ when 404
85
+ raise NotFoundError.new(body.error.message, response.status)
86
+ when 422
87
+ raise UnprocessableEntityError.new(body.error.message, response.status)
88
+ when 500..599
89
+ raise ServerError.new(body.error.message, response.status)
90
+ end
91
+ end
92
+
93
+ # rubocop:enable Metrics/AbcSize
94
+ end
95
+ end
@@ -0,0 +1,27 @@
1
+ module StrapiRuby
2
+ class Config
3
+ include StrapiRuby::Validations
4
+
5
+ attr_accessor :strapi_server_uri,
6
+ :strapi_token,
7
+ :faraday,
8
+ :convert_to_html,
9
+ :convert_to_datetime,
10
+ :show_endpoint
11
+
12
+ def initialize
13
+ @strapi_server_uri = nil
14
+ @strapi_token = nil
15
+ @faraday = nil
16
+ @convert_to_datetime = true
17
+ @convert_to_html = []
18
+ @show_endpoint = false
19
+ end
20
+
21
+ def call
22
+ validate_config(self)
23
+ # We convert to symbols if user passed strings for convert_to_html options
24
+ @convert_to_html.map!(&:to_sym)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module StrapiRuby
2
+ module Configuration
3
+ def config
4
+ @config ||= Config.new
5
+ end
6
+
7
+ def configure
8
+ yield(config)
9
+ config.call
10
+ client
11
+ end
12
+
13
+ private
14
+
15
+ def client
16
+ @client ||= Client.new(
17
+ strapi_server_uri: @config.strapi_server_uri, strapi_token: @config.strapi_token, &@config.faraday
18
+ )
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ module StrapiRuby
2
+ module Endpoint
3
+ class Builder
4
+ def initialize(options = {})
5
+ @resource = options[:resource]
6
+ @id = options[:id]
7
+ @query = Query.new(options).call
8
+ @result = nil
9
+ end
10
+
11
+ def call
12
+ build_endpoint
13
+ append_query
14
+ @result
15
+ end
16
+
17
+ private
18
+
19
+ def build_endpoint
20
+ @result = if builds_collection?
21
+ "#{base_uri}/#{@resource}/#{@id}"
22
+ else
23
+ "#{base_uri}/#{@resource}"
24
+ end
25
+ end
26
+
27
+ def append_query
28
+ @result += @query if @query
29
+ end
30
+
31
+ def builds_collection?
32
+ !@id.nil?
33
+ end
34
+
35
+ def base_uri
36
+ StrapiRuby.config.strapi_server_uri
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ module StrapiRuby
2
+ module Endpoint
3
+ class Query
4
+ include StrapiParameters
5
+
6
+ def initialize(options = {})
7
+ @result = ""
8
+ @options = options
9
+ parse_query_params
10
+ end
11
+
12
+ def call
13
+ if @options[:raw]
14
+ @result = @options[:raw]
15
+ else
16
+ @result
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def parse_query_params
23
+ return if @options[:raw]
24
+
25
+ @options.each do |key, value|
26
+ send(key, value) if @options[key] && respond_to?(key, true)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,132 @@
1
+ require "uri"
2
+
3
+ module StrapiRuby
4
+ module Endpoint
5
+ module StrapiParameters
6
+ private
7
+
8
+ def sort(args)
9
+ build_query_from_args(args, :sort)
10
+ end
11
+
12
+ def fields(args)
13
+ build_query_from_args(args, :fields)
14
+ end
15
+
16
+ def populate(args)
17
+ build_query_from_args(args, :populate)
18
+ end
19
+
20
+ def filters(args)
21
+ build_query_from_args(args, :filters)
22
+ end
23
+
24
+ def page_size(number)
25
+ raise TypeError, "#{ErrorMessage.expected_integer} Got #{number.class.name}" unless number.is_a?(Integer)
26
+
27
+ check_single_pagination
28
+ @result += "#{prefix}pagination[pageSize]=#{number}"
29
+ end
30
+
31
+ # Sets the page number for the query result.
32
+ #
33
+ # @param number [Integer] An Integer representing the page number.
34
+ #
35
+ # @return [String] The updated query string with the page number option added.
36
+ def page(number)
37
+ raise TypeError, "#{ErrorMessage.expected_integer} Got #{number.class.name}" unless number.is_a?(Integer)
38
+
39
+ check_single_pagination
40
+ @result += "#{prefix}pagination[page]=#{number}"
41
+ end
42
+
43
+ # Sets the offset for the query result.
44
+ #
45
+ # @param number [Integer] An Integer representing the offset.
46
+ #
47
+ # @return [String] The updated query string with the offset option added.
48
+ def start(number)
49
+ raise TypeError, "#{ErrorMessage.expected_integer} Got #{number.class.name}" unless number.is_a?(Integer)
50
+
51
+ check_single_pagination
52
+ @result += "#{prefix}pagination[start]=#{number}"
53
+ end
54
+
55
+ # Sets the limit for the query result.
56
+ #
57
+ # @param number [Integer] An Integer representing the limit.
58
+ #
59
+ # @return [String] The updated query string with the limit option added.
60
+ def limit(number)
61
+ raise TypeError unless number.is_a?(Integer)
62
+
63
+ check_single_pagination
64
+ @result += "#{prefix}pagination[limit]=#{number}"
65
+ end
66
+
67
+ ##
68
+ # Sets the locale for the query result.
69
+ #
70
+ # @params arg [String, Symbol] A String or Symbol representing the locale.
71
+ #
72
+ # @return [String] The updated query string with the locale option added.
73
+ def locale(arg)
74
+ raise TypeError, "#{ErrorMessage.expected_string_symbol} Got #{arg.class.name}" unless arg.is_a?(String) || arg.is_a?(Symbol)
75
+
76
+ @result += "#{prefix}locale=#{arg}"
77
+ end
78
+
79
+ def publication_state(arg)
80
+ raise TypeError, "#{ErrorMessage.expected_string_symbol} Got #{arg.class.name}" unless arg.is_a?(String) || arg.is_a?(Symbol)
81
+ raise ArgumentError, "#{ErrorMessage.publication_state} Got #{arg}" unless arg.to_sym == :live || arg.to_sym == :preview
82
+
83
+ @result += "#{prefix}publicationState=#{arg}"
84
+ end
85
+
86
+ # Checks params don't combine pagination methods.
87
+ #
88
+ def check_single_pagination
89
+ return unless (@options.key?(:page) && @options.key?(:start)) ||
90
+ (@options.key(:page) && @options.key?(:limit)) ||
91
+ (@options.key(:pagination) && options.key?(:start)) ||
92
+ (@options.key(:pagination) && options.key?(:limit))
93
+
94
+ raise ArgumentError, ErrorMessage.pagination
95
+ end
96
+
97
+ # builds the prefix for the query string (either "?" or "&" depending on whether the query string is empty or not).
98
+ #
99
+ # @return [String] A String representing the prefix.
100
+ def prefix
101
+ @result.empty? ? "?" : "&"
102
+ end
103
+
104
+ def build_query_from_args(args, method_name)
105
+ query = prefix
106
+ hash = {}
107
+ hash[method_name] = args
108
+ query += traverse_hash(hash)
109
+ @result += query
110
+ end
111
+
112
+ def traverse_hash(hash, parent_key = nil)
113
+ hash.map do |key, value|
114
+ current_key = parent_key ? "#{parent_key}[#{key}]" : key.to_s
115
+
116
+ if value.is_a?(Hash)
117
+ traverse_hash(value, current_key)
118
+ elsif value.is_a?(Array)
119
+ value.map.with_index do |item, index|
120
+ traverse_hash({ index => item }, current_key)
121
+ end
122
+ else
123
+ # We can pass values as symbols but we need to convert them to string
124
+ # to be able to escape them
125
+ value = value.to_s if value.is_a?(Symbol)
126
+ "#{current_key}=#{CGI.escape(value)}"
127
+ end
128
+ end.join("&")
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,48 @@
1
+ module StrapiRuby
2
+ class ClientError < StandardError
3
+ end
4
+
5
+ class ConnectionError < ClientError
6
+ def initialize(message)
7
+ super("There is a problem while initializing the connection with Faraday: #{message}")
8
+ end
9
+ end
10
+
11
+ class UnauthorizedError < ClientError
12
+ def initialize(message, status)
13
+ super("There is an error from the Strapi server with status #{status}: #{message}. Make sure your strapi_token is valid.")
14
+ end
15
+ end
16
+
17
+ class ForbiddenError < ClientError
18
+ def initialize(message, status)
19
+ super("There is an error from the Strapi server with status #{status}: #{message}. Make sure your strapi_token has the correct permissions or allow public access.")
20
+ end
21
+ end
22
+
23
+ class NotFoundError < ClientError
24
+ def initialize(message, status)
25
+ super("There is an error from the Strapi server with status #{status}: #{message}.")
26
+ end
27
+ end
28
+
29
+ class UnprocessableEntityError < ClientError
30
+ def initialize(message, status)
31
+ super("There is an error from the Strapi server with status #{status}: #{message}.")
32
+ end
33
+ end
34
+
35
+ class ServerError < ClientError
36
+ def initialize(message, status)
37
+ super("There is an error from the Strapi server with status #{status}: #{message}. Please try again later.")
38
+ end
39
+ end
40
+
41
+ class BadRequestError < ClientError
42
+ def initialize(message, status)
43
+ super("There is an error from the Strapi server with status #{status}: #{message}. Check parameters")
44
+ end
45
+ end
46
+
47
+ class JSONParsingError < ClientError; end
48
+ end
@@ -0,0 +1,7 @@
1
+ module StrapiRuby
2
+ class ConfigurationError < StandardError
3
+ def initialize(message)
4
+ super("You must configure StrapiRuby before using it. See README.md for details.\n #{message}")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ missing_configuration: >
2
+ StrapiRuby.configure do |config|
3
+ config.strapi_server_uri = 'https://example.com'
4
+ config.strapi_token = '123'
5
+ end
6
+ missing_ressource: >
7
+ StrapiRuby.get(resource: :restaurants)
8
+ missing_data: >
9
+ StrapiRuby.post(resource: :restaurants, data: { name: 'My restaurant' })
@@ -0,0 +1,41 @@
1
+ require "yaml"
2
+
3
+ module StrapiRuby
4
+ class ErrorMessage
5
+ class << self
6
+ def method_missing(method_name, *_args)
7
+ define_singleton_method(method_name) do
8
+ text = text_yaml[method_name.to_s]
9
+ code = code_yaml[method_name.to_s]
10
+
11
+ build_error_message(text, code)
12
+ end
13
+
14
+ send(method_name)
15
+ end
16
+
17
+ private
18
+
19
+ def text_yaml
20
+ load_yaml("text")
21
+ end
22
+
23
+ def code_yaml
24
+ load_yaml("code")
25
+ end
26
+
27
+ def load_yaml(filename)
28
+ path = File.join(current_directory, "error_#{filename}.yml")
29
+ YAML.load_file(path)
30
+ end
31
+
32
+ def current_directory
33
+ File.dirname(__FILE__)
34
+ end
35
+
36
+ def build_error_message(text, code = nil)
37
+ "\n\n#{text}\n\n#{code ? "Example:\n\n#{code}\n\n" : ""}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ missing_configuration: Config block missing. Configure plugin in /config/strapi_ruby.rb.
2
+ missing_ressource: Ressource is missing.
3
+ missing_data: Data key is missing from parameters. You need to pass a body to perform this HTTP request (POST, PUT).
4
+ connection_failed: Connection failed!
5
+ timeout: Timeout reached!
6
+ unexpected: An unexpected error occured.
7
+ expected_array: Invalid argument type. Expected Array.
8
+ expected_hash: Invalid argument type. Expected Hash.
9
+ expected_string_symbol: Invalid argument type. Expected String or Symbol.
10
+ expected_integer: Invalid argument type. Expected Integer.
11
+ expected_proc: Invalid argument type. Expected Proc.
12
+ publication_state: Invalid argument. Expected :live or :preview.
13
+ pagination: Use a single pagination method, either by page or by offset
@@ -0,0 +1,88 @@
1
+ module StrapiRuby
2
+ class Formatter
3
+ def initialize(options = {})
4
+ @keys_to_convert = options[:convert_to_html] || StrapiRuby.config.convert_to_html
5
+ end
6
+
7
+ def call(data)
8
+ # Check data for emptiness
9
+ check_emptiness(data)
10
+
11
+ converted_data = data.clone
12
+ convert_to_html!(converted_data)
13
+ convert_to_datetime!(converted_data)
14
+ converted_data
15
+ end
16
+
17
+ private
18
+
19
+ def convert_to_datetime!(data)
20
+ return unless StrapiRuby.config.convert_to_datetime
21
+
22
+ if collection?(data)
23
+ data.each { |item| parse_into_datetime!(item.attributes) }
24
+ else
25
+ parse_into_datetime!(data.attributes)
26
+ end
27
+ end
28
+
29
+ def parse_into_datetime!(attributes)
30
+ traverse_struct_and_change_key!(attributes, "createdAt")
31
+ traverse_struct_and_change_key!(attributes, "publishedAt")
32
+ traverse_struct_and_change_key!(attributes, "updatedAt")
33
+ end
34
+
35
+ def traverse_struct_and_change_key!(struct, key_to_check, path = [])
36
+ struct.each_pair do |key, value|
37
+ current_path = path + [key]
38
+
39
+ struct.send("#{key}=", DateTime.parse(struct.send(key_to_check.to_sym))) if key.to_s == key_to_check && struct.respond_to?(key_to_check.to_sym)
40
+
41
+ if value.is_a?(OpenStruct)
42
+ traverse_struct_and_change_key!(value, key_to_check, current_path)
43
+ elsif value.is_a?(Array)
44
+ value.each_with_index do |item, index|
45
+ traverse_struct_and_change_key!(item, key_to_check, current_path + [index]) if item.is_a?(OpenStruct)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def check_emptiness(data)
52
+ if collection?(data)
53
+ data if data.all? { |item| item.to_h.empty? }
54
+ elsif data.to_h.empty?
55
+ data
56
+ end
57
+ end
58
+
59
+ def collection?(data)
60
+ data.is_a?(Array)
61
+ end
62
+
63
+ def convert_to_html!(data)
64
+ if collection?(data)
65
+ data.each { |item| convert_attributes!(item.attributes) }
66
+ else
67
+ convert_attributes!(data.attributes)
68
+ end
69
+ end
70
+
71
+ def convert_attributes!(attributes)
72
+ # Loop through the methods of the attributes
73
+ attributes.methods.map do |method|
74
+ # Check if the method is in the keys to convert
75
+ next unless @keys_to_convert.include?(method)
76
+
77
+ # Get the value of the method
78
+ method_value = attributes.send(method)
79
+ # Convert and set the value of the method
80
+ attributes.send("#{method}=", convert_value_to_html(method_value))
81
+ end
82
+ end
83
+
84
+ def convert_value_to_html(value)
85
+ Markdown.instance.to_html(value)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,81 @@
1
+ module StrapiRuby
2
+ module Interface
3
+ include StrapiRuby::Validations
4
+
5
+ def get(options = {})
6
+ request(:get, options)
7
+ end
8
+
9
+ def post(options = {})
10
+ request(:post, options)
11
+ end
12
+
13
+ def put(options = {})
14
+ request(:put, options)
15
+ end
16
+
17
+ def delete(options = {})
18
+ request(:delete, options)
19
+ end
20
+
21
+ def escape_empty_answer(answer)
22
+ return answer.error.message if answer.data.nil? && answer.error
23
+ yield
24
+ end
25
+
26
+ private
27
+
28
+ def request(http_verb, options = {})
29
+ begin
30
+ validate_options(options)
31
+ @endpoint = build_endpoint(options)
32
+ answer = build_answer(http_verb, @endpoint, options)
33
+ data = format_data(answer.data, options)
34
+ meta = answer.meta
35
+
36
+ return_success_open_struct(data, meta, options)
37
+ rescue StrapiRuby::ClientError, StrapiRuby::ConfigError => e
38
+ return_error_open_struct(e, options)
39
+ end
40
+ end
41
+
42
+ def build_answer(http_verb, endpoint, options)
43
+ if %i[get delete].include?(http_verb)
44
+ @client.public_send(http_verb, endpoint)
45
+ else
46
+ validate_data_presence(options)
47
+ body = options[:data]
48
+ @client.public_send(http_verb, endpoint, body)
49
+ end
50
+ end
51
+
52
+ def show_endpoint?(options)
53
+ options[:show_endpoint] || StrapiRuby.config.show_endpoint
54
+ end
55
+
56
+ def return_success_open_struct(data, meta, error = nil, options = {})
57
+ if show_endpoint?(options)
58
+ OpenStruct.new(data: data,
59
+ meta: meta,
60
+ endpoint: @endpoint).freeze
61
+ else
62
+ OpenStruct.new(data: data, meta: meta).freeze
63
+ end
64
+ end
65
+
66
+ def return_error_open_struct(error, options = {})
67
+ OpenStruct.new(error: OpenStruct.new(message: "#{error.class}: #{error.message}"),
68
+ endpoint: @endpoint,
69
+ data: nil,
70
+ meta: nil).freeze
71
+ end
72
+
73
+ def build_endpoint(options)
74
+ Endpoint::Builder.new(options).call
75
+ end
76
+
77
+ def format_data(data, options = {})
78
+ Formatter.new(options).call(data)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,20 @@
1
+ require "singleton"
2
+ require "redcarpet"
3
+
4
+ # Use with Markdown.instance.to_html
5
+
6
+ module StrapiRuby
7
+ class Markdown
8
+ include Singleton
9
+
10
+ def to_html(markdown)
11
+ markdown_renderer.render(markdown)
12
+ end
13
+
14
+ private
15
+
16
+ def markdown_renderer
17
+ @markdown_renderer ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, { autolink: true })
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module StrapiRuby
2
+ class Railtie < Rails::Railtie
3
+ railtie_name :strapi_ruby
4
+
5
+ if defined?(Rake)
6
+ rake_tasks do
7
+ path = File.expand_path(__dir__)
8
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
9
+ end
10
+ end
11
+ end
12
+ end