snowpack 1.0.0.alpha2

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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.tool-versions +1 -0
  5. data/.travis.yml +16 -0
  6. data/Brewfile +2 -0
  7. data/CHANGELOG.md +25 -0
  8. data/Gemfile +13 -0
  9. data/LICENSE +21 -0
  10. data/README.md +116 -0
  11. data/Rakefile +6 -0
  12. data/bin/bootstrap +25 -0
  13. data/exe/snowpack +7 -0
  14. data/lib/snowpack/application.rb +98 -0
  15. data/lib/snowpack/application_cli.rb +39 -0
  16. data/lib/snowpack/cli/application/cli.rb +32 -0
  17. data/lib/snowpack/cli/application/command.rb +53 -0
  18. data/lib/snowpack/cli/application/commands/assets/clobber.rb +31 -0
  19. data/lib/snowpack/cli/application/commands/assets/compile.rb +32 -0
  20. data/lib/snowpack/cli/application/commands/console.rb +55 -0
  21. data/lib/snowpack/cli/application/commands/db/create.rb +33 -0
  22. data/lib/snowpack/cli/application/commands/db/create_migration.rb +38 -0
  23. data/lib/snowpack/cli/application/commands/db/drop.rb +33 -0
  24. data/lib/snowpack/cli/application/commands/db/migrate.rb +47 -0
  25. data/lib/snowpack/cli/application/commands/db/reset.rb +28 -0
  26. data/lib/snowpack/cli/application/commands/db/rollback.rb +54 -0
  27. data/lib/snowpack/cli/application/commands/db/sample_data.rb +41 -0
  28. data/lib/snowpack/cli/application/commands/db/seed.rb +41 -0
  29. data/lib/snowpack/cli/application/commands/db/structure/dump.rb +38 -0
  30. data/lib/snowpack/cli/application/commands/db/utils/database.rb +76 -0
  31. data/lib/snowpack/cli/application/commands/db/utils/database_config.rb +49 -0
  32. data/lib/snowpack/cli/application/commands/db/version.rb +37 -0
  33. data/lib/snowpack/cli/application/commands/generate/slice.rb +32 -0
  34. data/lib/snowpack/cli/application/commands/routes/update.rb +35 -0
  35. data/lib/snowpack/cli/application/commands.rb +27 -0
  36. data/lib/snowpack/cli/command.rb +48 -0
  37. data/lib/snowpack/cli/standalone/cli.rb +14 -0
  38. data/lib/snowpack/cli/standalone/commands/new.rb +33 -0
  39. data/lib/snowpack/cli/standalone/commands.rb +13 -0
  40. data/lib/snowpack/components/formalist.rb +27 -0
  41. data/lib/snowpack/components/persistence.rb +56 -0
  42. data/lib/snowpack/components.rb +5 -0
  43. data/lib/snowpack/console/context.rb +32 -0
  44. data/lib/snowpack/console/plugins/relation_readers.rb +17 -0
  45. data/lib/snowpack/console/plugins/slice_readers.rb +33 -0
  46. data/lib/snowpack/generator.rb +57 -0
  47. data/lib/snowpack/generators/application/generator.rb +67 -0
  48. data/lib/snowpack/generators/application/templates/.env-example.tt +10 -0
  49. data/lib/snowpack/generators/application/templates/.env.test-example.tt +1 -0
  50. data/lib/snowpack/generators/application/templates/.gitignore +20 -0
  51. data/lib/snowpack/generators/application/templates/.rspec +2 -0
  52. data/lib/snowpack/generators/application/templates/Brewfile +4 -0
  53. data/lib/snowpack/generators/application/templates/Gemfile.tt +46 -0
  54. data/lib/snowpack/generators/application/templates/Guardfile +7 -0
  55. data/lib/snowpack/generators/application/templates/Procfile.dev +3 -0
  56. data/lib/snowpack/generators/application/templates/Procfile.support +1 -0
  57. data/lib/snowpack/generators/application/templates/README.md.tt +45 -0
  58. data/lib/snowpack/generators/application/templates/Rakefile +5 -0
  59. data/lib/snowpack/generators/application/templates/bin/run +7 -0
  60. data/lib/snowpack/generators/application/templates/config/application.rb.tt +19 -0
  61. data/lib/snowpack/generators/application/templates/config/puma.rb.tt +14 -0
  62. data/lib/snowpack/generators/application/templates/config/routes.rb.tt +15 -0
  63. data/lib/snowpack/generators/application/templates/config.ru +5 -0
  64. data/lib/snowpack/generators/application/templates/db/migrate/.keep +0 -0
  65. data/lib/snowpack/generators/application/templates/db/sample_data.rb +1 -0
  66. data/lib/snowpack/generators/application/templates/db/seed.rb +1 -0
  67. data/lib/snowpack/generators/application/templates/lib/__application_path__/operation.rb.tt +12 -0
  68. data/lib/snowpack/generators/application/templates/lib/__application_path__/persistence/relations/.keep +0 -0
  69. data/lib/snowpack/generators/application/templates/lib/__application_path__/types.rb.tt +11 -0
  70. data/lib/snowpack/generators/application/templates/lib/__application_path__/web/action.rb.tt +56 -0
  71. data/lib/snowpack/generators/application/templates/lib/__application_path__/web/view/context.rb.tt +31 -0
  72. data/lib/snowpack/generators/application/templates/lib/hanami/action/csrf_protection.rb +225 -0
  73. data/lib/snowpack/generators/application/templates/log/.keep +0 -0
  74. data/lib/snowpack/generators/application/templates/package.json.tt +25 -0
  75. data/lib/snowpack/generators/application/templates/public/.keep +0 -0
  76. data/lib/snowpack/generators/application/templates/script/bootstrap +32 -0
  77. data/lib/snowpack/generators/application/templates/script/console +26 -0
  78. data/lib/snowpack/generators/application/templates/script/server +13 -0
  79. data/lib/snowpack/generators/application/templates/script/setup +27 -0
  80. data/lib/snowpack/generators/application/templates/script/support +9 -0
  81. data/lib/snowpack/generators/application/templates/script/test +19 -0
  82. data/lib/snowpack/generators/application/templates/script/update +13 -0
  83. data/lib/snowpack/generators/application/templates/spec/spec_helper.rb +13 -0
  84. data/lib/snowpack/generators/application/templates/spec/suite/.keep +0 -0
  85. data/lib/snowpack/generators/application/templates/spec/support/suite.rb +9 -0
  86. data/lib/snowpack/generators/application/templates/system/__application_path__/import.rb.tt +5 -0
  87. data/lib/snowpack/generators/application/templates/system/boot/assets.rb.tt +17 -0
  88. data/lib/snowpack/generators/application/templates/system/boot/logger.rb.tt +10 -0
  89. data/lib/snowpack/generators/application/templates/system/boot/monitor.rb.tt +9 -0
  90. data/lib/snowpack/generators/application/templates/system/boot/persistence.rb.tt +9 -0
  91. data/lib/snowpack/generators/application/templates/system/boot/settings.rb.tt +18 -0
  92. data/lib/snowpack/generators/application/templates/system/boot/web.rb.tt +9 -0
  93. data/lib/snowpack/generators/slice/generator.rb +62 -0
  94. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/.keep +0 -0
  95. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/web/action.rb.tt +10 -0
  96. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/web/actions/.keep +0 -0
  97. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/web/view.rb.tt +17 -0
  98. data/lib/snowpack/generators/slice/templates/lib/__slice_path__/web/views/.keep +0 -0
  99. data/lib/snowpack/generators/slice/templates/system/__slice_path__/import.rb.tt +9 -0
  100. data/lib/snowpack/generators/slice/templates/system/__slice_path__/slice.rb.tt +10 -0
  101. data/lib/snowpack/instrumentation/appsignal/appsignal_ext.rb +13 -0
  102. data/lib/snowpack/instrumentation/appsignal/que.rb +70 -0
  103. data/lib/snowpack/instrumentation/appsignal/rack.rb +84 -0
  104. data/lib/snowpack/roda/web.rb +47 -0
  105. data/lib/snowpack/slice.rb +85 -0
  106. data/lib/snowpack/test/suite.rb +164 -0
  107. data/lib/snowpack/test/tasks.rake +40 -0
  108. data/lib/snowpack/test_tasks.rb +4 -0
  109. data/lib/snowpack/types.rb +9 -0
  110. data/lib/snowpack/version.rb +5 -0
  111. data/lib/snowpack/view/part_builder.rb +45 -0
  112. data/lib/snowpack/view/parts/pager.rb +107 -0
  113. data/lib/snowpack/view/parts/paginated.rb +22 -0
  114. data/lib/snowpack/web/application.rb +38 -0
  115. data/lib/snowpack/web/assets.rb +56 -0
  116. data/lib/snowpack/web/endpoint_resolver.rb +66 -0
  117. data/lib/snowpack/web/form.rb +38 -0
  118. data/lib/snowpack/web/plugin.rb +44 -0
  119. data/lib/snowpack/web/rack_logger.rb +73 -0
  120. data/lib/snowpack/web/router.rb +58 -0
  121. data/lib/snowpack/web.rb +20 -0
  122. data/lib/snowpack.rb +27 -0
  123. data/snowpack.gemspec +34 -0
  124. metadata +327 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appsignal"
