joshua 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ class Joshua
2
+ class Error < StandardError
3
+ end
4
+
5
+ RESCUE_FROM = {}
6
+
7
+ class << self
8
+ # rescue_from CustomError do ...
9
+ # for unhandled
10
+ # rescue_from :all do
11
+ # api.error 500, 'Error happens'
12
+ # end
13
+ # define handled error code and description
14
+ # error :not_found, 'Document not found'
15
+ # error 404, 'Document not found'
16
+ # in api methods
17
+ # error 404
18
+ # error :not_found
19
+ def rescue_from klass, desc=nil, &block
20
+ RESCUE_FROM[klass] = desc || block
21
+ end
22
+
23
+ # show and render single error in class error format
24
+ # usually when API class not found
25
+ def error text
26
+ out = Response.new nil
27
+ out.error text
28
+ out.render
29
+ end
30
+
31
+ def error_print error
32
+ return if ENV['RACK_ENV'] == 'test'
33
+
34
+ puts
35
+ puts 'Joshua error dump'.red
36
+ puts '---'
37
+ puts '%s: %s' % [error.class, error.message]
38
+ puts '---'
39
+ puts error.backtrace
40
+ puts '---'
41
+ end
42
+ end
43
+
44
+ ###
45
+
46
+ def error desc
47
+ if err = RESCUE_FROM[desc]
48
+ if err.is_a?(Proc)
49
+ err.call
50
+ else
51
+ response.error desc, err
52
+ desc = err
53
+ end
54
+
55
+ return
56
+ end
57
+
58
+ raise Joshua::Error, desc
59
+ end
60
+
61
+ end
@@ -0,0 +1,204 @@
1
+ class Joshua
2
+ PARAMS ||= Params::Define.new
3
+ ANNOTATIONS ||= {}
4
+ OPTS = {}
5
+ PLUGINS = {}
6
+ DOCUMENTED = []
7
+
8
+ class << self
9
+ def base what
10
+ set :opts, :base, what
11
+ end
12
+
13
+ # if you want to make API DOC public use "documented"
14
+ def documented
15
+ if self == Joshua
16
+ DOCUMENTED.map(&:to_s).sort.map(&:constantize)
17
+ else
18
+ DOCUMENTED.push self unless DOCUMENTED.include?(self)
19
+ end
20
+ end
21
+
22
+ def api_path
23
+ to_s.underscore.sub(/_api$/, '')
24
+ end
25
+
26
+ # define method annotations
27
+ # annotation :unsecure! do
28
+ # @is_unsecure = true
29
+ # end
30
+ # unsecure!
31
+ # def login
32
+ # ...
33
+ def annotation name, &block
34
+ ANNOTATIONS[name] = block
35
+ self.define_singleton_method name do |*args|
36
+ PARAMS.__add_annotation name, args
37
+ end
38
+ end
39
+
40
+ # /api/companies/1/show
41
+ def member &block
42
+ @method_type = :member
43
+ class_exec &block
44
+ @method_type = nil
45
+ end
46
+
47
+ # /api/companies/list?countrty_id=1
48
+ def collection &block
49
+ @method_type = :collection
50
+ class_exec &block
51
+ @method_type = nil
52
+ end
53
+
54
+ # There are multiple ways to create params
55
+ # params :name, String, req: true
56
+ # params.name!, String
57
+ # params do
58
+ # name String, required: true
59
+ # name! String
60
+ # end
61
+ # params :label do |value, opts|
62
+ # # validate is value a label, return coarced label
63
+ # # or raise error with error
64
+ # end
65
+ def params *args, &block
66
+ if name = args.first
67
+ if block
68
+ # if argument is provided we create a validator
69
+ Params::Parse.define name, &block
70
+ else
71
+ only_in_api_methods!
72
+ PARAMS.send *args
73
+ end
74
+ elsif block
75
+ PARAMS.instance_eval &block
76
+ else
77
+ only_in_api_methods!
78
+ PARAMS
79
+ end
80
+ end
81
+
82
+ # api method icon
83
+ # you can find great icons at https://boxicons.com/ - export to svg
84
+ def icon data
85
+ if @method_type
86
+ raise ArgumentError.new('Icons cant be added on methods')
87
+ else
88
+ set :opts, :icon, data
89
+ end
90
+ end
91
+
92
+ # api method description
93
+ def desc data
94
+ if @method_type
95
+ PARAMS.__add :desc, data
96
+ else
97
+ set :opts, :desc, data
98
+ end
99
+ end
100
+
101
+ # api method detailed description
102
+ def detail data
103
+ return if data.to_s == ''
104
+
105
+ if @method_type
106
+ PARAMS.__add :detail, data
107
+ else
108
+ set :opts, :detail, data
109
+ end
110
+ end
111
+
112
+ # method in available for GET requests as well
113
+ def gettable
114
+ if @method_type
115
+ PARAMS.__add :gettable
116
+ else
117
+ raise ArgumentError.new('gettable can only be set on methods')
118
+ end
119
+ end
120
+
121
+ # allow methods without @api.bearer token set
122
+ def unsafe
123
+ if @method_type
124
+ PARAMS.__add :unsafe
125
+ else
126
+ raise ArgumentError.new('Only api methods can be unsafe')
127
+ end
128
+ end
129
+
130
+ # all api methods are secure (require bearer token)
131
+ def unsecure
132
+
133
+ end
134
+
135
+ # block execute before any public method or just some member or collection methods
136
+ def before &block
137
+ set_callback :before, block
138
+ end
139
+
140
+ # block execute after any public method or just some member or collection methods
141
+ # used to add meta tags to response
142
+ def after &block
143
+ set_callback :after, block
144
+ end
145
+
146
+ # simplified module include, masked as plugin
147
+ # Joshua.plugin :foo do ...
148
+ # Joshua.plugin :foo
149
+ def plugin name, &block
150
+ if block_given?
151
+ # if block given, define a plugin
152
+ PLUGINS[name] = block
153
+ else
154
+ # without a block execute it
155
+ blk = PLUGINS[name]
156
+ raise ArgumentError.new('Plugin :%s not defined' % name) unless blk
157
+ instance_exec &blk
158
+ end
159
+ end
160
+
161
+ def get *args
162
+ opts.dig *args
163
+ end
164
+
165
+ # dig all options for a current class
166
+ def opts
167
+ out = {}
168
+
169
+ # dig down the ancestors tree till Object class
170
+ ancestors.each do |klass|
171
+ break if klass == Object
172
+
173
+ # copy all member and collection method options
174
+ keys = (OPTS[klass.to_s] || {}).keys
175
+ keys.each do |type|
176
+ for k, v in (OPTS.dig(klass.to_s, type) || {})
177
+ out[type] ||= {}
178
+ out[type][k] ||= v
179
+ end
180
+ end
181
+ end
182
+
183
+ out
184
+ end
185
+
186
+ private
187
+
188
+ # generic opts set
189
+ # set :foo, :bar, :baz
190
+ def set *args
191
+ name, value = args.pop(2)
192
+ args.unshift to_s
193
+ pointer = OPTS
194
+
195
+ for el in args
196
+ pointer[el] ||= {}
197
+ pointer = pointer[el]
198
+ end
199
+
200
+ pointer[name] = value
201
+ end
202
+ end
203
+ end
204
+
@@ -0,0 +1,53 @@
1
+ class Joshua
2
+ module Params
3
+ class Define
4
+ def initialize
5
+ @opts = { }
6
+ end
7
+
8
+ def method_missing name, *args
9
+ name = name.to_s
10
+
11
+ raise ArgumentError.new('! is not allowed in params') if name.include?('!')
12
+
13
+ type, opts = args
14
+
15
+ if type.is_a?(Hash)
16
+ opts = args.first
17
+ type = :string
18
+ end
19
+
20
+ type = :string if type.nil?
21
+
22
+ opts ||= {}
23
+ opts[:type] = type.to_s.dasherize.downcase.to_sym
24
+
25
+ opts[:required] = true if opts[:required].nil?
26
+ opts[:required] = false if name.sub! /\?$/, ''
27
+ opts[:required] = false if opts.delete(:optional)
28
+
29
+ opts.merge!(type: :boolean, default: false) if opts[:type] == :false
30
+ opts.merge!(type: :boolean, default: true) if opts[:type] == :true
31
+ opts[:required] = false if opts[:type] == :boolean
32
+
33
+ @opts[:params] ||= {}
34
+ @opts[:params][name.to_sym] = opts
35
+ end
36
+
37
+ def fetch_and_clear_opts
38
+ @opts
39
+ .dup
40
+ .tap { @opts = {} }
41
+ end
42
+
43
+ def __add name, value=true
44
+ @opts[name] = value
45
+ end
46
+
47
+ def __add_annotation name, data
48
+ @opts[:annotations] ||= {}
49
+ @opts[:annotations][name] = data
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,56 @@
1
+ class Joshua
2
+ module Params
3
+ class Parse
4
+ class << self
5
+ def define name, &block
6
+ define_method 'check_%s' % name do |value, opts|
7
+ block.call value, opts || {}
8
+ end
9
+ end
10
+ end
11
+
12
+ ###
13
+
14
+ # check :boolean, 'on'
15
+ def check type, value, opts={}
16
+ opts[:required] = true if opts.delete(:req)
17
+
18
+ if value.to_s == ''
19
+ if !opts[:default].nil?
20
+ opts[:default]
21
+ elsif opts[:required]
22
+ error 'Argument missing'
23
+ end
24
+ else
25
+ m = 'check_%s' % type
26
+ hard_error 'Unsupported paramter type: %s' % type unless respond_to?(m)
27
+
28
+ if opts[:array]
29
+ delimiter = opts[:array].is_a?(TrueClass) ? /\s*[,:;]\s*/ : opts[:array]
30
+
31
+ value = value.split(delimiter) unless value.is_a?(Array)
32
+ value.map { |_| check_send m, _, opts }
33
+ else
34
+ check_send m, value, opts
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def check_send m, value, opts
42
+ send(m, value, opts).tap do |_|
43
+ error 'Value not in range of values' if opts[:values] && !opts[:values].include?(_)
44
+ end
45
+ end
46
+
47
+ def error desc
48
+ raise Joshua::Error, desc
49
+ end
50
+
51
+ def hard_error desc
52
+ raise StandardError, desc
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,152 @@
1
+ require 'date'
2
+
3
+ class Joshua
4
+ module Params
5
+ class Parse
6
+ # params.is_active :boolean, default: false
7
+ def check_boolean value, opts={}
8
+ return false unless value
9
+
10
+ if %w(true 1 on).include?(value.to_s)
11
+ true
12
+ elsif %w(false 0 off).include?(value.to_s)
13
+ false
14
+ else
15
+ error 'Unsupported boolean param value: %s' % value
16
+ end
17
+ end
18
+
19
+ def check_integer value, opts={}
20
+ value.to_i.tap do |test|
21
+ error localized(:not_integer) if test.to_s != value.to_s
22
+ error localized(:min_value) % opts[:min] if opts[:min] && test < opts[:min]
23
+ error localized(:max_value) % opts[:max] if opts[:max] && test > opts[:max]
24
+ end
25
+ end
26
+
27
+ def check_string value, opts={}
28
+ value
29
+ .to_s
30
+ .sub(/^\s+/, '')
31
+ .sub(/\s+$/, '')
32
+ end
33
+
34
+ def check_float value, opts={}
35
+ value =
36
+ if opts[:round]
37
+ value.to_f.round(opts[:round])
38
+ else
39
+ value.to_f
40
+ end
41
+
42
+ error localized(:min_value) % opts[:min] if opts[:min] && value < opts[:min]
43
+ error localized(:max_value) % opts[:max] if opts[:max] && value > opts[:max]
44
+
45
+ value
46
+ end
47
+
48
+ def check_date value, opts={}
49
+ date = DateTime.parse(value)
50
+ date = DateTime.new(date.year, date.month, date.day)
51
+
52
+ check_date_min_max date, opts
53
+ end
54
+
55
+ def check_date_time value, opts={}
56
+ date = DateTime.parse(value)
57
+ check_date_min_max date, opts
58
+ end
59
+
60
+ def check_hash value, opts={}
61
+ value = {} unless value.is_a?(Hash)
62
+
63
+ if opts[:allow]
64
+ for key in value.keys
65
+ value.delete(key) unless opts[:allow].include?(key)
66
+ end
67
+ end
68
+
69
+ value
70
+ end
71
+
72
+ def check_email email, opts={}
73
+ error localized(:email_min) unless email.to_s.length > 7
74
+ error localized(:email_missing) unless email.include?('@')
75
+ email.downcase
76
+ end
77
+
78
+ def check_url url
79
+ error localized(:url_start) unless url =~ /^https?:\/\/./
80
+ url
81
+ end
82
+
83
+ # geolocation point. google maps url will be automaticly converted
84
+ # https://www.google.com/maps/@51.5254742,-0.1057319,13z
85
+ def check_point value, opts={}
86
+ parts = value.split(/\s*,\s*/) unless parts.is_a?(Array)
87
+
88
+ error localized(:point_format) unless parts[1]
89
+
90
+ for part in parts
91
+ error localized(:point_format) unless part.include?('.')
92
+ error localized(:point_format) unless part.length > 5
93
+ end
94
+
95
+ parts.join(',')
96
+ end
97
+
98
+ def check_oib oib, opts={}
99
+ oib = oib.to_s
100
+
101
+ return false unless oib.match(/^[0-9]{11}$/)
102
+
103
+ control_sum = (0..9).inject(10) do |middle, position|
104
+ middle += oib.at(position).to_i
105
+ middle %= 10
106
+ middle = 10 if middle == 0
107
+ middle *= 2
108
+ middle %= 11
109
+ end
110
+
111
+ control_sum = 11 - control_sum
112
+ control_sum = 0 if control_sum == 10
113
+
114
+ if control_sum == oib.at(10).to_i
115
+ oib.to_i
116
+ else
117
+ error 'Wrong OIB'
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def check_date_min_max value, opts={}
124
+ if min = opts[:min]
125
+ min = DateTime.parse(min)
126
+ error localized(:min_date) % min if min > value
127
+ end
128
+
129
+ if max = opts[:max]
130
+ max = DateTime.parse(max)
131
+ error localized(:max_date) % max if value > max
132
+ end
133
+
134
+ value
135
+ end
136
+
137
+ def localized error
138
+ locale =
139
+ if defined?(Lux)
140
+ Lux.current.locale.to_s
141
+ elsif defined?(I18n)
142
+ I18n.with_locale || I18n.locale
143
+ else
144
+ :en
145
+ end
146
+
147
+ pointer = ERRORS[locale.to_sym] || ERRORS[:en]
148
+ pointer[error]
149
+ end
150
+ end
151
+ end
152
+ end