ngmoco-request-log-analyzer 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. data/.gitignore +10 -0
  2. data/DESIGN.rdoc +41 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +8 -0
  6. data/bin/request-log-analyzer +114 -0
  7. data/lib/cli/command_line_arguments.rb +301 -0
  8. data/lib/cli/database_console.rb +26 -0
  9. data/lib/cli/database_console_init.rb +43 -0
  10. data/lib/cli/progressbar.rb +213 -0
  11. data/lib/cli/tools.rb +46 -0
  12. data/lib/request_log_analyzer.rb +44 -0
  13. data/lib/request_log_analyzer/aggregator.rb +49 -0
  14. data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
  15. data/lib/request_log_analyzer/aggregator/echo.rb +29 -0
  16. data/lib/request_log_analyzer/aggregator/summarizer.rb +175 -0
  17. data/lib/request_log_analyzer/controller.rb +332 -0
  18. data/lib/request_log_analyzer/database.rb +102 -0
  19. data/lib/request_log_analyzer/database/base.rb +115 -0
  20. data/lib/request_log_analyzer/database/connection.rb +38 -0
  21. data/lib/request_log_analyzer/database/request.rb +22 -0
  22. data/lib/request_log_analyzer/database/source.rb +13 -0
  23. data/lib/request_log_analyzer/database/warning.rb +14 -0
  24. data/lib/request_log_analyzer/file_format.rb +160 -0
  25. data/lib/request_log_analyzer/file_format/amazon_s3.rb +71 -0
  26. data/lib/request_log_analyzer/file_format/apache.rb +141 -0
  27. data/lib/request_log_analyzer/file_format/merb.rb +67 -0
  28. data/lib/request_log_analyzer/file_format/rack.rb +11 -0
  29. data/lib/request_log_analyzer/file_format/rails.rb +176 -0
  30. data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
  31. data/lib/request_log_analyzer/filter.rb +30 -0
  32. data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
  33. data/lib/request_log_analyzer/filter/field.rb +42 -0
  34. data/lib/request_log_analyzer/filter/timespan.rb +45 -0
  35. data/lib/request_log_analyzer/line_definition.rb +111 -0
  36. data/lib/request_log_analyzer/log_processor.rb +99 -0
  37. data/lib/request_log_analyzer/mailer.rb +62 -0
  38. data/lib/request_log_analyzer/output.rb +113 -0
  39. data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
  40. data/lib/request_log_analyzer/output/html.rb +184 -0
  41. data/lib/request_log_analyzer/request.rb +175 -0
  42. data/lib/request_log_analyzer/source.rb +72 -0
  43. data/lib/request_log_analyzer/source/database_loader.rb +87 -0
  44. data/lib/request_log_analyzer/source/log_parser.rb +274 -0
  45. data/lib/request_log_analyzer/tracker.rb +206 -0
  46. data/lib/request_log_analyzer/tracker/duration.rb +104 -0
  47. data/lib/request_log_analyzer/tracker/frequency.rb +95 -0
  48. data/lib/request_log_analyzer/tracker/hourly_spread.rb +107 -0
  49. data/lib/request_log_analyzer/tracker/timespan.rb +81 -0
  50. data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
  51. data/request-log-analyzer.gemspec +40 -0
  52. data/spec/database.yml +23 -0
  53. data/spec/fixtures/apache_combined.log +5 -0
  54. data/spec/fixtures/apache_common.log +10 -0
  55. data/spec/fixtures/decompression.log +12 -0
  56. data/spec/fixtures/decompression.log.bz2 +0 -0
  57. data/spec/fixtures/decompression.log.gz +0 -0
  58. data/spec/fixtures/decompression.log.zip +0 -0
  59. data/spec/fixtures/decompression.tar.gz +0 -0
  60. data/spec/fixtures/decompression.tgz +0 -0
  61. data/spec/fixtures/header_and_footer.log +6 -0
  62. data/spec/fixtures/merb.log +84 -0
  63. data/spec/fixtures/merb_prefixed.log +9 -0
  64. data/spec/fixtures/multiple_files_1.log +5 -0
  65. data/spec/fixtures/multiple_files_2.log +2 -0
  66. data/spec/fixtures/rails.db +0 -0
  67. data/spec/fixtures/rails_1x.log +59 -0
  68. data/spec/fixtures/rails_22.log +12 -0
  69. data/spec/fixtures/rails_22_cached.log +10 -0
  70. data/spec/fixtures/rails_unordered.log +24 -0
  71. data/spec/fixtures/syslog_1x.log +5 -0
  72. data/spec/fixtures/test_file_format.log +13 -0
  73. data/spec/fixtures/test_language_combined.log +14 -0
  74. data/spec/fixtures/test_order.log +16 -0
  75. data/spec/integration/command_line_usage_spec.rb +84 -0
  76. data/spec/integration/munin_plugins_rails_spec.rb +58 -0
  77. data/spec/integration/scout_spec.rb +151 -0
  78. data/spec/lib/helpers.rb +52 -0
  79. data/spec/lib/macros.rb +18 -0
  80. data/spec/lib/matchers.rb +77 -0
  81. data/spec/lib/mocks.rb +76 -0
  82. data/spec/lib/testing_format.rb +46 -0
  83. data/spec/spec_helper.rb +24 -0
  84. data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
  85. data/spec/unit/aggregator/summarizer_spec.rb +26 -0
  86. data/spec/unit/controller/controller_spec.rb +41 -0
  87. data/spec/unit/controller/log_processor_spec.rb +18 -0
  88. data/spec/unit/database/base_class_spec.rb +183 -0
  89. data/spec/unit/database/connection_spec.rb +34 -0
  90. data/spec/unit/database/database_spec.rb +133 -0
  91. data/spec/unit/file_format/amazon_s3_format_spec.rb +49 -0
  92. data/spec/unit/file_format/apache_format_spec.rb +203 -0
  93. data/spec/unit/file_format/file_format_api_spec.rb +69 -0
  94. data/spec/unit/file_format/line_definition_spec.rb +75 -0
  95. data/spec/unit/file_format/merb_format_spec.rb +52 -0
  96. data/spec/unit/file_format/rails_format_spec.rb +164 -0
  97. data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
  98. data/spec/unit/filter/field_filter_spec.rb +66 -0
  99. data/spec/unit/filter/filter_spec.rb +17 -0
  100. data/spec/unit/filter/timespan_filter_spec.rb +58 -0
  101. data/spec/unit/mailer_spec.rb +30 -0
  102. data/spec/unit/request_spec.rb +111 -0
  103. data/spec/unit/source/log_parser_spec.rb +119 -0
  104. data/spec/unit/tracker/duration_tracker_spec.rb +130 -0
  105. data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
  106. data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
  107. data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
  108. data/spec/unit/tracker/tracker_api_spec.rb +124 -0
  109. data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
  110. data/tasks/github-gem.rake +323 -0
  111. data/tasks/request_log_analyzer.rake +26 -0
  112. metadata +220 -0