4
+ require_relative "appsignal_ext"
5
+ require "securerandom"
6
+
7
+ # Remove Appsignal's own Que integration hook, so the Snowpack-hosted
8
+ # integration can be included manually.
9
+ Appsignal::Hooks.hooks.delete :que
10
+
11
+ module Snowpack
12
+ module Instrumentation
13
+ module Appsignal
14
+ module Que
15
+ # Enable like so:
16
+ #
17
+ # Que.error_notifier = Snowpack::Instrumentation::Appsignal::Que::ErrorNotifier
18
+
19
+ # Actually, why is this even necessry?
20
+ ErrorNotifier = -> error, _job {
21
+ ::Appsignal::Transaction.current.set_error(error)
22
+ }
23
+
24
+ def self.included(base)
25
+ base.class_eval do
26
+ def _run_with_appsignal(*args)
27
+ env = {
28
+ :metadata => {
29
+ :id => attrs[:job_id],
30
+ :queue => attrs[:queue],
31
+ :run_at => attrs[:run_at].to_s,
32
+ :priority => attrs[:priority],
33
+ :attempts => attrs[:error_count].to_i
34
+ },
35
+ :params => attrs[:args]
36
+ }
37
+
38
+ request = ::Appsignal::Transaction::GenericRequest.new(env)
39
+
40
+ transaction = ::Appsignal::Transaction.create(
41
+ SecureRandom.uuid,
42
+ ::Appsignal::Transaction::BACKGROUND_JOB,
43
+ request,
44
+ )
45
+
46
+ transaction.discard! unless instrument?
47
+
48
+ begin
49
+ ::Appsignal.instrument("perform_job.que") { _run_without_appsignal(*args) }
50
+ rescue Exception => error
51
+ transaction.set_error(error)
52
+ raise error
53
+ ensure
54
+ transaction.set_action "#{attrs[:job_class]}#run"
55
+ ::Appsignal::Transaction.complete_current!
56
+ end
57
+ end
58
+
59
+ alias_method :_run_without_appsignal, :_run
60
+ alias_method :_run, :_run_with_appsignal
61
+
62
+ def instrument?
63
+ true
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # use Snowpack::Instrumentation::Rack::Appsignal, {
4
+ # sample: 0.2,
5
+ # filters: {
6
+ # "/some/exact/route" => 0.05, # <- custom sample rate (5%) for specific URL
7
+ # %r{/another/.+/route} => false, # <- do not instrument this URL, n.b. match via Regexp
8
+ # },
9
+ # }
10
+
11
+ require "appsignal"
12
+ require_relative "appsignal_ext"
13
+ require "rack"
14
+ require "securerandom"
15
+
16
+ module Snowpack
17
+ module Instrumentation
18
+ module Appsignal
19
+ class Rack
20
+ attr_reader :app, :options
21
+
22
+ def initialize(app, **options)
23
+ @app = app
24
+ @options = options
25
+ end
26
+
27
+ def call(env)
28
+ if ::Appsignal.active?
29
+ call_with_appsignal_monitoring(env)
30
+ else
31
+ app.call(env)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def call_with_appsignal_monitoring(env)
38
+ request = ::Rack::Request.new(env)
39
+
40
+ transaction = ::Appsignal::Transaction.create(
41
+ SecureRandom.uuid,
42
+ ::Appsignal::Transaction::HTTP_REQUEST,
43
+ request,
44
+ )
45
+
46
+ transaction.discard! unless sample?(request)
47
+
48
+ begin
49
+ ::Appsignal.instrument "process_action.generic" do
50
+ app.call(env)
51
+ end
52
+ rescue Exception => error
53
+ transaction.set_error(error)
54
+ raise error
55
+ ensure
56
+ if env["appsignal.route"]
57
+ transaction.set_action_if_nil(env["appsignal.route"])
58
+ else
59
+ transaction.set_action_if_nil("unknown")
60
+ end
61
+
62
+ transaction.set_metadata("path", request.path)
63
+ transaction.set_metadata("method", request.request_method)
64
+
65
+ transaction.set_http_or_background_queue_start
66
+ ::Appsignal::Transaction.complete_current!
67
+ end
68
+ end
69
+
70
+ def sample?(request)
71
+ rate = options.fetch(:sample, 1)
72
+
73
+ options.fetch(:filters, {}).each do |match, sample|
74
+ rate = sample and break if request.path.match?(match)
75
+ end
76
+
77
+ rate = 1 if rate == true
78
+
79
+ rate && rand <= rate
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO wrap in begin/rescue loaderror and raise a meaningful exception
4
+ require "roda"
5
+ require "roda/plugins/flow"
6
+
7
+ module Snowpack
8
+ module Roda
9
+ # Mostly copied from Dry::Web::Roda::Application
10
+ class Web < ::Roda
11
+ extend Dry::Configurable
12
+
13
+ setting :container, reader: true
14
+ setting :routes
15
+
16
+ plugin :flow
17
+
18
+ # Disable this so we can better control when it's used
19
+ #
20
+ # def self.configure(&block)
21
+ # super.tap do
22
+ # use(container[:rack_monitor]) if container.config.listeners
23
+ # end
24
+ # end
25
+
26
+ def self.resolve(name)
27
+ container[name]
28
+ end
29
+
30
+ def self.[](name)
31
+ container[name]
32
+ end
33
+
34
+ def self.load_routes!
35
+ Dir[root.join("#{config.routes}/**/*.rb")].each { |f| require f }
36
+ end
37
+
38
+ def self.root
39
+ container.config.root
40
+ end
41
+
42
+ # def notifications
43
+ # self.class[:notifications]
44
+ # end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "../snowpack"
5
+
6
+ module Snowpack
7
+ class Slice < Dry::System::Container
8
+ use :env, inferrer: -> { ENV.fetch('RACK_ENV', 'development').to_sym }
9
+
10
+ def self.inherited(klass)
11
+ super
12
+
13
+ if klass.superclass == Snowpack::Slice
14
+ raise "Snowpack.application not configured yet" unless Snowpack.application?
15
+
16
+ app = Snowpack.application
17
+
18
+ klass.instance_variable_set :@application, app
19
+
20
+ klass.config.env = app.env
21
+ klass.config.name = klass.slice_name.to_sym
22
+ klass.config.auto_register = [File.join("lib", klass.slice_namespace_path)]
23
+ klass.config.default_namespace = klass.slice_namespace_identifier_prefix
24
+
25
+ slice_path = File.join(app.config.root, app.config.slices_dir, klass.slice_name)
26
+ if File.directory?(slice_path)
27
+ klass.config.root = slice_path if File.directory?(slice_path)
28
+ klass.load_paths! "lib"
29
+ end
30
+
31
+ klass.import application: app
32
+ end
33
+ end
34
+
35
+ def self.import_slice(*slices)
36
+ @slice_imports ||= []
37
+ @slice_imports += slices
38
+ @slice_imports.uniq!
39
+ self
40
+ end
41
+
42
+ def self.application
43
+ @application
44
+ end
45
+
46
+ def self.boot!
47
+ finalize!
48
+ end
49
+
50
+ def self.finalize!
51
+ # Force `after :configure` hooks to run
52
+ configure do; end
53
+
54
+ super do
55
+ Array(@slice_imports).each do |slice|
56
+ import slice => application.slices.fetch(slice)
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ MODULE_DELIMITER = "::"
64
+
65
+ def self.slice_namespace
66
+ inflector.constantize(name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER))
67
+ end
68
+
69
+ def self.slice_namespace_path
70
+ inflector.underscore(slice_namespace.to_s)
71
+ end
72
+
73
+ def self.slice_namespace_identifier_prefix
74
+ slice_namespace_path.gsub("/", ".")
75
+ end
76
+
77
+ def self.slice_name
78
+ inflector.underscore(slice_namespace.to_s.split(MODULE_DELIMITER).last)
79
+ end
80
+
81
+ def self.inflector
82
+ application[:inflector]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core'
4
+ require 'json'
5
+
6
+ require_relative "../../snowpack"
7
+
8
+ module Snowpack
9
+ module Test
10
+ module Helpers
11
+ module_function
12
+
13
+ def suite
14
+ @__suite__ ||= RSpec.configuration.suite
15
+ end
16
+ end
17
+
18
+ class Suite
19
+ DB_CLEANUP_PATH_REGEX = /(features|integration)/
20
+
21
+ class << self
22
+ attr_accessor :current
23
+
24
+ def root
25
+ @__root__ ||= Pathname(Dir.pwd).join("spec").freeze
26
+ end
27
+
28
+ def instance
29
+ @__instance__ ||= new
30
+ end
31
+ end
32
+
33
+ def self.inherited(klass)
34
+ super
35
+ Suite.current = klass
36
+ end
37
+
38
+ def self.configure
39
+ RSpec.configure do |config|
40
+ config.add_setting :suite
41
+
42
+ config.suite = self.new
43
+
44
+ config.include Helpers
45
+
46
+ config.disable_monkey_patching!
47
+
48
+ config.expect_with :rspec do |expectations|
49
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
50
+ end
51
+
52
+ config.mock_with :rspec do |mocks|
53
+ mocks.verify_partial_doubles = true
54
+ end
55
+
56
+ config.filter_run :focus
57
+ config.run_all_when_everything_filtered = true
58
+
59
+ config.example_status_persistence_file_path = root.join("../tmp").realpath.join("spec/examples.txt").to_s
60
+
61
+ if config.files_to_run.one?
62
+ config.default_formatter = "doc"
63
+ end
64
+
65
+ config.profile_examples = 10
66
+
67
+ config.order = :random
68
+
69
+ Kernel.srand config.seed
70
+
71
+ yield(config) if block_given?
72
+ end
73
+ end
74
+
75
+ attr_reader :application, :root
76
+
77
+ def initialize(application: Snowpack.application, root: self.class.root.join("suite"))
78
+ @application = application
79
+ @root = root
80
+
81
+ application.load_slices
82
+ end
83
+
84
+ def start_coverage
85
+ return unless coverage?
86
+
87
+ require 'simplecov'
88
+
89
+ if parallel?
90
+ SimpleCov.command_name(test_group_name)
91
+ end
92
+
93
+ SimpleCov.start do
94
+ add_filter '/spec/'
95
+ add_filter '/system/'
96
+ end
97
+ end
98
+
99
+ def coverage_threshold
100
+ ENV.fetch('COVERAGE_THRESHOLD').to_f.round
101
+ end
102
+
103
+ def current_coverage
104
+ data = JSON.load(application.root.join('coverage/.last_run.json'))
105
+ data.fetch('result').fetch('covered_percent').to_f.round
106
+ end
107
+
108
+ def test_group_name
109
+ @__test_group_name__ ||= "test_suite_#{build_idx}"
110
+ end
111
+
112
+ def chdir(name)
113
+ self.class.new(
114
+ application: application,
115
+ root: root.join(name.to_s),
116
+ )
117
+ end
118
+
119
+ def files
120
+ dirs.map { |dir| dir.join("**/*_spec.rb") }.flat_map { |path| Dir[path] }.sort
121
+ end
122
+
123
+ def groups
124
+ dirs.map(&:basename).map(&:to_s).map(&:to_sym).sort
125
+ end
126
+
127
+ def dirs
128
+ Dir[root.join("*")].map(&Kernel.method(:Pathname)).select(&:directory?)
129
+ end
130
+
131
+ def build_idx
132
+ ENV.fetch('CIRCLE_NODE_INDEX', -1).to_i
133
+ end
134
+
135
+ def coverage?
136
+ ENV['COVERAGE'] == 'true'
137
+ end
138
+
139
+ def ci?
140
+ !ENV['CIRCLECI'].nil?
141
+ end
142
+
143
+ def parallel?
144
+ ENV['CIRCLE_NODE_TOTAL'].to_i > 1
145
+ end
146
+
147
+ def capybara_server_port
148
+ 3001
149
+ end
150
+
151
+ def log_dir
152
+ application.root.join("log").to_s
153
+ end
154
+
155
+ def tmp_dir
156
+ application.root.join("tmp").to_s
157
+ end
158
+
159
+ def clean_db?(example)
160
+ DB_CLEANUP_PATH_REGEX.match?(example.metadata[:file_path])
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,40 @@
1
+ begin
2
+ require "rspec/core/rake_task"
3
+ require "snowpack/test/suite"
4
+
5
+ require Snowpack::Test::Suite.root.join("support/suite")
6
+ suite = Snowpack::Test::Suite.current.new
7
+
8
+ desc "Run all specs"
9
+ RSpec::Core::RakeTask.new :spec do |t|
10
+ opts = ["#{suite.root} --pattern **/*_spec.rb"]
11
+
12
+ if suite.ci?
13
+ opts << "--format RspecJunitFormatter"
14
+ opts << "--out /tmp/test-results/rspec.xml"
15
+ opts << "--format progress"
16
+ end
17
+
18
+ t.rspec_opts = opts.join(" ")
19
+ end
20
+
21
+ namespace :spec do
22
+ suite.groups.each do |group|
23
+ desc "Run #{group} specs"
24
+ RSpec::Core::RakeTask.new(group) do |t|
25
+ t.rspec_opts = "#{suite.chdir(group).root} --pattern **/*_spec.rb"
26
+ end
27
+ end
28
+
29
+ desc 'Verify coverage'
30
+ task :verify_coverage do
31
+ if suite.current_coverage.to_i < suite.coverage_threshold
32
+ puts "Coverage too low. Current: #{suite.current_coverage}%; Expected min: #{suite.coverage_threshold}%"
33
+ exit 1
34
+ end
35
+ end
36
+ end
37
+
38
+ task default: [:spec]
39
+ rescue LoadError; end
40
+ # FIXME ^ make that way more specific
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ load "snowpack/test/tasks.rake"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types"
4
+
5
+ module Snowpack
6
+ module Types
7
+ include Dry.Types(default: :strict)
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snowpack
4
+ VERSION = "1.0.0.alpha2"
5
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/view/part_builder"
4
+ require "snowpack/view/parts/pager"
5
+ require_relative "parts/paginated"
6
+
7
+ module Snowpack
8
+ module View
9
+ class PartBuilder < Dry::View::PartBuilder
10
+ def call(name, value, **options)
11
+ if paginated_collection?(value)
12
+ build_paginated_collection_part(name, value, **options)
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def paginated_collection?(value)
21
+ value.respond_to?(:to_a) && value.respond_to?(:pager)
22
+ end
23
+
24
+ def build_paginated_collection_part(name, value, **options)
25
+ pager_part = build_part(:pager, value.pager, as: Snowpack::View::Parts::Pager)
26
+
27
+ collection_as = collection_options(name: name, **options)[:as]
28
+ item_name, item_as = collection_item_options(name: name, **options).values_at(:name, :as)
29
+
30
+ arr = value.to_ary.map { |obj|
31
+ call(item_name, obj, **options.merge(as: item_as))
32
+ }
33
+
34
+ collection_part_class = part_class(name: name, fallback_class: Parts::Paginated, **options.merge(as: collection_as))
35
+
36
+ collection_part_class.new(
37
+ name: name,
38
+ value: arr,
39
+ pager: pager_part,
40
+ render_env: render_env,
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/view/part"
4
+ require "rack/utils"
5
+ require "uri"
6
+
7
+ module Snowpack
8
+ module View
9
+ module Parts
10
+ class Pager < Dry::View::Part
11
+ attr_reader :part_args
12
+ attr_reader :buffer
13
+
14
+ def initialize(buffer: 3, **part_args)
15
+ @part_args = part_args
16
+ @buffer = buffer
17
+
18
+ super(**part_args)
19
+ end
20
+
21
+ def with(**options)
22
+ new_options = {
23
+ buffer: buffer
24
+ }.merge(options)
25
+
26
+ self.class.new(**new_options, **part_args)
27
+ end
28
+
29
+ def first_page
30
+ 1
31
+ end
32
+
33
+ def first_page?
34
+ current_page == first_page
35
+ end
36
+
37
+ def last_page
38
+ total_pages
39
+ end
40
+
41
+ def last_page?
42
+ current_page == last_page
43
+ end
44
+
45
+ def prev_gap?
46
+ current_page - buffer > first_page
47
+ end
48
+
49
+ def next_gap?
50
+ current_page + buffer < last_page
51
+ end
52
+
53
+ def single_page?
54
+ total_pages <= 1
55
+ end
56
+
57
+ def all_page_numbers
58
+ (first_page..last_page).to_a
59
+ end
60
+
61
+ def each_entry
62
+ return to_enum(:each_entry) unless block_given?
63
+
64
+ entry_numbers.each do |number|
65
+ yield Entry.new(number, current: number == current_page)
66
+ end
67
+ end
68
+
69
+ def url_for_page(page)
70
+ uri = URI(_context.fullpath)
71
+ params = Rack::Utils.parse_query(uri.query).merge("page" => page.to_s)
72
+ uri.query = Rack::Utils.build_query(params)
73
+ uri.to_s
74
+ end
75
+
76
+ private
77
+
78
+ def entry_numbers
79
+ left_edge = current_page - buffer
80
+ left_edge = 1 if left_edge < 1
81
+
82
+ right_edge = current_page + buffer
83
+ right_edge = total_pages if right_edge > total_pages
84
+
85
+ left_edge.upto(right_edge).to_a
86
+ end
87
+
88
+ class Entry
89
+ attr_reader :number
90
+
91
+ def initialize(number, current:)
92
+ @number = number
93
+ @current = current
94
+ end
95
+
96
+ def to_s
97
+ number.to_s
98
+ end
99
+
100
+ def current?
101
+ !!@current
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/view/part"
4
+
5
+ module Snowpack
6
+ module View
7
+ module Parts
8
+ class Paginated < Dry::View::Part
9
+ attr_reader :pager
10
+
11
+ def initialize(pager:, **part_args)
12
+ @pager = pager
13
+ super(**part_args)
14
+ end
15
+
16
+ def pagination(locals: {}, **options)
17
+ pager.with(**options).render(:pagination, **locals)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end