rsodx 0.0.1 → 0.0.3

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,63 @@
1
+ require "fileutils"
2
+ require_relative "scaffold_common"
3
+
4
+ module Rsodx::Cli::Commands
5
+ class Scaffold < Dry::CLI::Command
6
+ include Rsodx::Cli::Commands::ScaffoldCommon
7
+ desc "Scaffold a new Rsodx project"
8
+
9
+ argument :name, required: true, desc: "Project name"
10
+
11
+ def call(arg)
12
+ @name = arg[:name]
13
+ @app_path = File.join(Dir.pwd, @name)
14
+
15
+ puts Rsodx::LOGO
16
+
17
+ puts "\e[33m🛤️ Initializing Transport Empire for '#{@name}'...\e[0m"
18
+ puts "\e[34m📦 Creating files and folders...\e[0m"
19
+
20
+ create_folders
21
+ create_files
22
+
23
+ puts "\e[32m✅ Done! Your microservice has been scaffolded at the '#{@app_path}' railway!\e[0m"
24
+ rescue => e
25
+ abort "\e[31m❌ Failed to scaffold: #{e.message}\e[0m"
26
+ end
27
+
28
+ private
29
+
30
+ def create_folders
31
+ FOLDERS.each do |path|
32
+ FileUtils.mkdir_p(File.join(@app_path, path))
33
+ end
34
+ end
35
+
36
+ def create_files
37
+ write("Gemfile", GEMFILE)
38
+ write(".ruby-version", RUBY_VERSION)
39
+ write(".env", ENVFILE)
40
+ write(".gitignore", GITIGNORE)
41
+ write("config/environment.rb", ENV_LOADER)
42
+ write("Rakefile", RAKEFILE)
43
+ write("app/controllers/app_controller.rb", APP_CONTROLLER)
44
+ write("app/services/app_services.rb", APP_SERVICE)
45
+ write("app/serializers/app_serializer.rb", APP_SERIALIZER)
46
+ write("app/presenters/app_presenter.rb", APP_PRESENTER)
47
+ write("app/app.rb", APP)
48
+ write("app/router.rb", ROUTE)
49
+ write("config/environments/development.rb", "")
50
+ write("config.ru", CONFIGRU)
51
+ write("bin/console", CONSOLE)
52
+ write("bin/rsodx", BINRSODX)
53
+
54
+ FileUtils.chmod("+x", File.join(@app_path, "bin/rsodx"))
55
+ FileUtils.chmod("+x", File.join(@app_path, "bin/console"))
56
+ end
57
+
58
+ def write(relative_path, content)
59
+ full_path = File.join(@app_path, relative_path)
60
+ File.write(full_path, content)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,107 @@
1
+ module Rsodx::Cli::Commands::ScaffoldCommon
2
+ RUBY_VERSION = "3.4.2".freeze
3
+
4
+ GITIGNORE = <<~GITIGNORE.freeze
5
+ .env
6
+ tmp/
7
+ GITIGNORE
8
+
9
+ # path: "../rsodx"
10
+ GEMFILE = <<~GEMFILE.freeze
11
+ source "https://rubygems.org"
12
+
13
+ gem "rsodx"
14
+ gem "pg"
15
+ GEMFILE
16
+
17
+ BINRSODX = <<~BINRSODX.freeze
18
+ #!/usr/bin/env ruby
19
+
20
+ require "fileutils"
21
+ require "optparse"
22
+ require "rsodx"
23
+
24
+ Rsodx::Cli.setup!
25
+ Rsodx::CLI.call
26
+ BINRSODX
27
+
28
+ CONSOLE = <<~CONSOLE.freeze
29
+ #!/usr/bin/env ruby
30
+
31
+ require "irb"
32
+ require "irb/completion"
33
+
34
+ def reload!
35
+ puts "🔄 Reloading..."
36
+ load File.expand_path("../config/environment.rb", __dir__)
37
+ end
38
+
39
+ require_relative "../config/environment"
40
+
41
+ puts "🔬 Welcome to Rsodx console (#{ENV['RACK_ENV'] || 'development'})"
42
+ puts "Tip: access Rsodx modules, models, services, etc."
43
+
44
+ IRB.start
45
+ CONSOLE
46
+
47
+ CONFIGRU = <<~RACK.freeze
48
+ require_relative "./app/app"
49
+ run App
50
+ RACK
51
+
52
+ ENV_LOADER = <<~ENV_LOADER.freeze
53
+ require "rsodx"
54
+ Rsodx::Environment.load_dotenv(ENV["RACK_ENV"] || "development")
55
+ Rsodx::Connect.connect ENV["DATABASE_URL"]
56
+ Rsodx::Environment.load_initializers(File.expand_path("../..", __FILE__))
57
+ Rsodx::Boot.load_app_structure(File.expand_path("../..", __FILE__))
58
+ ENV_LOADER
59
+
60
+ ROUTE = <<~ROUTE.freeze
61
+ class Router < Rsodx::Router
62
+ end
63
+ ROUTE
64
+
65
+ APP = <<~APP.freeze
66
+ require "rsodx"
67
+ require_relative "../config/environment"
68
+ require_relative "router"
69
+
70
+ class App < Rsodx::Base
71
+ use Router
72
+ end
73
+ APP
74
+
75
+ APP_SERVICE = <<~APP_SERVICE.freeze
76
+ class AppService < Rsodx::Service
77
+ end
78
+ APP_SERVICE
79
+
80
+ APP_CONTROLLER = <<~APP_CONTROLLER.freeze
81
+ class AppController < Rsodx::Controller
82
+ end
83
+ APP_CONTROLLER
84
+
85
+ APP_SERIALIZER = <<~APP_SERIALIZER.freeze
86
+ class AppSerializer < Rsodx::Serializer
87
+ end
88
+ APP_SERIALIZER
89
+
90
+ APP_PRESENTER = <<~APP_PRESENTER.freeze
91
+ class AppPresenter < Rsodx::Presenter
92
+ end
93
+ APP_PRESENTER
94
+
95
+ RAKEFILE = <<~RAKEFILE.freeze
96
+ require_relative "config/environment"
97
+ require "rsodx/tasks"
98
+ RAKEFILE
99
+
100
+ ENVFILE = <<~ENVFILE.freeze
101
+ DATABASE_URL=postgres://rsodx:paSs4321@localhost:5432/rsodx_development
102
+ ENVFILE
103
+
104
+ FOLDERS = %w[app/controllers app/services app/models app/presenters
105
+ app/serializers config/initializers
106
+ config/environments db/migrations spec bin].freeze
107
+ end
@@ -0,0 +1,27 @@
1
+ require "dry/cli"
2
+
3
+ module Rsodx::Cli::Commands
4
+ class Server < Dry::CLI::Command
5
+ desc "Start the Rsodx development server"
6
+
7
+ option :port, type: :integer, default: 9292, desc: "Port to run the server on"
8
+ option :env, type: :string, default: "development", desc: "Rack environment"
9
+
10
+ def call(args)
11
+ env = args[:env]
12
+ port = args[:port]
13
+ ENV["RACK_ENV"] = env
14
+
15
+ rackup_path = File.expand_path("config.ru", Dir.pwd)
16
+ unless File.exist?(rackup_path)
17
+ abort "❌ config.ru not found in #{Dir.pwd}"
18
+ end
19
+
20
+ pid = spawn("bundle exec rackup --port=#{port} --host=0.0.0.0 #{rackup_path}")
21
+ puts "🚀 Rsodx started!"
22
+ Process.wait(pid)
23
+ rescue => e
24
+ abort "❌ Failed to start server: #{e.class} - #{e.message}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module Rsodx::Constants
2
+ ASCII_LOGO = <<~ASCII.freeze
3
+ \e[36m _____ _____ ____ _____ __ __
4
+ | __ \\ / ____| / __ \\ | __ \\ \\ \\ / /
5
+ | |__) | | (___ | | | | | | | | \\ V /
6
+ | _ / \\___ \\ | | | | | | | | \e[35m> <\e[0m\e[36m
7
+ | | \\ \\ ____) | | |__| | | |__| | / \e[35m.\e[0m\e[36m \\
8
+ |_| \\_\\ |_____/ \\____/ |_____/ /_/ \\_\\ \e[33mv__VERSION__\e[0m
9
+ ASCII
10
+
11
+ LOGO = ASCII_LOGO.sub("__VERSION__", Rsodx::VERSION).freeze
12
+ end
13
+
14
+ module Rsodx
15
+ include Rsodx::Constants
16
+ end
@@ -0,0 +1,4 @@
1
+ module Rsodx
2
+ class Controller < Rsodx::Service
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ require "forwardable"
2
+
3
+ module Rsodx
4
+ module Delegate
5
+ def delegate(*methods, to:)
6
+ raise ArgumentError, "Missing target for delegation (to:)" unless to
7
+
8
+ mod = Module.new do
9
+ extend Forwardable
10
+ def_delegators to, *methods
11
+ end
12
+
13
+ include mod
14
+ end
15
+ end
16
+ end
data/lib/rsodx/error.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  module Rsodx
2
- module Error
3
- class Error < StandardError; end
4
- class ContractError < Error; end
2
+ class Error < StandardError
5
3
  end
6
4
  end
@@ -0,0 +1,31 @@
1
+ module Rsodx
2
+ class Headers
3
+ attr_reader :headers
4
+
5
+ def initialize(request)
6
+ @headers = request.env.each_with_object({}) do |(k, v), memo|
7
+ if k.start_with?("HTTP_")
8
+ key = k.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
9
+ memo[key] = v
10
+ end
11
+ end
12
+ end
13
+
14
+ def [](key)
15
+ headers[key]
16
+ end
17
+
18
+ def authorization
19
+ headers["Authorization"]
20
+ end
21
+
22
+ def bearer_token
23
+ return nil unless authorization&.start_with?("Bearer ")
24
+ authorization.sub("Bearer ", "")
25
+ end
26
+
27
+ def to_h
28
+ headers
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ require 'oj'
2
+ module Rsodx
3
+ class Presenter < Rsodx::Service
4
+ def call
5
+ present
6
+ end
7
+
8
+ private
9
+
10
+ def present(dto: nil, **options)
11
+ dto ||= context.dto
12
+ Oj.dump(dto, { mode: :compat }.merge(options))
13
+ rescue => e
14
+ raise PresenterError, "Failed to present DTO: #{e.message}"
15
+ end
16
+
17
+ class PresenterError < StandardError; end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ module Rsodx
2
+ class Request
3
+ attr_reader :verb, :path, :request
4
+
5
+ def initialize(verb:, path:, request:)
6
+ @verb = verb.to_s.upcase
7
+ @path = path
8
+ @request = request
9
+ @parsed_payload = nil
10
+ end
11
+
12
+ def headers
13
+ @headers ||= Rsodx::Headers.new(request)
14
+ end
15
+
16
+ def params
17
+ request.params.transform_keys(&:to_sym).merge(payload)
18
+ end
19
+
20
+ def payload
21
+ return @parsed_payload if @parsed_payload
22
+
23
+ request.body.rewind
24
+ raw = request.body.read.strip
25
+
26
+ if raw.empty? || raw == 'null'
27
+ @parsed_payload = {}
28
+ elsif raw.start_with?("{") && raw.end_with?("}")
29
+ begin
30
+ @parsed_payload = JSON.parse(raw, symbolize_names: true)
31
+ rescue JSON::ParserError
32
+ @parsed_payload = {}
33
+ end
34
+ else
35
+ @parsed_payload = {}
36
+ end
37
+ end
38
+ end
39
+ end
@@ -7,38 +7,28 @@ module Rsodx
7
7
  end
8
8
 
9
9
  module ClassMethods
10
- def route(verb, path, interactor, opts = {})
10
+ def route(verb, path, handler_class, options = {})
11
11
  define_route(verb, path) do
12
- payload = begin
13
- request.body.rewind
14
- JSON.parse(request.body.read, symbolize_names: true)
15
- rescue JSON::ParserError
16
- {}
17
- end
12
+ req = Request.new(verb: verb, path: path, request: request)
13
+ result = Action.call(request: req, handler_class: handler_class, options: options)
18
14
 
19
- ctx = {
20
- params: params.to_h,
21
- json: payload
22
- }.merge(opts)
23
-
24
- result = interactor.call(ctx)
25
-
26
- status(result.error_code || 200)
15
+ content_type :json
16
+ status(result.error_code || result.code || 200)
27
17
 
28
18
  if result.success?
29
- { success: true, data: result.data }.to_json
19
+ result.json_response
30
20
  else
31
21
  { success: false, error: result.error }.to_json
32
22
  end
33
23
  end
34
24
  end
35
25
 
36
- %i[get post put patch delete].each do |verb|
26
+ %i[get post put patch delete head options].each do |verb|
37
27
  define_method(verb) do |path, interactor = nil, **opts, &block|
38
28
  if interactor
39
29
  route(verb, path, interactor, opts)
40
30
  else
41
- super(path, &block)
31
+ define_route(verb, path, &block)
42
32
  end
43
33
  end
44
34
  end
@@ -0,0 +1,24 @@
1
+ module Rsodx
2
+ class Serializer < Rsodx::Service
3
+ def call
4
+ context.dto = serialize_object
5
+ rescue => e
6
+ halt(500, e.message)
7
+ end
8
+
9
+ private
10
+
11
+ def serialize_object
12
+ case object
13
+ when Hash
14
+ object
15
+ when OpenStruct
16
+ object.to_h
17
+ else
18
+ raise NotSerializableError, "Can't serialize #{object.class.name}"
19
+ end
20
+ end
21
+
22
+ class NotSerializableError < StandardError; end
23
+ end
24
+ end
@@ -1,8 +1,11 @@
1
1
  require "interactor"
2
2
  require "json"
3
3
  module Rsodx
4
- class Interactor
4
+ class Service
5
5
  include ::Interactor
6
+ extend Delegate
7
+
8
+ delegate :params, :object, to: :context
6
9
 
7
10
  class << self
8
11
  attr_accessor :contract_class
@@ -21,7 +24,7 @@ module Rsodx
21
24
 
22
25
  def halt(code, message)
23
26
  log_error(code, message)
24
- context.fail!(error_code: code, error: Rsodx::Error::ContractError.new(message))
27
+ context.fail!(error_code: code, error: message)
25
28
  end
26
29
 
27
30
  private
data/lib/rsodx/tasks.rb CHANGED
@@ -3,11 +3,6 @@ include Rake::DSL if defined?(Rake::DSL)
3
3
 
4
4
  require "sequel/extensions/migration"
5
5
 
6
- desc "Запуск Puma-сервера"
7
- task :server do
8
- sh "bundle exec puma -p 9292 config.ru"
9
- end
10
-
11
6
  namespace :db do
12
7
  task :migrate do
13
8
  Sequel::Migrator.run(DB.connect, "db/migrations")
data/lib/rsodx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rsodx
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.3"
5
5
  end
data/lib/rsodx.rb CHANGED
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir[File.join(__dir__, "rsodx/*.rb")].sort.each { |f| require_relative f }
4
- Dir[File.join(__dir__, "rsodx/**/*.rb")].sort.each { |f| require_relative f }
3
+ require_relative 'rsodx/version'
4
+ require_relative 'rsodx/delegate'
5
+ require_relative 'rsodx/service'
6
+
7
+ base = File.expand_path(__dir__)
8
+
9
+ Dir.glob("#{base}/rsodx/*.rb").sort.each do |file|
10
+ require_relative file.sub("#{base}/", "")
11
+ end
12
+ Dir.glob("#{base}/rsodx/**/*.rb").sort.each do |file|
13
+ require_relative file.sub("#{base}/", "")
14
+ end
5
15
 
6
16
  module Rsodx
7
17
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsodx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eugene Pervushin
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-14 00:00:00.000000000 Z
10
+ date: 2025-04-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rake
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '13.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rackup
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.2'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: sinatra
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -150,33 +164,33 @@ dependencies:
150
164
  - !ruby/object:Gem::Version
151
165
  version: '0.5'
152
166
  - !ruby/object:Gem::Dependency
153
- name: rspec
167
+ name: oj
154
168
  requirement: !ruby/object:Gem::Requirement
155
169
  requirements:
156
170
  - - "~>"
157
171
  - !ruby/object:Gem::Version
158
- version: '3.0'
159
- type: :development
172
+ version: '3.16'
173
+ type: :runtime
160
174
  prerelease: false
161
175
  version_requirements: !ruby/object:Gem::Requirement
162
176
  requirements:
163
177
  - - "~>"
164
178
  - !ruby/object:Gem::Version
165
- version: '3.0'
179
+ version: '3.16'
166
180
  - !ruby/object:Gem::Dependency
167
- name: rubocop
181
+ name: dry-cli
168
182
  requirement: !ruby/object:Gem::Requirement
169
183
  requirements:
170
184
  - - "~>"
171
185
  - !ruby/object:Gem::Version
172
- version: '1.75'
173
- type: :development
186
+ version: '1.2'
187
+ type: :runtime
174
188
  prerelease: false
175
189
  version_requirements: !ruby/object:Gem::Requirement
176
190
  requirements:
177
191
  - - "~>"
178
192
  - !ruby/object:Gem::Version
179
- version: '1.75'
193
+ version: '1.2'
180
194
  description: Rsodx is a lightweight Ruby microframework designed for modular, service-oriented
181
195
  applications. Built with simplicity and performance in mind, it's perfect for small
182
196
  web apps, microservices, and CLI tools.
@@ -193,22 +207,33 @@ files:
193
207
  - bin/rsodx
194
208
  - bin/setup
195
209
  - lib/rsodx.rb
210
+ - lib/rsodx/action.rb
196
211
  - lib/rsodx/base.rb
197
212
  - lib/rsodx/boot.rb
198
- - lib/rsodx/cli/generate.rb
199
- - lib/rsodx/cli/generate_interactor.rb
200
- - lib/rsodx/cli/generate_migration.rb
201
- - lib/rsodx/cli/handler.rb
202
- - lib/rsodx/cli/scaffold.rb
203
- - lib/rsodx/cli/server.rb
213
+ - lib/rsodx/cli/cli.rb
214
+ - lib/rsodx/cli/commands/generator.rb
215
+ - lib/rsodx/cli/commands/generators/action.rb
216
+ - lib/rsodx/cli/commands/generators/controller.rb
217
+ - lib/rsodx/cli/commands/generators/migration.rb
218
+ - lib/rsodx/cli/commands/generators/presenter.rb
219
+ - lib/rsodx/cli/commands/generators/serializer.rb
220
+ - lib/rsodx/cli/commands/scaffold.rb
221
+ - lib/rsodx/cli/commands/scaffold_common.rb
222
+ - lib/rsodx/cli/commands/server.rb
204
223
  - lib/rsodx/connect.rb
224
+ - lib/rsodx/constants.rb
205
225
  - lib/rsodx/contract.rb
226
+ - lib/rsodx/controller.rb
227
+ - lib/rsodx/delegate.rb
206
228
  - lib/rsodx/environment.rb
207
229
  - lib/rsodx/error.rb
208
- - lib/rsodx/interactor.rb
209
- - lib/rsodx/interactors/healthcheck.rb
230
+ - lib/rsodx/headers.rb
231
+ - lib/rsodx/presenter.rb
232
+ - lib/rsodx/request.rb
210
233
  - lib/rsodx/router.rb
211
234
  - lib/rsodx/router_dsl.rb
235
+ - lib/rsodx/serializer.rb
236
+ - lib/rsodx/service.rb
212
237
  - lib/rsodx/tasks.rb
213
238
  - lib/rsodx/version.rb
214
239
  - spec/rsodx_spec.rb
@@ -1,17 +0,0 @@
1
- module Rsodx::Cli
2
- class Generate
3
- def self.run(args)
4
- type = args.shift
5
- name = args.shift
6
- case type
7
- when "interactor", "in"
8
- GenerateInteractor.new(name).create
9
- when "migration"
10
- GenerateMigration.new(name).create
11
- else
12
- puts "Unknown generate type: #{type}"
13
- exit(1)
14
- end
15
- end
16
- end
17
- end
@@ -1,34 +0,0 @@
1
- module Rsodx
2
- module Cli
3
- class GenerateInteractor
4
- def initialize(name)
5
- @name = name
6
- @file_name = File.join("app/interactors", snake_case(name) + ".rb")
7
- end
8
-
9
- def create
10
- puts "📦 Creating interactor: #{@file_name}"
11
- FileUtils.mkdir_p("app/interactors")
12
- File.write(@file_name, content)
13
- end
14
-
15
- def content
16
- <<~RUBY
17
- class #{@name} < AppInteractor
18
- def call
19
- # implement logic here
20
- end
21
- end
22
- RUBY
23
- end
24
-
25
- def snake_case(str)
26
- str.gsub(/::/, '/').
27
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
28
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
29
- tr("-", "_").
30
- downcase
31
- end
32
- end
33
- end
34
- end