@@ -0,0 +1,102 @@
1
+ require 'rubygems'
2
+ require 'activerecord'
3
+
4
+ class RequestLogAnalyzer::Database
5
+
6
+ def self.const_missing(const) # :nodoc:
7
+ RequestLogAnalyzer::load_default_class_file(self, const)
8
+ end
9
+
10
+ include RequestLogAnalyzer::Database::Connection
11
+
12
+ attr_accessor :file_format
13
+ attr_reader :line_classes
14
+
15
+ def initialize(connection_identifier = nil)
16
+ @line_classes = []
17
+ RequestLogAnalyzer::Database::Base.database = self
18
+ connect(connection_identifier)
19
+ end
20
+
21
+ # Returns the ORM class for the provided line type
22
+ def get_class(line_type)
23
+ line_type = line_type.name if line_type.respond_to?(:name)
24
+ Object.const_get("#{line_type}_line".camelize)
25
+ end
26
+
27
+ def default_classes
28
+ [RequestLogAnalyzer::Database::Request, RequestLogAnalyzer::Database::Source, RequestLogAnalyzer::Database::Warning]
29
+ end
30
+
31
+ # Loads the ORM classes by inspecting the tables in the current database
32
+ def load_database_schema!
33
+ connection.tables.map do |table|
34
+ case table.to_sym
35
+ when :warnings then RequestLogAnalyzer::Database::Warning
36
+ when :sources then RequestLogAnalyzer::Database::Source
37
+ when :requests then RequestLogAnalyzer::Database::Request
38
+ else load_activerecord_class(table)
39
+ end
40
+ end
41
+ end
42
+
43
+ # Returns an array of all the ActiveRecord-bases ORM classes for this database
44
+ def orm_classes
45
+ default_classes + line_classes
46
+ end
47
+
48
+ # Loads an ActiveRecord-based class that correspond to the given parameter, which can either be
49
+ # a table name or a LineDefinition instance.
50
+ def load_activerecord_class(linedefinition_or_table)
51
+
52
+ case linedefinition_or_table
53
+ when String, Symbol
54
+ klass_name = linedefinition_or_table.to_s.singularize.camelize
55
+ klass = RequestLogAnalyzer::Database::Base.subclass_from_table(linedefinition_or_table)
56
+ when RequestLogAnalyzer::LineDefinition
57
+ klass_name = "#{linedefinition_or_table.name}_line".camelize
58
+ klass = RequestLogAnalyzer::Database::Base.subclass_from_line_definition(linedefinition_or_table)
59
+ end
60
+
61
+ Object.const_set(klass_name, klass)
62
+ klass = Object.const_get(klass_name)
63
+ @line_classes << klass
64
+ return klass
65
+ end
66
+
67
+ def fileformat_classes
68
+ raise "No file_format provided!" unless file_format
69
+ line_classes = file_format.line_definitions.map { |(name, definition)| load_activerecord_class(definition) }
70
+ return default_classes + line_classes
71
+ end
72
+
73
+ # Creates the database schema and related ActiveRecord::Base subclasses that correspond to the
74
+ # file format definition. These ORM classes will later be used to create records in the database.
75
+ def create_database_schema!
76
+ fileformat_classes.each { |klass| klass.create_table! }
77
+ end
78
+
79
+ # Drops the table of all the ORM classes, and unregisters the classes
80
+ def drop_database_schema!
81
+ file_format ? fileformat_classes.map(&:drop_table!) : orm_classes.map(&:drop_table!)
82
+ remove_orm_classes!
83
+ end
84
+
85
+ # Registers the default ORM classes in the default namespace
86
+ def register_default_orm_classes!
87
+ Object.const_set('Request', RequestLogAnalyzer::Database::Request)
88
+ Object.const_set('Source', RequestLogAnalyzer::Database::Source)
89
+ Object.const_set('Warning', RequestLogAnalyzer::Database::Warning)
90
+ end
91
+
92
+ # Unregisters every ORM class constant
93
+ def remove_orm_classes!
94
+ orm_classes.each do |klass|
95
+ if klass.respond_to?(:name) && !klass.name.blank?
96
+ klass_name = klass.name.split('::').last
97
+ Object.send(:remove_const, klass_name) if Object.const_defined?(klass_name)
98
+ end
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,115 @@
1
+ class RequestLogAnalyzer::Database::Base < ActiveRecord::Base
2
+
3
+ self.abstract_class = true
4
+
5
+ def <=>(other)
6
+ if (source_id.nil? && other.source_id.nil?) || (source_comparison = source_id <=> other.source_id) == 0
7
+ lineno <=> other.lineno
8
+ else
9
+ source_comparison
10
+ end
11
+ end
12
+
13
+ def line_type
14
+ self.class.name.underscore.gsub(/_line$/, '').to_sym
15
+ end
16
+
17
+ class_inheritable_accessor :line_definition
18
+ cattr_accessor :database
19
+
20
+ def self.subclass_from_line_definition(definition)
21
+ klass = Class.new(RequestLogAnalyzer::Database::Base)
22
+ klass.set_table_name("#{definition.name}_lines")
23
+
24
+ klass.line_definition = definition
25
+
26
+ # Set relations with requests and sources table
27
+ klass.belongs_to :request
28
+ klass.belongs_to :source
29
+
30
+ # Serialize complex fields into the database
31
+ definition.captures.select { |c| c.has_key?(:provides) }.each do |capture|
32
+ klass.send(:serialize, capture[:name], Hash)
33
+ end
34
+
35
+ RequestLogAnalyzer::Database::Request.has_many "#{definition.name}_lines".to_sym
36
+ RequestLogAnalyzer::Database::Source.has_many "#{definition.name}_lines".to_sym
37
+
38
+ return klass
39
+ end
40
+
41
+ def self.subclass_from_table(table)
42
+ raise "Table #{table} not found!" unless database.connection.table_exists?(table)
43
+
44
+ klass = Class.new(RequestLogAnalyzer::Database::Base)
45
+ klass.set_table_name(table)
46
+
47
+ if klass.column_names.include?('request_id')
48
+ klass.belongs_to :request
49
+ RequestLogAnalyzer::Database::Request.has_many table.to_sym
50
+ end
51
+
52
+ if klass.column_names.include?('source_id')
53
+ klass.belongs_to :source
54
+ RequestLogAnalyzer::Database::Source.has_many table.to_sym
55
+ end
56
+
57
+ return klass
58
+ end
59
+
60
+ def self.drop_table!
61
+ database.connection.remove_index(self.table_name, [:source_id]) rescue nil
62
+ database.connection.remove_index(self.table_name, [:request_id]) rescue nil
63
+ database.connection.drop_table(self.table_name) if database.connection.table_exists?(self.table_name)
64
+ end
65
+
66
+ def self.create_table!
67
+ raise "No line_definition available to base table schema on!" unless self.line_definition
68
+
69
+ unless table_exists?
70
+ database.connection.create_table(table_name.to_sym) do |t|
71
+
72
+ # Default fields
73
+ t.column :request_id, :integer
74
+ t.column :source_id, :integer
75
+ t.column :lineno, :integer
76
+
77
+ line_definition.captures.each do |capture|
78
+ # Add a field for every capture
79
+ t.column(capture[:name], column_type(capture[:type]))
80
+
81
+ # If the capture provides other field as well, create columns for them, too
82
+ capture[:provides].each { |field, field_type| t.column(field, column_type(field_type)) } if capture[:provides].kind_of?(Hash)
83
+ end
84
+ end
85
+ end
86
+
87
+ # Add indices to table for more speedy querying
88
+ database.connection.add_index(self.table_name.to_sym, [:request_id]) # rescue
89
+ database.connection.add_index(self.table_name.to_sym, [:source_id]) # rescue
90
+ end
91
+
92
+
93
+ # Function to determine the column type for a field
94
+ # TODO: make more robust / include in file-format definition
95
+ def self.column_type(type_indicator)
96
+ case type_indicator
97
+ when :eval; :text
98
+ when :hash; :text
99
+ when :text; :text
100
+ when :string; :string
101
+ when :sec; :double
102
+ when :msec; :double
103
+ when :duration; :double
104
+ when :float; :double
105
+ when :double; :double
106
+ when :integer; :integer
107
+ when :int; :int
108
+ when :timestamp; :datetime
109
+ when :datetime; :datetime
110
+ when :date; :date
111
+ else :string
112
+ end
113
+ end
114
+
115
+ end
@@ -0,0 +1,38 @@
1
+ module RequestLogAnalyzer::Database::Connection
2
+
3
+ def self.from_string(string)
4
+ hash = {}
5
+ if string =~ /^(?:\w+=(?:[^;])*;)*\w+=(?:[^;])*$/
6
+ string.scan(/(\w+)=([^;]*);?/) { |variable, value| hash[variable.to_sym] = value }
7
+ elsif string =~ /^(\w+)\:\/\/(?:(?:([^:]+)(?:\:([^:]+))?\@)?([\w\.-]+)\/)?([\w\:\-\.\/]+)$/
8
+ hash[:adapter], hash[:username], hash[:password], hash[:host], hash[:database] = $1, $2, $3, $4, $5
9
+ hash.delete_if { |k, v| v.nil? }
10
+ end
11
+ return hash.empty? ? nil : hash
12
+ end
13
+
14
+ def connect(connection_identifier)
15
+ if connection_identifier.kind_of?(Hash)
16
+ RequestLogAnalyzer::Database::Base.establish_connection(connection_identifier)
17
+ elsif connection_identifier == ':memory:'
18
+ RequestLogAnalyzer::Database::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
19
+ elsif connection_hash = RequestLogAnalyzer::Database::Connection.from_string(connection_identifier)
20
+ RequestLogAnalyzer::Database::Base.establish_connection(connection_hash)
21
+ elsif connection_identifier.kind_of?(String) # Normal SQLite 3 database file
22
+ RequestLogAnalyzer::Database::Base.establish_connection(:adapter => 'sqlite3', :database => connection_identifier)
23
+ elsif connection_identifier.nil?
24
+ nil
25
+ else
26
+ raise "Cannot connect with this connection_identifier: #{connection_identifier.inspect}"
27
+ end
28
+ end
29
+
30
+ def disconnect
31
+ RequestLogAnalyzer::Database::Base.remove_connection
32
+ end
33
+
34
+ def connection
35
+ RequestLogAnalyzer::Database::Base.connection
36
+ end
37
+
38
+ end
@@ -0,0 +1,22 @@
1
+ class RequestLogAnalyzer::Database::Request < RequestLogAnalyzer::Database::Base
2
+
3
+ # Returns an array of all the Line objects of this request in the correct order.
4
+ def lines
5
+ @lines ||= begin
6
+ lines = []
7
+ self.class.reflections.each { |r, d| lines += self.send(r).all }
8
+ lines.sort
9
+ end
10
+ end
11
+
12
+ # Creates the table to store requests in.
13
+ def self.create_table!
14
+ unless database.connection.table_exists?(:requests)
15
+ database.connection.create_table(:requests) do |t|
16
+ t.column :first_lineno, :integer
17
+ t.column :last_lineno, :integer
18
+ end
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,13 @@
1
+ class RequestLogAnalyzer::Database::Source < RequestLogAnalyzer::Database::Base
2
+
3
+ def self.create_table!
4
+ unless database.connection.table_exists?(:sources)
5
+ database.connection.create_table(:sources) do |t|
6
+ t.column :filename, :string
7
+ t.column :mtime, :datetime
8
+ t.column :filesize, :integer
9
+ end
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,14 @@
1
+ class RequestLogAnalyzer::Database::Warning < RequestLogAnalyzer::Database::Base
2
+
3
+ def self.create_table!
4
+ unless database.connection.table_exists?(:warnings)
5
+ database.connection.create_table(:warnings) do |t|
6
+ t.column :warning_type, :string, :limit => 30, :null => false
7
+ t.column :message, :string
8
+ t.column :source_id, :integer
9
+ t.column :lineno, :integer
10
+ end
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,160 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ def self.const_missing(const)
4
+ RequestLogAnalyzer::load_default_class_file(self, const)
5
+ end
6
+
7
+ # Loads a FileFormat::Base subclass instance.
8
+ # You can provide:
9
+ # * A FileFormat instance (which will return itself)
10
+ # * A FileFormat class (of which an imstance will be returned)
11
+ # * A filename (from which the FileFormat class is loaded)
12
+ # * A symbol of a built-in file format (e.g. :rails)
13
+ def self.load(file_format, *args)
14
+ klass = nil
15
+ if file_format.kind_of?(RequestLogAnalyzer::FileFormat::Base)
16
+ # this already is a file format! return itself
17
+ return @current_file_format = file_format
18
+
19
+ elsif file_format.kind_of?(Class) && file_format.ancestors.include?(RequestLogAnalyzer::FileFormat::Base)
20
+ # a usable class is provided. Use this format class.
21
+ klass = file_format
22
+
23
+ elsif file_format.kind_of?(String) && File.exist?(file_format)
24
+ # load a format from a ruby file
25
+ require file_format
26
+ const = RequestLogAnalyzer::to_camelcase(File.basename(file_format, '.rb'))
27
+ if RequestLogAnalyzer::FileFormat.const_defined?(const)
28
+ klass = RequestLogAnalyzer::FileFormat.const_get(const)
29
+ elsif Object.const_defined?(const)
30
+ klass = Object.const_get(const)
31
+ else
32
+ raise "Cannot load class #{const} from #{file_format}!"
33
+ end
34
+
35
+ else
36
+ # load a provided file format
37
+ klass = RequestLogAnalyzer::FileFormat.const_get(RequestLogAnalyzer::to_camelcase(file_format))
38
+ end
39
+
40
+ # check the returned klass to see if it can be used
41
+ raise "Could not load a file format from #{file_format.inspect}" if klass.nil?
42
+ raise "Invalid FileFormat class" unless klass.kind_of?(Class) && klass.ancestors.include?(RequestLogAnalyzer::FileFormat::Base)
43
+
44
+ @current_file_format = klass.create(*args) # return an instance of the class
45
+ end
46
+
47
+ # Base class for all log file format definitions. This class provides functions for subclasses to
48
+ # define their LineDefinitions and to define a summary report.
49
+ #
50
+ # A subclass of this class is instantiated when request-log-analyzer is started and this instance
51
+ # is shared with all components of the application so they can act on the specifics of the format
52
+ class Base
53
+
54
+ attr_reader :line_definitions, :report_trackers
55
+
56
+ ####################################################################################
57
+ # CLASS METHODS for format definition
58
+ ####################################################################################
59
+
60
+ # Registers the line definer instance for a subclass.
61
+ def self.inherited(subclass)
62
+ if subclass.superclass == RequestLogAnalyzer::FileFormat::Base
63
+
64
+ # Create aline and report definer for this class
65
+ subclass.class_eval do
66
+ instance_variable_set(:@line_definer, RequestLogAnalyzer::LineDefinition::Definer.new)
67
+ instance_variable_set(:@report_definer, RequestLogAnalyzer::Aggregator::Summarizer::Definer.new)
68
+ class << self; attr_accessor :line_definer, :report_definer; end
69
+ end
70
+
71
+ # Create a custom Request class for this file format
72
+ subclass.const_set('Request', Class.new(RequestLogAnalyzer::Request)) unless subclass.const_defined?('Request')
73
+ else
74
+
75
+ # Copy the line and report definer from the parent class.
76
+ subclass.class_eval do
77
+ instance_variable_set(:@line_definer, superclass.line_definer.clone)
78
+ instance_variable_set(:@report_definer, superclass.report_definer.clone)
79
+ class << self; attr_accessor :line_definer, :report_definer; end
80
+ end
81
+
82
+ # Create a custom Request class based on the superclass's Request class
83
+ subclass.const_set('Request', Class.new(subclass.superclass::Request)) unless subclass.const_defined?('Request')
84
+ end
85
+ end
86
+
87
+ # Specifies a single line defintions.
88
+ def self.line_definition(name, &block)
89
+ @line_definer.send(name, &block)
90
+ end
91
+
92
+ # Specifies multiple line definitions at once using a block
93
+ def self.format_definition(&block)
94
+ if block_given?
95
+ yield self.line_definer
96
+ else
97
+ return self.line_definer
98
+ end
99
+ end
100
+
101
+ # Specifies the summary report using a block.
102
+ def self.report(mode = :append, &block)
103
+ self.report_definer.reset! if mode == :overwrite
104
+ yield(self.report_definer)
105
+ end
106
+
107
+ ####################################################################################
108
+ # Instantiation
109
+ ####################################################################################
110
+
111
+ def self.create(*args)
112
+ # Ignore arguments
113
+ return self.new(line_definer.line_definitions, report_definer.trackers)
114
+ end
115
+
116
+ def initialize(line_definitions = {}, report_trackers = [])
117
+ @line_definitions, @report_trackers = line_definitions, report_trackers
118
+ end
119
+
120
+ ####################################################################################
121
+ # INSTANCE methods
122
+ ####################################################################################
123
+
124
+ # Returns the Request class of this file format
125
+ def request_class
126
+ self.class::Request
127
+ end
128
+
129
+ # Returns a Request instance with the given parsed lines that should be provided as hashes.
130
+ def request(*hashes)
131
+ request_class.create(self, *hashes)
132
+ end
133
+
134
+ # Checks whether the line definitions form a valid language.
135
+ # A file format should have at least a header and a footer line type
136
+ def valid?
137
+ line_definitions.any? { |(name, ld)| ld.header } && line_definitions.any? { |(name, ld)| ld.footer }
138
+ end
139
+
140
+ # Returns true if this language captures the given symbol in one of its line definitions
141
+ def captures?(name)
142
+ line_definitions.any? { |(name, ld)| ld.captures?(name) }
143
+ end
144
+
145
+ # Function that a file format con implement to monkey patch the environment.
146
+ # * <tt>controller</tt> The environment is provided as a controller instance
147
+ def setup_environment(controller)
148
+ end
149
+
150
+ # Parses a line by trying to parse it using every line definition in this file format
151
+ def parse_line(line, &warning_handler)
152
+ self.line_definitions.each do |lt, definition|
153
+ match = definition.matches(line, &warning_handler)
154
+ return match if match
155
+ end
156
+
157
+ return nil
158
+ end
159
+ end
160
+ end