plan_my_stuff 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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +28 -0
  3. data/README.md +284 -0
  4. data/app/controllers/plan_my_stuff/application_controller.rb +76 -0
  5. data/app/controllers/plan_my_stuff/comments_controller.rb +82 -0
  6. data/app/controllers/plan_my_stuff/issues_controller.rb +145 -0
  7. data/app/controllers/plan_my_stuff/labels_controller.rb +30 -0
  8. data/app/controllers/plan_my_stuff/project_items_controller.rb +93 -0
  9. data/app/controllers/plan_my_stuff/projects_controller.rb +17 -0
  10. data/app/views/plan_my_stuff/comments/edit.html.erb +16 -0
  11. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +32 -0
  12. data/app/views/plan_my_stuff/issues/edit.html.erb +12 -0
  13. data/app/views/plan_my_stuff/issues/index.html.erb +37 -0
  14. data/app/views/plan_my_stuff/issues/new.html.erb +7 -0
  15. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +41 -0
  16. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +23 -0
  17. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +32 -0
  18. data/app/views/plan_my_stuff/issues/show.html.erb +58 -0
  19. data/app/views/plan_my_stuff/projects/index.html.erb +13 -0
  20. data/app/views/plan_my_stuff/projects/show.html.erb +101 -0
  21. data/config/routes.rb +25 -0
  22. data/lib/generators/plan_my_stuff/install/install_generator.rb +38 -0
  23. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +106 -0
  24. data/lib/generators/plan_my_stuff/views/views_generator.rb +22 -0
  25. data/lib/plan_my_stuff/application_record.rb +39 -0
  26. data/lib/plan_my_stuff/base_metadata.rb +136 -0
  27. data/lib/plan_my_stuff/client.rb +143 -0
  28. data/lib/plan_my_stuff/comment.rb +360 -0
  29. data/lib/plan_my_stuff/comment_metadata.rb +56 -0
  30. data/lib/plan_my_stuff/configuration.rb +139 -0
  31. data/lib/plan_my_stuff/custom_fields.rb +65 -0
  32. data/lib/plan_my_stuff/engine.rb +11 -0
  33. data/lib/plan_my_stuff/errors.rb +87 -0
  34. data/lib/plan_my_stuff/issue.rb +486 -0
  35. data/lib/plan_my_stuff/issue_metadata.rb +111 -0
  36. data/lib/plan_my_stuff/label.rb +59 -0
  37. data/lib/plan_my_stuff/markdown.rb +83 -0
  38. data/lib/plan_my_stuff/metadata_parser.rb +53 -0
  39. data/lib/plan_my_stuff/project.rb +504 -0
  40. data/lib/plan_my_stuff/project_item.rb +414 -0
  41. data/lib/plan_my_stuff/test_helpers.rb +501 -0
  42. data/lib/plan_my_stuff/user_resolver.rb +61 -0
  43. data/lib/plan_my_stuff/verifier.rb +102 -0
  44. data/lib/plan_my_stuff/version.rb +19 -0
  45. data/lib/plan_my_stuff.rb +69 -0
  46. data/lib/tasks/plan_my_stuff.rake +23 -0
  47. metadata +126 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ class CommentMetadata < BaseMetadata
