rsodx 0.0.1 → 0.0.2

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,58 @@
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 "🚀 Creating project: #{@name}"
16
+ create_folders
17
+ create_files
18
+ puts "✅ Done! Your project is ready at #{@app_path}"
19
+ rescue => e
20
+ abort "❌ Failed to scaffold: #{e.message}"
21
+ end
22
+
23
+ private
24
+
25
+ def create_folders
26
+ FOLDERS.each do |path|
27
+ FileUtils.mkdir_p(File.join(@app_path, path))
28
+ end
29
+ end
30
+
31
+ def create_files
32
+ write("Gemfile", GEMFILE)
33
+ write(".ruby-version", RUBY_VERSION)
34
+ write(".env", ENVFILE)
35
+ write(".gitignore", GITIGNORE)
36
+ write("config/environment.rb", ENV_LOADER)
37
+ write("Rakefile", RAKEFILE)
38
+ write("app/controllers/app_controller.rb", APP_CONTROLLER)
39
+ write("app/services/app_services.rb", APP_SERVICE)
40
+ write("app/serializers/app_serializer.rb", APP_SERIALIZER)
41
+ write("app/presenters/app_presenter.rb", APP_PRESENTER)
42
+ write("app/app.rb", APP)
43
+ write("app/router.rb", ROUTE)
44
+ write("config/environments/development.rb", "")
45
+ write("config.ru", CONFIGRU)
46
+ write("bin/console", CONSOLE)
47
+ write("bin/rsodx", BINRSODX)
48
+
49
+ FileUtils.chmod("+x", File.join(@app_path, "bin/rsodx"))
50
+ FileUtils.chmod("+x", File.join(@app_path, "bin/console"))
51
+ end
52
+
53
+ def write(relative_path, content)
54
+ full_path = File.join(@app_path, relative_path)
55
+ File.write(full_path, content)
56
+ end
57
+ end
58
+ 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,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.2"
5
5
  end
data/lib/rsodx.rb CHANGED
@@ -1,7 +1,16 @@
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/delegate'
4
+ require_relative 'rsodx/service'
5
+
6
+ base = File.expand_path(__dir__)
7
+
8
+ Dir.glob("#{base}/rsodx/*.rb").sort.each do |file|
9
+ require_relative file.sub("#{base}/", "")
10
+ end
11
+ Dir.glob("#{base}/rsodx/**/*.rb").sort.each do |file|
12
+ require_relative file.sub("#{base}/", "")
13
+ end
5
14
 
6
15
  module Rsodx
7
16
  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.2
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
@@ -149,6 +163,34 @@ dependencies:
149
163
  - - "~>"
150
164
  - !ruby/object:Gem::Version
151
165
  version: '0.5'
166
+ - !ruby/object:Gem::Dependency
167
+ name: oj
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '3.16'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '3.16'
180
+ - !ruby/object:Gem::Dependency
181
+ name: dry-cli
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '1.2'
187
+ type: :runtime
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '1.2'
152
194
  - !ruby/object:Gem::Dependency
153
195
  name: rspec
154
196
  requirement: !ruby/object:Gem::Requirement
@@ -193,22 +235,32 @@ files:
193
235
  - bin/rsodx
194
236
  - bin/setup
195
237
  - lib/rsodx.rb
238
+ - lib/rsodx/action.rb
196
239
  - lib/rsodx/base.rb
197
240
  - 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
241
+ - lib/rsodx/cli/cli.rb
242
+ - lib/rsodx/cli/commands/generator.rb
243
+ - lib/rsodx/cli/commands/generators/action.rb
244
+ - lib/rsodx/cli/commands/generators/controller.rb
245
+ - lib/rsodx/cli/commands/generators/migration.rb
246
+ - lib/rsodx/cli/commands/generators/presenter.rb
247
+ - lib/rsodx/cli/commands/generators/serializer.rb
248
+ - lib/rsodx/cli/commands/scaffold.rb
249
+ - lib/rsodx/cli/commands/scaffold_common.rb
250
+ - lib/rsodx/cli/commands/server.rb
204
251
  - lib/rsodx/connect.rb
205
252
  - lib/rsodx/contract.rb
253
+ - lib/rsodx/controller.rb
254
+ - lib/rsodx/delegate.rb
206
255
  - lib/rsodx/environment.rb
207
256
  - lib/rsodx/error.rb
208
- - lib/rsodx/interactor.rb
209
- - lib/rsodx/interactors/healthcheck.rb
257
+ - lib/rsodx/headers.rb
258
+ - lib/rsodx/presenter.rb
259
+ - lib/rsodx/request.rb
210
260
  - lib/rsodx/router.rb
211
261
  - lib/rsodx/router_dsl.rb
262
+ - lib/rsodx/serializer.rb
263
+ - lib/rsodx/service.rb
212
264
  - lib/rsodx/tasks.rb
213
265
  - lib/rsodx/version.rb
214
266
  - 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
@@ -1,43 +0,0 @@
1
- require "fileutils"
2
- require "time"
3
-
4
- module Rsodx::Cli
5
- class GenerateMigration
6
- def initialize(name)
7
- @name = name
8
- @timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
9
- @file_name = File.join("db/migrations", "#{@timestamp}_#{snake_case(name)}.rb")
10
- end
11
-
12
- def create
13
- puts "📦 Creating migration: #{@file_name}"
14
- FileUtils.mkdir_p("db/migrations")
15
- File.write(@file_name, content)
16
- end
17
-
18
- private
19
-
20
- def content
21
- <<~RUBY
22
- Sequel.migration do
23
- change do
24
- # create_table(:example) do
25
- # primary_key :id
26
- # String :name
27
- # DateTime :created_at
28
- # DateTime :updated_at
29
- # end
30
- end
31
- end
32
- RUBY
33
- end
34
-
35
- def snake_case(str)
36
- str.gsub(/::/, '/')
37
- .gsub(/([A-Z]+)([A-Z][a-z])/,'\\1_\\2')
38
- .gsub(/([a-z\\d])([A-Z])/,'\\1_\\2')
39
- .tr("-", "_")
40
- .downcase
41
- end
42
- end
43
- end
@@ -1,26 +0,0 @@
1
- require_relative "scaffold"
2
-
3
- module Rsodx::Cli
4
- class Handler
5
- def self.run
6
- command = ARGV.shift
7
- case command
8
- when "s", "server"
9
- Rsodx::Cli::Server.run
10
- when "generate", "g"
11
- Rsodx::Cli::Generate.run(ARGV)
12
- when "new"
13
- name = ARGV.shift
14
- if name.nil? || name.empty?
15
- puts "Usage: rsodx new <project_name>"
16
- exit(1)
17
- end
18
- Scaffold.new(name).create
19
- else
20
- puts "Unknown command: #{command}"
21
- puts "Usage: rsodx new <project_name>"
22
- exit(1)
23
- end
24
- end
25
- end
26
- end