5
+ # @return [Boolean] true if this comment holds the issue's body content
6
+ attr_accessor :issue_body
7
+
8
+ class << self
9
+ # Builds a CommentMetadata from a parsed hash (e.g. from MetadataParser)
10
+ #
11
+ # @param hash [Hash]
12
+ #
13
+ # @return [CommentMetadata]
14
+ #
15
+ def from_hash(hash)
16
+ metadata = new
17
+ apply_common_from_hash(metadata, hash)
18
+ metadata.issue_body = hash[:issue_body] || false
19
+
20
+ metadata
21
+ end
22
+
23
+ # Builds a new CommentMetadata for comment creation, auto-filling gem defaults
24
+ #
25
+ # @param user [Object, Integer] user object or user_id
26
+ # @param visibility [String] "public" or "internal"
27
+ # @param custom_fields [Hash] app-defined field values
28
+ # @param issue_body [Boolean] whether this comment holds the issue body
29
+ #
30
+ # @return [CommentMetadata]
31
+ #
32
+ def build(user:, visibility: 'internal', custom_fields: {}, issue_body: false)
33
+ metadata = new
34
+ apply_common_build(metadata, user: user, visibility: visibility, custom_fields_data: custom_fields)
35
+ metadata.issue_body = issue_body
36
+
37
+ metadata
38
+ end
39
+ end
40
+
41
+ def initialize
42
+ super
43
+ @issue_body = false
44
+ end
45
+
46
+ # @return [Boolean]
47
+ def issue_body?
48
+ issue_body == true
49
+ end
50
+
51
+ # @return [Hash]
52
+ def to_h
53
+ super.merge(issue_body: issue_body)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ class Configuration
5
+ # @return [String] GitHub PAT with repo and project scopes. Required.
6
+ attr_accessor :access_token
7
+
8
+ # @return [String] GitHub organization name. Required.
9
+ attr_accessor :organization
10
+
11
+ # @return [Symbol, nil] default repo key used when callers omit repo: param
12
+ attr_accessor :default_repo
13
+
14
+ # @return [Integer, nil] default GitHub Projects V2 number for add_to_project calls
15
+ attr_accessor :default_project_number
16
+
17
+ # @return [String] consuming app's user model class name, constantized for lookups
18
+ attr_accessor :user_class
19
+
20
+ # @return [Symbol] method called on user object to get display name for comment headers
21
+ attr_accessor :display_name_method
22
+
23
+ # @return [Symbol] method called on user object to extract the app-side user ID
24
+ attr_accessor :user_id_method
25
+
26
+ # Determines if a user is support staff. Symbol (method name on user) or Proc that
27
+ # receives the user object and returns boolean.
28
+ #
29
+ # @return [Symbol, Proc]
30
+ #
31
+ attr_accessor :support_method
32
+
33
+ # @return [Symbol] which markdown gem to use: :commonmarker or :redcarpet
34
+ attr_accessor :markdown_renderer
35
+
36
+ # Default options passed to the markdown renderer. Per-call options in
37
+ # Markdown.render merge on top of these.
38
+ #
39
+ # For :commonmarker - passed as `options:` to `Commonmarker.to_html`
40
+ # e.g. `{ render: { hardbreaks: true } }`
41
+ #
42
+ # For :redcarpet - :render_options and :renderer are extracted for the HTML renderer;
43
+ # remaining keys are passed as extensions to `Redcarpet::Markdown.new`
44
+ # e.g. `{ render_options: { hard_wrap: true, no_styles: true }, autolink: true }`
45
+ #
46
+ # @return [Hash]
47
+ #
48
+ attr_accessor :markdown_options
49
+
50
+ # Proc returning boolean, or nil (always send). When it returns false the request is
51
+ # deferred to a background job instead of hitting GitHub.
52
+ #
53
+ # @return [Proc, nil]
54
+ #
55
+ attr_accessor :should_send_request
56
+
57
+ # Map of action type to job class name for deferred requests.
58
+ # Keys: :create_ticket, :post_comment, :update_status.
59
+ #
60
+ # @return [Hash{Symbol => String}]
61
+ #
62
+ attr_accessor :job_classes
63
+
64
+ # @return [Proc, nil] custom notifier for deferred requests, or nil to use DeferredMailer
65
+ attr_accessor :deferred_notifier
66
+
67
+ # @return [String, nil] sender address for built-in deferred request notifications
68
+ attr_accessor :deferred_email_from
69
+
70
+ # @return [String, nil] recipient address for built-in deferred request notifications
71
+ attr_accessor :deferred_email_to
72
+
73
+ # App-defined field definitions stored in issue/comment metadata.
74
+ # Keys are field names, values are hashes with :type and :required.
75
+ #
76
+ # @return [Hash{Symbol => Hash}]
77
+ #
78
+ attr_accessor :custom_fields
79
+
80
+ # @return [String, nil] URL prefix for building user-facing ticket URLs in the consuming app
81
+ attr_accessor :issues_url_prefix
82
+
83
+ # @return [String, nil] name of the consuming app, stored in metadata (e.g. "Atlas")
84
+ attr_accessor :app_name
85
+
86
+ # Named repo configs. Set via config.repos[:element] = 'BrandsInsurance/Element'.
87
+ #
88
+ # @return [Hash{Symbol => String}]
89
+ #
90
+ attr_reader :repos
91
+
92
+ # @return [Configuration]
93
+ def initialize
94
+ @repos = {}
95
+ @user_class = 'User'
96
+ @display_name_method = :to_s
97
+ @user_id_method = :id
98
+ @support_method = :support?
99
+ @markdown_renderer = :commonmarker
100
+ @markdown_options = {}
101
+ @job_classes = {}
102
+ @custom_fields = {}
103
+ end
104
+
105
+ # Sets the authentication block for engine controllers.
106
+ #
107
+ # @return [void]
108
+ #
109
+ def authenticate_with(&block)
110
+ if block
111
+ @authenticate_with = block
112
+ else
113
+ @authenticate_with
114
+ end
115
+ end
116
+
117
+ # Validates that required configuration options are set.
118
+ #
119
+ # @raise [ConfigurationError] if required options are missing
120
+ #
121
+ # @return [void]
122
+ #
123
+ def validate!
124
+ missing = []
125
+ missing << 'access_token' if access_token.nil? || access_token.to_s.strip.empty?
126
+ missing << 'organization' if organization.nil? || organization.to_s.strip.empty?
127
+
128
+ return if missing.empty?
129
+
130
+ raise(
131
+ ConfigurationError,
132
+ "Missing required PlanMyStuff configuration: #{missing.join(', ')}",
133
+ )
134
+ end
135
+ end
136
+
137
+ class ConfigurationError < StandardError
138
+ end
139
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ # Dynamic accessor object for app-defined custom fields stored in metadata.
5
+ # Backed by the config.custom_fields schema, provides both hash-style and
6
+ # method-style access to field values.
7
+ class CustomFields
8
+ # @param schema [Hash{Symbol => Hash}] field definitions from config.custom_fields
9
+ # @param data [Hash] parsed field data from metadata JSON
10
+ #
11
+ def initialize(schema, data = {})
12
+ @schema = schema || {}
13
+ @data = (data || {}).transform_keys(&:to_sym)
14
+ end
15
+
16
+ # @param key [Symbol, String]
17
+ #
18
+ # @return [Object]
19
+ #
20
+ def [](key)
21
+ @data[key.to_sym]
22
+ end
23
+
24
+ # @param key [Symbol, String]
25
+ # @param value [Object]
26
+ #
27
+ # @return [Object]
28
+ #
29
+ def []=(key, value)
30
+ @data[key.to_sym] = value
31
+ end
32
+
33
+ # @return [Hash]
34
+ def to_h
35
+ @data.dup
36
+ end
37
+
38
+ # @return [String]
39
+ def to_json(...)
40
+ to_h.to_json(...)
41
+ end
42
+
43
+ private
44
+
45
+ def respond_to_missing?(method_name, include_private = false)
46
+ key = method_name.to_s.delete_suffix('=').to_sym
47
+ @schema.key?(key) || @data.key?(key) || super
48
+ end
49
+
50
+ def method_missing(method_name, *args)
51
+ name = method_name.to_s
52
+
53
+ if name.end_with?('=')
54
+ key = name.delete_suffix('=').to_sym
55
+ if @schema.key?(key) || @data.key?(key)
56
+ return @data[key] = args.first
57
+ end
58
+ elsif @schema.key?(method_name) || @data.key?(method_name)
59
+ return @data[method_name]
60
+ end
61
+
62
+ super
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace PlanMyStuff
6
+
7
+ rake_tasks do
8
+ load(File.expand_path('../tasks/plan_my_stuff.rake', __dir__))
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ # Base error for all PlanMyStuff errors
5
+ class Error < StandardError
6
+ end
7
+
8
+ # Raised when GitHub REST API returns a non-success HTTP status
9
+ class APIError < Error
10
+ # @return [Integer]
11
+ attr_reader :status
12
+
13
+ # @param message [String]
14
+ # @param status [Integer]
15
+ #
16
+ def initialize(message = nil, status: nil)
17
+ @status = status
18
+ super(message)
19
+ end
20
+ end
21
+
22
+ # Raised when GitHub GraphQL API returns errors in the response body
23
+ class GraphQLError < Error
24
+ # @return [Array<Hash>]
25
+ attr_reader :errors
26
+
27
+ # @param message [String]
28
+ # @param errors [Array<Hash>]
29
+ #
30
+ def initialize(message = nil, errors: [])
31
+ @errors = errors
32
+ super(message)
33
+ end
34
+ end
35
+
36
+ # Raised when GitHub rate limit is exhausted (429 or rate limit headers)
37
+ class RateLimitError < Error
38
+ # @return [Time]
39
+ attr_reader :retry_after
40
+
41
+ # @param message [String]
42
+ # @param retry_after [Time]
43
+ #
44
+ def initialize(message = nil, retry_after: nil)
45
+ @retry_after = retry_after
46
+ super(message)
47
+ end
48
+ end
49
+
50
+ # Raised when an object has been modified remotely since it was loaded
51
+ class StaleObjectError < Error
52
+ # @return [Time, nil]
53
+ attr_reader :local_updated_at
54
+
55
+ # @return [Time, nil]
56
+ attr_reader :remote_updated_at
57
+
58
+ # @param message [String]
59
+ # @param local_updated_at [Time, nil]
60
+ # @param remote_updated_at [Time, nil]
61
+ #
62
+ def initialize(message = nil, local_updated_at: nil, remote_updated_at: nil)
63
+ @local_updated_at = local_updated_at
64
+ @remote_updated_at = remote_updated_at
65
+ super(message)
66
+ end
67
+ end
68
+
69
+ # Raised when custom field validation fails (Phase 1)
70
+ class ValidationError < Error
71
+ # @return [String, nil]
72
+ attr_reader :field
73
+
74
+ # @return [Symbol, nil]
75
+ attr_reader :expected_type
76
+
77
+ # @param message [String]
78
+ # @param field [String, nil]
79
+ # @param expected_type [Symbol, nil]
80
+ #
81
+ def initialize(message = nil, field: nil, expected_type: nil)
82
+ @field = field
83
+ @expected_type = expected_type
84
+ super(message)
85
+ end
86
+ end
87
+ end