fmpvc 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b3a390e3cfe02d80a7f15cf4751e8772f3da314d
4
+ data.tar.gz: a89b88d4365f3279ec46d66a93e1ef5440db736d
5
+ SHA512:
6
+ metadata.gz: cb2cabdb3e496445f15f4b9748284643e8f8a12e03eb3b455175af704e1b7562c4e1040d09934f13e983068dc49aa456704cb840cbe7c921f3b53be6bcacd421
7
+ data.tar.gz: d8c928f7b5e7f38ff02d79f4133008ff74a5a9ea19feda60d68bf859e1e47e94174ca914f59e837db66c97e65a19c0beca8171f83d47fa8c32ea321eb4bb6929
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ spec/data/test_1/fmp_text/
12
+ spec/data/test_2/fmp_text/
13
+ spec/data/test_3/fmp_text/
14
+ spec/data/test_4/fmp_text/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format progress
3
+
@@ -0,0 +1 @@
1
+ fmversioning
@@ -0,0 +1 @@
1
+ ruby-2.1.0
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'nokogiri', '~> 1.6.6'
4
+ gem 'activesupport', '~> 4.2'
5
+
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Martin S. Boswell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,88 @@
1
+ # FMPVC
2
+
3
+ `FMPVC` is a tool to help FileMaker developers by creating a set of text files which represent design objects in their databases (e.g. scripts, custom functions, layouts, etc.). `fmpvc` has no access to database content. The command, `fmpvc`, parses a Database Design Report (DDR) produced by FileMaker Pro Advanced and creates text files for each of the primary FileMaker objects described in the DDR. With those files the developer may:
4
+
5
+ 1. use a version control system to track changes to databases
6
+ 1. diff current objects with objects from previous versions (e.g. compare different versions of a script)
7
+ 2. perform full text searches of a set of FileMaker datases (e.g. find all uses of a custom function in a solution)
8
+ 3. obtain text representations of FileMaker objects (e.g. create a list of fields in a table)
9
+
10
+ DDR parsing is a one-way process, and there is currently no way to re-create a FileMaker file from DDR, Therefore, there is no direct way to restore, for instance, an old version of a FileMaker Script to your current working FileMaker file. The best we can do is retrieve the previous text version and examine it to recreate the FileMaker script manually. It is recommended that developers save clones of the FileMaker databases with each version control commit so that older versions of some of the items may be copied from the clone and pasted into latest versions (or, of course, entire databases may be restored).
11
+
12
+
13
+ ## Installation
14
+
15
+ `FMPVC` is a ruby gem and may be installed as follows:
16
+
17
+ $ gem install fmpvc
18
+
19
+ `FMPVC` requires both Nokogiri and ActiveSupport gems.
20
+
21
+ `FMPVC` requires ruby 2.0 or later. FMPVC has only been tested on Mac OS X, and in it's current state, it is unlikely to work properly in a Windows ruby environment (due to line endings, file paths, etc.). `fmpvc` should run fine on a Linux machine, but of course, the DDR generation requires FileMaker Pro Advanced, which is only available on Mac OS X and Windows.
22
+
23
+ ## Usage
24
+
25
+ By default the `fmpvc` command looks for a `Summary.xml` file in a directory called `fmp_ddr` in the current working directory. It reads the contents of that file and then processes each of the referenced report files (there is one for each FileMaker file included in the DDR). It produces a set of text files and directories representing each database inside of the directory, `fmp_text`. Example output looks like this:
26
+
27
+ ├── fmp_clone/
28
+ │ └── FMServer_Sample Clone.fmp12
29
+ ├── fmp_ddr/
30
+ │ ├── FMServer_Sample_fmp12.xml
31
+ │ └── Summary.xml
32
+ ├── fmp_text/
33
+ │ └── FMServer_Sample_fmp12.xml/
34
+ │ ├── Accounts.txt
35
+ │ ├── CustomFunctions/
36
+ │ │ └── GetWorkDays (id 1).txt
37
+ ...etc.
38
+
39
+ Usage in brief:
40
+
41
+ - change directory to the location where you'd like to save the DDR and clones and produce the text files
42
+ - create a directory, `fmp_ddr`, to hold DDR
43
+ - from FileMaker Pro Advanced, choose "Database Design Report..." from the Tools menu.
44
+ - choose project database files
45
+ - include all tables for each file
46
+ - include all DDR sections
47
+ - choose XML output
48
+ - save in the folder created above with the default name, `Summary`
49
+ - optionally, save clones of the same databases in a folder named, `fmp_clone` (this is not required)
50
+ - run the command `fmpvc`
51
+
52
+ Command-line options:
53
+
54
+ -h Show help message
55
+ -b, --base-dir <directory> Path to base directory (contains fmp_ddr/).
56
+ -d, --ddr-dir <directory> Look for DDR files in directory named <directory>.
57
+ -D, --text-dir <directory> Write text files in directory named <directory>.
58
+ -q, --quiet Suppress progress messages.
59
+ -s, --summary-file <filename> Look for Summary file named <filename>.
60
+ -t, --tree-file <filename> Set tree file name.
61
+ -T, --no-tree Don't create a tree file.
62
+ -Y, --no-yaml Suppress YAML in text files.
63
+
64
+ ### YAML
65
+
66
+ By default, `fmpvc` appends a [YAML](http://www.yaml.org/spec/1.2/spec.html) representation of the FileMaker element to each text file. `fmpvc` doesn't capture all of the details of every FileMaker object, and even when it does, there are cases where there isn't an easy way to represent the object that is more concise or clear than the YAML description. In cases where `fmpvc` doesn't describe an aspect of a FileMaker element, by including a full YAML representation, changes will not be missed in a diff. The YAML is typically more human-readable and easier to diff than the original XML from the DDR.
67
+
68
+ YAML generation can be suppressed with the `-Y` command-line option.
69
+
70
+ ### tree command
71
+
72
+ The tree command creates a textual representation of a directory and its filesystem objects. By default, `fmpvc` searches for `tree` in the shell's path and if it finds, one, uses it to create the file, `./fmp_text/tree.txt`.
73
+
74
+ `tree` is not available by default on Mac OS X, but can be easily installed with a package manager such as [homebrew](http://brew.sh/). Most Linux installations include the `tree` command.
75
+
76
+ ## Development
77
+
78
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
79
+
80
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
81
+
82
+ ## Contributing
83
+
84
+ 1. Fork it ( https://github.com/MartinBoswell/fmpvc/fork )
85
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
86
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
87
+ 4. Push to the branch (`git push origin my-new-feature`)
88
+ 5. Create a new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fmpvc"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
+ load File.expand_path("../../exe/fmpvc", __FILE__)
5
+
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ # Requires: Summary.xml, run from main directory (which includes fmp_{clone,ddr,text}/)
3
+
4
+ require 'fmpvc'
5
+ require 'optparse'
6
+ include FMPVC
7
+
8
+ FMPVC.configure {}
9
+
10
+ OptionParser.new do |opts|
11
+ # opts.banner = "Usage: #{$0} [options] [base_directory_path]"
12
+ opts.on('-h', 'Show help message') do
13
+ puts opts
14
+ exit
15
+ end
16
+ opts.on('-b', '--base-dir <directory>', 'Path to base directory (contains fmp_ddr/).') do |b|
17
+ FMPVC.configuration.ddr_basedir = b
18
+ end
19
+ opts.on('-d', '--ddr-dir <directory>', 'Look for DDR files in directory named <directory>.') do |d|
20
+ FMPVC.configuration.ddr_dirname = d
21
+ end
22
+ opts.on('-D', '--text-dir <directory>', 'Write text files in directory named <directory>.') do |d|
23
+ FMPVC.configuration.text_dirname = d
24
+ end
25
+ opts.on('-q', '--quiet', 'Suppress progress messages.') do
26
+ FMPVC.configuration.quiet = true
27
+ end
28
+ opts.on('-s', '--summary-file <filename>', 'Look for Summary file named <filename>.') do |s|
29
+ FMPVC.configuration.ddr_filename = s
30
+ end
31
+ opts.on('-t', '--tree-file <filename>', 'Set tree file name.') do |t|
32
+ FMPVC.configuration.tree_filename = t
33
+ end
34
+ opts.on('-T', '--no-tree', "Don't create a tree file.") do
35
+ FMPVC.configuration.tree_filename = nil
36
+ end
37
+ opts.on('-Y', '--no-yaml', 'Suppress YAML in text files.') do
38
+ FMPVC.configuration.yaml = false
39
+ end
40
+ end.parse!
41
+
42
+ time_start = Time.new
43
+
44
+ ddr_dir = [FMPVC.configuration.ddr_basedir, FMPVC.configuration.ddr_dirname].join('/')
45
+ ddr = DDR.new(ddr_dir)
46
+ ddr.process_reports
47
+ ddr.write_summary
48
+ ddr.write_reports
49
+
50
+ # create a tree file, if available
51
+ unless ( FMPVC.configuration.tree_filename.nil? || FMPVC.configuration.tree_filename == '' )
52
+ require 'mkmf'
53
+ module MakeMakefile::Logging
54
+ # don't let MakeMake leave extraneous logs
55
+ @logfile = File::NULL
56
+ end
57
+
58
+ tree_command = find_executable 'tree'
59
+ if tree_command
60
+ puts "Creating tree.txt." unless FMPVC.configuration.quiet
61
+ `#{tree_command} -F > #{FMPVC.configuration.tree_filename}`
62
+ end
63
+ end
64
+
65
+ time_end = Time.new
66
+ unless FMPVC.configuration.quiet
67
+ puts "Processing complete."
68
+ puts "Elapsed time: #{(time_end - time_start).round(0)} seconds."
69
+ puts
70
+ end
71
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'fmpvc/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "fmpvc"
9
+ spec.version = FMPVC::VERSION
10
+ spec.authors = ["Martin S. Boswell"]
11
+ spec.email = ["mboswell@me.com"]
12
+
13
+ spec.summary = %q{Create a text version of the design elements of a FileMaker database.}
14
+ spec.description = %q{Process FileMaker Pro Advanced's Database Design Report (DDR) to produce textual representations of the design objects for use with version control systems, text editors, etc.}
15
+ spec.homepage = "http://rubygems.org/gems/fmpvc"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.9'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+
26
+ spec.add_dependency 'nokogiri', '~> 1.6.6'
27
+ spec.add_dependency 'activesupport', '~> 4.2'
28
+
29
+ end
@@ -0,0 +1,16 @@
1
+ require "fmpvc/version"
2
+ require "fmpvc/ddr"
3
+ require "fmpvc/fmpreport"
4
+ require "fmpvc/configuration"
5
+
6
+ module FMPVC
7
+
8
+ class << self
9
+ attr_accessor :configuration
10
+ end
11
+ def self.configure
12
+ @configuration = Configuration.new
13
+ yield(configuration)
14
+ end
15
+
16
+ end
@@ -0,0 +1,107 @@
1
+ module FMPVC
2
+
3
+ require 'nokogiri'
4
+
5
+ # for xml2yaml
6
+ require 'active_support/core_ext/hash/conversions'
7
+ require 'yaml'
8
+
9
+ class DDR
10
+
11
+ attr_reader :content, :type, :fmp_files, :xml_files, :base_dir_ddr, :base_dir_text_path, :reports, :fmpa_version, :creation_time, :creation_date, :reports
12
+
13
+ def initialize(summary_directory, summary_filename = FMPVC.configuration.ddr_filename)
14
+
15
+ @summary_filename = summary_filename
16
+ @base_dir_ddr = File.expand_path(summary_directory) ; raise(RuntimeError, "Error: can't find the DDR directory, #{@base_dir_ddr}") unless File.readable?(@base_dir_ddr)
17
+ summary_file_path = "#{@base_dir_ddr}/#{summary_filename}" ; raise(RuntimeError, "Error: can't find the DDR Summary.xml file, #{summary_file_path}") unless File.readable?(summary_file_path)
18
+ @base_dir_text_path = @base_dir_ddr.gsub(%r{#{FMPVC.configuration.ddr_dirname}}, FMPVC.configuration.text_dirname)
19
+ @summary_text_path = "#{@base_dir_text_path}/#{summary_filename.gsub(%r{\.xml}, '.txt')}"
20
+
21
+ @content = IO.read(summary_file_path)
22
+ @reports = Array.new
23
+ self.parse
24
+
25
+ end
26
+
27
+ def parse
28
+ post_notification('Summary', 'Parsing')
29
+ summary = Nokogiri::XML(@content)
30
+ attrs = summary.xpath("//FMPReport").first # "there can be only one"
31
+ @type = attrs["type"] ; raise RuntimeError, "Incorrect file type: not a DDR Summary.xml file!" unless @type == "Summary"
32
+ @fmpa_version = attrs["version"]
33
+ @creation_time = attrs["creationTime"]
34
+ @creation_date = attrs["creationDate"]
35
+ @summary_yaml = element2yaml(summary)
36
+
37
+ fmp_reports = summary.xpath("//FMPReport/File")
38
+ @reports = fmp_reports.map do |a_report|
39
+ {
40
+ :name => a_report['name'],
41
+ :link => a_report['link'],
42
+ :path => a_report['path'],
43
+ :attrs => Hash[ a_report.xpath("./*").map {|v| [v.name, v['count']]} ]
44
+ }
45
+ end
46
+ @xml_files = fmp_reports.collect {|node| node['link']}
47
+ @fmp_files = fmp_reports.collect {|node| node['name']}
48
+ end
49
+
50
+ def process_reports
51
+ @reports.each do |r|
52
+ # $stdout.puts
53
+ post_notification(r[:link].gsub(%r{\./+},''), 'Processing')
54
+ r[:report] = FMPReport.new(r[:link], self)
55
+ end
56
+ end
57
+
58
+ def post_notification(object, verb = 'Updating')
59
+ $stdout.puts [verb, object].join(" ") unless FMPVC.configuration.quiet
60
+ end
61
+
62
+ def stringer(n, str = " ")
63
+ n.times.map {str}.join
64
+ end
65
+
66
+ def write_summary
67
+ post_notification('Summary file', 'Writing')
68
+ FileUtils.mkdir(@base_dir_text_path) unless File.directory?(@base_dir_text_path)
69
+ summary_format = "%25s %-512s\n"
70
+ # report_params = ["BaseTables", "Tables", "Relationships", "Privileges", "ExtendedPrivileges", "FileAccess", "Layouts", "Scripts", "ValueLists", "CustomFunctions", "FileReferences", "CustomMenuSets", "CustomMenus"]
71
+ report_params = @reports.first[:attrs].keys # better to get the keys dynamically than a fixed list
72
+ params_label = report_params.map {|p| "%-2s" + stringer(p.length) }.join()
73
+ report_format = "%25s " + params_label
74
+ header = stringer(25 - "Report".length) + "Report" + " " + report_params.join(' ')
75
+ separator = header.gsub(%r{\w}, '-')
76
+ File.open(@summary_text_path, 'w') do |f|
77
+ f.write format(summary_format, "Summary file:", @summary_filename)
78
+ f.write format(summary_format, "Summary path:", @base_dir_ddr)
79
+ f.write format(summary_format, "FileMaker Pro version:", @fmpa_version)
80
+ f.write format(summary_format, "Creation Date:", @creation_date)
81
+ f.write format(summary_format, "Creation Time:", @creation_time)
82
+ f.puts
83
+ f.puts header
84
+ f.puts separator
85
+ @reports.each do |r|
86
+ f.puts format(report_format, r[:name] + ' ', *report_params.map { |p| r[:attrs][p] })
87
+ end
88
+ f.puts
89
+ f.puts @summary_yaml
90
+ end
91
+ end
92
+
93
+ def write_reports
94
+ self.process_reports if @reports.first[:report].nil?
95
+ @reports.each { |r| r[:report].write_all_objects }
96
+ end
97
+
98
+ def element2yaml(xml_element)
99
+ return '' unless FMPVC.configuration.yaml
100
+ element_xml = xml_element.to_xml({:encoding => 'UTF-8'}) # REMEMBER: the encoding
101
+ element_hash = Hash.from_xml(element_xml)
102
+ element_yaml = element_hash.to_yaml
103
+ end
104
+
105
+ end
106
+
107
+ end
@@ -0,0 +1,538 @@
1
+ module FMPVC
2
+
3
+ require 'nokogiri'
4
+ require 'fileutils'
5
+
6
+ # for xml2yaml
7
+ require 'active_support/core_ext/hash/conversions'
8
+ require 'yaml'
9
+
10
+ NEWLINE = "\n"
11
+
12
+ class FMPReport
13
+
14
+ attr_reader :content, :type, :text_dir, :text_filename, :report_dirpath, :named_objects
15
+
16
+ def initialize(report_filename, ddr)
17
+ report_dirpath = "#{ddr.base_dir_ddr}/#{report_filename}" # location of the fmpfilename.xml file
18
+ raise(RuntimeError, "Error: can't find the report file, #{report_dirpath}") unless File.readable?(report_dirpath)
19
+
20
+ @content = IO.read(report_dirpath, mode: 'rb:UTF-16:UTF-8') # transcode is specifically for a spec content match
21
+ @text_dir = "#{ddr.base_dir_ddr}/../#{FMPVC.configuration.text_dirname}"
22
+ @text_filename = fs_sanitize(report_filename)
23
+ @report_dirpath = "#{@text_dir}/#{@text_filename}"
24
+
25
+ self.define_content_procs
26
+
27
+ self.parse
28
+ self.clean_dir
29
+ self.write_dir
30
+
31
+ ### hierarchical folder structure
32
+ @scripts = parse_fmp_obj( "/FMPReport/File/ScriptCatalog", "/*[name()='Group' or name()='Script']", @script_content )
33
+ @layouts = parse_fmp_obj( "/FMPReport/File/LayoutCatalog", "/*[name()='Group' or name()='Layout']", @layouts_content )
34
+ ### single folder with files
35
+ @value_lists = parse_fmp_obj( "/FMPReport/File/ValueListCatalog", "/*[name()='ValueList']", @value_list_content )
36
+ @tables = parse_fmp_obj( "/FMPReport/File/BaseTableCatalog", "/*[name()='BaseTable']", @table_content )
37
+ @custom_functions = parse_fmp_obj( "/FMPReport/File/CustomFunctionCatalog", "/*[name()='CustomFunction']", @custom_function_content )
38
+ @menu_sets = parse_fmp_obj( "/FMPReport/File/CustomMenuSetCatalog", "/*[name()='CustomMenuSet']", @menu_sets_content )
39
+ @custom_menus = parse_fmp_obj( "/FMPReport/File/CustomMenuCatalog", "/*[name()='CustomMenu']", @custom_menus_content )
40
+ ### single file output
41
+ @accounts = parse_fmp_obj( "/FMPReport/File/AccountCatalog", "/*[name()='Account']", @accounts_content, true )
42
+ @privileges = parse_fmp_obj( "/FMPReport/File/PrivilegesCatalog", "/*[name()='PrivilegeSet']", @privileges_content, true )
43
+ @extended_privileges = parse_fmp_obj( "/FMPReport/File/ExtendedPrivilegeCatalog", "/*[name()='ExtendedPrivilege']", @extended_priviledge_content, true )
44
+ @relationships = parse_fmp_obj( "/FMPReport/File/RelationshipGraph", "/RelationshipList/*[name()='Relationship']", @relationships_content, true )
45
+ @file_access = parse_fmp_obj( "/FMPReport/File/AuthFileCatalog", '', @file_access_content, true )
46
+ @external_sources = parse_fmp_obj( "/FMPReport/File/ExternalDataSourcesCatalog", '', @external_sources_content, true )
47
+ @file_options = parse_fmp_obj( "/FMPReport/File/Options", '', @file_options_content, true )
48
+ @themes = parse_fmp_obj( "/FMPReport/File/ThemeCatalog", "/*[name()='Theme']", @themes_content, true )
49
+
50
+ @named_objects = [
51
+ { :content => @scripts, :disk_path => "/Scripts" },
52
+ { :content => @layouts, :disk_path => "/Layouts" },
53
+ { :content => @value_lists, :disk_path => "/ValueLists" },
54
+ { :content => @tables, :disk_path => "/Tables" },
55
+ { :content => @custom_functions, :disk_path => "/CustomFunctions" },
56
+ { :content => @menu_sets, :disk_path => "/CustomMenuSets" },
57
+ { :content => @custom_menus, :disk_path => "/CustomMenus" },
58
+ { :content => @accounts, :disk_path => "/Accounts.txt" },
59
+ { :content => @privileges, :disk_path => "/PrivilegeSets.txt" },
60
+ { :content => @extended_privileges, :disk_path => "/ExtendedPrivileges.txt" },
61
+ { :content => @relationships, :disk_path => "/Relationships.txt" },
62
+ { :content => @file_access, :disk_path => "/FileAccess.txt" },
63
+ { :content => @external_sources, :disk_path => "/ExternalDataSources.txt" },
64
+ { :content => @file_options, :disk_path => "/Options.txt" },
65
+ { :content => @themes, :disk_path => "/Themes.txt" }
66
+ ]
67
+
68
+ end
69
+
70
+ def write_all_objects()
71
+ post_notification('report files', 'Writing')
72
+ @named_objects.each { |obj| write_obj_to_disk(obj[:content], @report_dirpath + obj[:disk_path])}
73
+ end
74
+
75
+ def parse
76
+ @report = Nokogiri::XML(@content)
77
+ @type = @report.xpath("//FMPReport").first["type"]
78
+ raise RuntimeError, "Incorrect file type: not an FMPReport Report file" unless @type == "Report"
79
+ end
80
+
81
+ def fs_sanitize(text_string)
82
+ safe_name = text_string.gsub(%r{\A [\/\.]+ }mx, '') # remove leading dir symbols: . /
83
+ safe_name.gsub(%r{[\/]}, '_') # just remove [ / ] for now.
84
+ end
85
+
86
+ def fs_id(fs_name, id)
87
+ fs_name + " (id #{id})"
88
+ end
89
+
90
+ # e.g. /FMPReport/File/ScriptCatalog , /FMPReport/File/ScriptCatalog/Group[1]/Group
91
+ # return: "/Actors/Actor Triggers"
92
+ def disk_path_from_base(object_base, object_xpath, path = '')
93
+ return "#{path}" if object_xpath == object_base
94
+ curent_node_filename = @report.xpath("#{object_xpath}").first['name']
95
+ current_node_id = @report.xpath("#{object_xpath}").first['id']
96
+ parent_node_xpath = @report.xpath("#{object_xpath}/..").first.path
97
+ disk_path_from_base(object_base, parent_node_xpath, "/#{fs_id(curent_node_filename, current_node_id)}" + "#{path}" )
98
+ end
99
+
100
+ def write_dir
101
+ FileUtils.mkdir_p(@report_dirpath)
102
+ end
103
+
104
+ def clean_dir
105
+ FileUtils.rm_rf(@report_dirpath)
106
+ end
107
+
108
+ def element2yaml(xml_element)
109
+ return '' unless FMPVC.configuration.yaml
110
+ element_xml = xml_element.to_xml({:encoding => 'UTF-8'}) # REMEMBER: the encoding
111
+ element_hash = Hash.from_xml(element_xml)
112
+ element_yaml = element_hash.to_yaml
113
+ end
114
+
115
+ def post_notification(object, verb = 'Updating')
116
+ $stdout.puts [verb, object].join(" ") unless FMPVC.configuration.quiet
117
+ end
118
+
119
+
120
+ def parse_fmp_obj(object_base, object_nodes, obj_content, one_file = false)
121
+ post_notification(object_base.gsub(%r{\/FMPReport\/File\/},''), ' Parsing')
122
+ objects_parsed = Array.new
123
+ objects = @report.xpath("#{object_base}#{object_nodes}")
124
+ objects.each do |an_obj|
125
+ obj_id = an_obj['id']
126
+ if one_file
127
+ sanitized_obj_name_id_ext = nil
128
+ else
129
+ obj_name = an_obj['name']
130
+ sanitized_obj_name = fs_sanitize(obj_name)
131
+ sanitized_obj_name_id = fs_id(sanitized_obj_name, obj_id)
132
+ sanitized_obj_name_id_ext = sanitized_obj_name_id + '.txt'
133
+ end
134
+
135
+ obj_parsed = {
136
+ :name => sanitized_obj_name_id_ext \
137
+ , :type => :file \
138
+ , :xpath => an_obj.path \
139
+ }
140
+
141
+ # if it's a Group, then make a directory for it, else make a file
142
+ if an_obj.name == 'Group'
143
+ obj_parsed[:type] = :dir
144
+ obj_parsed[:name] = sanitized_obj_name_id
145
+ obj_parsed[:children] = parse_fmp_obj(an_obj.path, object_nodes, obj_content)
146
+ else
147
+ obj_parsed[:content] = one_file ? obj_content.call(objects) : obj_content.call(an_obj)
148
+ obj_parsed[:yaml] = one_file ? element2yaml(@report.xpath(object_base)) : element2yaml(an_obj)
149
+ end
150
+
151
+ objects_parsed.push(obj_parsed)
152
+ break if one_file == true
153
+ end
154
+ objects_parsed
155
+ end
156
+
157
+ def write_obj_to_disk(objs, full_path)
158
+ post_notification(full_path.gsub(%r{.*#{FMPVC.configuration.text_dirname}/},''), ' Writing')
159
+ if full_path =~ %r{\.txt}
160
+ # single file objects
161
+ File.open(full_path, 'w') do |f|
162
+ unless objs.empty?
163
+ f.write(objs.first[:content] + NEWLINE) unless objs.first[:content] == ''
164
+ f.write(NEWLINE + objs.first[:yaml])
165
+ end
166
+ end
167
+ else
168
+ # multi-file objects in directory
169
+ FileUtils.mkdir_p(full_path) unless File.directory?(full_path)
170
+ objs.each do |obj|
171
+ if obj[:type] == :file
172
+ File.open("#{full_path}/#{obj[:name]}", 'w') do |f|
173
+ f.write(obj[:content] + NEWLINE) unless obj[:content] == ''
174
+ f.write(NEWLINE + obj[:yaml])
175
+ end
176
+ elsif obj[:type] == :dir
177
+ write_obj_to_disk(obj[:children], full_path + "/#{obj[:name]}")
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ def define_content_procs
184
+
185
+ @script_content = Proc.new do |a_script|
186
+ content = ''
187
+ a_script.xpath("./StepList/Step/StepText").each {|t| content += t.text.gsub(%r{\n},'') + "\n" } # remove \n from middle of steps
188
+ content
189
+ end
190
+
191
+ @layouts_content = Proc.new do |a_layout|
192
+ content = ''
193
+ layout_name = a_layout['name']
194
+ layout_id = a_layout['id']
195
+ layout_table = a_layout.xpath('./Table').first['name']
196
+ layout_theme = a_layout.xpath('./Theme').first['name']
197
+ layout_format = "%18s %-25s\n"
198
+ object_format = " %-16s %-35s\n"
199
+ content += format(layout_format, "Layout name: ", layout_name)
200
+ content += format(layout_format, "id: ", layout_id)
201
+ content += format(layout_format, "Table: ", layout_table)
202
+ content += format(layout_format, "Theme: ", layout_theme)
203
+ content += NEWLINE
204
+ content += format(layout_format, "Objects: ", '')
205
+ layout_objects = a_layout.xpath("./*[name()='Object']") # find all objects
206
+ layout_objects_types = layout_objects.map { |o| o['type']} # list of 'types'
207
+ if !layout_objects_types.empty? # [].uniq! => nil - don't do that
208
+ layout_objects_types.uniq!
209
+ content += format(object_format, "Type", "'Name'" )
210
+ content += format(object_format, "----", "------" )
211
+ end
212
+ layout_objects_types.each do |a_type|
213
+ selected_objects = layout_objects.select { |o| o['type'] == a_type } # get all the objects of a given type
214
+ selected_objects.each do |type_obj| # collect all objects of type
215
+ content += format(object_format, type_obj['type'], type_obj.xpath('./*/Name').text) unless type_obj['type'] == "Text"
216
+ end
217
+ end
218
+
219
+ content
220
+ end
221
+
222
+
223
+ @value_list_content = Proc.new do |a_value_list|
224
+ content = ''
225
+ source_type = a_value_list.xpath("./Source").first['value']
226
+ if source_type == "Custom"
227
+ a_value_list.xpath("./CustomValues/Text").each {|t| content += t.text}
228
+ end
229
+ content
230
+ end
231
+
232
+ @table_content = Proc.new do |a_table|
233
+ content = ''
234
+ table_format = "%6d %-25s %-15s %-15s %-50s\n"
235
+ table_header_format = table_format.gsub(%r{d}, 's')
236
+ content += format(table_header_format, "id", "Field Name", "Data Type", "Field Type", "Comment")
237
+ content += format(table_header_format, "--", "----------", "---------", "----------", "-------")
238
+ a_table.xpath("//BaseTable[@name='#{a_table['name']}']/FieldCatalog/*[name()='Field']").each do |t|
239
+ t_comment = t.xpath("./Comment").text
240
+ content += format(table_format, t['id'], t['name'], t['dataType'], t['fieldType'], t_comment)
241
+ end
242
+ content
243
+ end
244
+
245
+ @custom_function_content = Proc.new do |a_custom_function|
246
+ content = ''
247
+ content += a_custom_function.xpath("./Calculation").map {|t| t.text}.join(NEWLINE)
248
+ content
249
+ end
250
+
251
+ @menu_sets_content = Proc.new do |a_menu_set|
252
+ content = ''
253
+ menu_set_format = "%6d %-35s\n"
254
+ menu_set_header_format = menu_set_format.gsub(%r{d}, 's')
255
+ content += format(menu_set_header_format, "id", "Menu")
256
+ content += format(menu_set_header_format, "--", "----")
257
+ a_menu_set.xpath("./CustomMenuList/*[name()='CustomMenu']").each do |a_menu|
258
+ content += format(menu_set_format, a_menu['id'], a_menu['name'])
259
+ end
260
+
261
+ content
262
+ end
263
+
264
+ @custom_menus_content = Proc.new do |a_menu|
265
+ content = ''
266
+ menu_name = a_menu['name']
267
+ menu_id = a_menu['id']
268
+ menu_base = a_menu.xpath('./BaseMenu').first['name']
269
+ menu_comment = a_menu.xpath('./Comment').text
270
+ menu_format = "%17s %-35s\n"
271
+
272
+ content += format(menu_format, "Menu name:", menu_name)
273
+ content += format(menu_format, "id:", menu_id)
274
+ content += format(menu_format, "Base menu:", menu_base)
275
+ content += format(menu_format, "Comment:", menu_comment)
276
+ content += NEWLINE
277
+ menu_items = a_menu.xpath("./MenuItemList/*[name()='MenuItem']")
278
+ menu_items.each do |an_item|
279
+ an_item.xpath('./Command').each { |c| content += " #{c['name']}\n"}
280
+ end
281
+
282
+ content
283
+ end
284
+
285
+
286
+ @accounts_content = Proc.new do |accounts|
287
+ content = ''
288
+ accounts_format = "%6d %-25s %-10s %-12s %-20s %-12s %-12s %-50s"
289
+ accounts_header_format = accounts_format.gsub(%r{d}, 's')
290
+ content += format(accounts_header_format, "id", "Name", "Status", "Management", "Privilege Set", "Empty Pass?", "Change Pass?", "Description") + NEWLINE
291
+ content += format(accounts_header_format, "--", "----", "------", "----------", "-------------", "-----------", "------------", "-----------") + NEWLINE
292
+ content += accounts.map do |an_account|
293
+ account_name = an_account['name']
294
+ account_id = an_account['id']
295
+ account_privilegeSet = an_account['privilegeSet']
296
+ account_emptyPassword = an_account['emptyPassword']
297
+ account_changePasswordOnNextLogin = an_account['changePasswordOnNextLogin']
298
+ account_managedBy = an_account['managedBy']
299
+ account_status = an_account['status']
300
+ account_Description = an_account.xpath('./Description').text
301
+ format(
302
+ accounts_format \
303
+ , account_id \
304
+ , account_name \
305
+ , account_status \
306
+ , account_managedBy \
307
+ , account_privilegeSet \
308
+ , account_emptyPassword \
309
+ , account_changePasswordOnNextLogin \
310
+ , account_Description
311
+ )
312
+ end.join(NEWLINE)
313
+
314
+ content
315
+ end
316
+
317
+ @privileges_content = Proc.new do |privileges|
318
+ content = ''
319
+ privileges_format = "%6d %-25s %-8s %-10s %-15s %-12s %-12s %-12s %-8s %-18s %-11s %-10s %-12s %-10s %-16s %-10s %-70s"
320
+ privileges_header_format = privileges_format.gsub(%r{d}, 's')
321
+ content += format(privileges_header_format, "id", "Name", "Print?", "Export?", "Manage Ext'd?", "Override?", "Disconnect?", "Password?", "Menus", "Records", "Layouts", "(Creation)", "ValueLists", "(Creation)", "Scripts", "(Creation)", "Description") + NEWLINE
322
+ content += format(privileges_header_format, "--", "----", "------", "-------", "-------------", "---------", "-----------", "---------", "-----", "-------", "-------", "----------", "----------", "----------", "-------", "----------", "-----------") + NEWLINE
323
+ privileges.each do |a_privilege_set|
324
+ privilege_set_id = a_privilege_set['id']
325
+ privilege_set_name = a_privilege_set['name']
326
+ privilege_set_comment = a_privilege_set['comment']
327
+ privilege_set_printing = a_privilege_set['printing']
328
+ privilege_set_exporting = a_privilege_set['exporting']
329
+ privilege_set_managedExtended = a_privilege_set['managedExtended']
330
+ privilege_set_overrideValidationWarning = a_privilege_set['overrideValidationWarning']
331
+ privilege_set_idleDisconnect = a_privilege_set['idleDisconnect']
332
+ privilege_set_allowModifyPassword = a_privilege_set['allowModifyPassword']
333
+ privilege_set_menu = a_privilege_set['menu']
334
+
335
+ privilege_set_records_value = a_privilege_set.xpath('./Records').first['value']
336
+ privilege_set_layouts_value = a_privilege_set.xpath('./Layouts').first['value']
337
+ privilege_set_layouts_creation = a_privilege_set.xpath('./Layouts').first['allowCreation']
338
+ privilege_set_valuelists_value = a_privilege_set.xpath('./ValueLists').first['value']
339
+ privilege_set_valuelists_creation = a_privilege_set.xpath('./ValueLists').first['allowCreation']
340
+ privilege_set_scripts_value = a_privilege_set.xpath('./Scripts').first['value']
341
+ privilege_set_scripts_creation = a_privilege_set.xpath('./Scripts').first['allowCreation']
342
+
343
+ content += format(
344
+ privileges_format \
345
+ , privilege_set_id \
346
+ , privilege_set_name \
347
+ , privilege_set_printing \
348
+ , privilege_set_exporting \
349
+ , privilege_set_managedExtended \
350
+ , privilege_set_overrideValidationWarning \
351
+ , privilege_set_idleDisconnect \
352
+ , privilege_set_allowModifyPassword \
353
+ , privilege_set_menu \
354
+ , privilege_set_records_value \
355
+ , privilege_set_layouts_value \
356
+ , privilege_set_layouts_creation \
357
+ , privilege_set_valuelists_value \
358
+ , privilege_set_valuelists_creation \
359
+ , privilege_set_scripts_value \
360
+ , privilege_set_scripts_creation \
361
+ , privilege_set_comment \
362
+ ) + NEWLINE
363
+ end
364
+ content
365
+ end
366
+
367
+ @extended_priviledge_content = Proc.new do |ext_privileges|
368
+ content = ''
369
+ ext_privilege_format = "%6d %-20s %-85s %-150s\n"
370
+ ext_privilege_header_format = ext_privilege_format.gsub(%r{d}, 's')
371
+ content += format(ext_privilege_header_format, "id", "Name", "Description", "Privilege Sets")
372
+ content += format(ext_privilege_header_format, "--", "----", "-----------", "--------------")
373
+ ext_privileges.each do |an_ext_privilege|
374
+ ext_privilege_id = an_ext_privilege['id']
375
+ ext_privilege_name = an_ext_privilege['name']
376
+ ext_privilege_comment = an_ext_privilege['comment']
377
+ ext_privilege_sets = an_ext_privilege.xpath('./PrivilegeSetList/*[name()="PrivilegeSet"]').map {|s| s['name']}.join(", ")
378
+
379
+ content += format(
380
+ ext_privilege_format \
381
+ , ext_privilege_id \
382
+ , ext_privilege_name \
383
+ , ext_privilege_comment \
384
+ , ext_privilege_sets \
385
+ )
386
+ end
387
+ content
388
+ end
389
+
390
+ @relationships_content = Proc.new do |relationships|
391
+ content = ''
392
+
393
+ tables = @report.xpath("/FMPReport/File/RelationshipGraph/TableList/*[name()='Table']")
394
+ table_format = " %-25s %-25s"
395
+ content +="Tables\n"
396
+ content += NEWLINE
397
+ content +=format(table_format, "Base Table (id)", "Table occurrence (id)") + NEWLINE
398
+ content +=format(table_format, "---------------", "---------------------") + NEWLINE
399
+ content += NEWLINE
400
+ tables.each do |a_table|
401
+ table_id = a_table['id']
402
+ table_name = a_table['name']
403
+ basetable_id = a_table['baseTableId']
404
+ basetable_name = a_table['baseTable']
405
+ content +=format(table_format, "#{basetable_name} (#{basetable_id})", "#{table_name} (#{table_id})") + NEWLINE
406
+ end
407
+ content += NEWLINE
408
+
409
+ relationship_format = " %-35s %-15s %-35s"
410
+ content +="Relationships" + NEWLINE
411
+ relationships.each do |a_relationship|
412
+ content += NEWLINE
413
+ content += format(" Relationship: %-4d", a_relationship['id']) + NEWLINE
414
+ predicates = a_relationship.xpath('./JoinPredicateList/*[name()="JoinPredicate"]')
415
+ predicates.each do |a_predicate|
416
+ predicate_type = a_predicate['type']
417
+
418
+ left_field = a_predicate.xpath('./LeftField/*[name()="Field"]').first
419
+ left_table = left_field['table']
420
+ left_field_name = left_field['name']
421
+
422
+ right_field = a_predicate.xpath('./RightField/*[name()="Field"]').first
423
+ right_table = right_field['table']
424
+ right_field_name = right_field['name']
425
+ content += format(relationship_format, "#{left_table}::#{left_field_name}", "#{predicate_type}", "#{right_table}::#{right_field_name}") + NEWLINE
426
+ end
427
+ end
428
+ content
429
+ end
430
+
431
+ @file_access_content = Proc.new do |file_access|
432
+ content = ''
433
+ inbound_access = file_access.xpath("./Inbound/*[name()='InboundAuthorization']")
434
+ outbound_access = file_access.xpath("./Outbound/*[name()='OutboundAuthorization']")
435
+ access_format = " %6d %-25s %-25s %-25s"
436
+ access_format_header = access_format.gsub(%r{d}, 's')
437
+
438
+ auth_requirement = file_access.first['requireAuthorization']
439
+ content += "Authorization required: #{auth_requirement}" + NEWLINE
440
+ if auth_requirement == "True"
441
+ content += NEWLINE
442
+ content += format(access_format_header, "id", "Timestamp", "Account", "Filenames") + NEWLINE
443
+ content += format(access_format_header, "--", "---------", "-------", "---------") + NEWLINE
444
+ content += format("%12s", "Inbound:") + NEWLINE
445
+ inbound_access.each do |i|
446
+ content += format(access_format, i['id'], i['date'], i['user'], i['filenames']) + NEWLINE
447
+ end
448
+ content += format("%12s", "Outbound:") + NEWLINE
449
+ outbound_access.each do |o|
450
+ content += format(access_format, o['id'], o['date'], o['user'], o['filenames']) + NEWLINE
451
+ end
452
+ end
453
+ content += NEWLINE
454
+
455
+ content
456
+ end
457
+
458
+ @external_sources_content = Proc.new do |data_sources|
459
+ content = ''
460
+ file_references = data_sources.xpath("./*[name()='FileReference']")
461
+ odbc_sources = data_sources.xpath("./*[name()='OdbcDataSource']")
462
+ file_references_format = " %6d %-25s %-25s\n"
463
+ file_references_header_format = file_references_format.gsub(%r{d},'s')
464
+ odbc_source_format = " %6d %-25s %-25s %-25s\n"
465
+ odbc_source_header_format = odbc_source_format.gsub(%r{d},'s')
466
+
467
+ content += format(file_references_header_format, "id", "File Reference", "Path List")
468
+ content += format(file_references_header_format, "--", "--------------", "---------")
469
+ file_references.each do |r|
470
+ content += format(file_references_format, r['id'], r['name'], r['pathList'])
471
+ end
472
+ content += NEWLINE
473
+ content += format(odbc_source_header_format, "id", "ODBC Source", "DSN", "Link")
474
+ content += format(odbc_source_header_format, "--", "-----------", "---", "----")
475
+ odbc_sources.each do |s|
476
+ content += format(odbc_source_format, s['id'], s['name'], s['DSN'], s['link'])
477
+ end
478
+
479
+ content
480
+ end
481
+
482
+ @file_options_content = Proc.new do |file_options|
483
+ content = ''
484
+ file_options_format = " %-27s %-30s\n"
485
+ trigger_format = " %-23s %-30s\n"
486
+
487
+ # optional <FMPReport><File><Options>, see DDR_grammar doc, p. 5
488
+ open_account_search = file_options.xpath('./OnOpen/Account')
489
+ open_account = (open_account_search.size > 0 ? open_account_search.first['name']: "")
490
+ open_layout_search = file_options.xpath('./OnOpen/Layout')
491
+ open_layout = ( open_layout_search.size > 0 ? open_layout_search.first['name'] : "" )
492
+
493
+ encryption_type = file_options.xpath('./Encryption').first['type']
494
+ encryption_note = case encryption_type
495
+ when "0"
496
+ "no encryption"
497
+ when "1"
498
+ "AES256 encrypted"
499
+ end
500
+
501
+ content += "File Options\n"
502
+ content += "------------\n"
503
+ content += NEWLINE
504
+ content += format(file_options_format, "Encryption:", "#{encryption_type} (#{encryption_note})")
505
+ content += NEWLINE
506
+ content += format(file_options_format, "Minimum Allowed Version:", file_options.xpath('./OnOpen/MinimumAllowedVersion').first['name'])
507
+ content += format(file_options_format, "Account:", open_account)
508
+ content += format(file_options_format, "Layout:", open_layout)
509
+ content += NEWLINE
510
+ content += format(file_options_format, "Default Custom Menu Set:", file_options.xpath('./DefaultCustomMenuSet/CustomMenuSet').first['name'])
511
+ content += NEWLINE
512
+ content += " Triggers\n"
513
+ file_options.xpath('./WindowTriggers/*').each do |t|
514
+ content += format(trigger_format, t.name, t.xpath('./Script').first['name'])
515
+ end
516
+
517
+ content
518
+ end
519
+
520
+ @themes_content = Proc.new do |themes|
521
+ content = ''
522
+ theme_format = " %6s %-20s %-20s %-10s %-10s %-20s\n"
523
+ content += format(theme_format, "id", "Name", "Group", "Version", "Locale", "Internal Name")
524
+ content += format(theme_format, "--", "----", "-----", "-------", "------", "-------------")
525
+ themes.each do |a_theme|
526
+ content += format(theme_format, a_theme['id'], a_theme['name'], a_theme['group'], a_theme['version'], a_theme['locale'], a_theme['internalName'])
527
+ end
528
+
529
+ content
530
+ end
531
+
532
+ end
533
+
534
+
535
+ end
536
+
537
+ end
538
+
@@ -0,0 +1,18 @@
1
+ module FMPVC
2
+ class Configuration
3
+
4
+ attr_accessor :quiet, :yaml, :ddr_dirname, :ddr_filename, :ddr_basedir, :text_dirname, :tree_filename
5
+
6
+ def initialize
7
+ # set default config settings
8
+ @quiet = false # don't print progress to stdout
9
+ @yaml = true # append full YAML to text files
10
+ @ddr_filename = 'Summary.xml' # name of primary DDR file to open
11
+ @ddr_dirname = 'fmp_ddr' # directory containing DDR
12
+ @ddr_basedir = './' # base directory (containing fmp_ddr, fmp_text)
13
+ @text_dirname = 'fmp_text' # text file base directory
14
+ @tree_filename = 'tree.txt' # set to nil to disable tree file generation
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module FMPVC
2
+ VERSION = "0.3.2"
3
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fmpvc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.2
5
+ platform: ruby
6
+ authors:
7
+ - Martin S. Boswell
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-06-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.6.6
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.6.6
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.2'
69
+ description: Process FileMaker Pro Advanced's Database Design Report (DDR) to produce
70
+ textual representations of the design objects for use with version control systems,
71
+ text editors, etc.
72
+ email:
73
+ - mboswell@me.com
74
+ executables:
75
+ - fmpvc
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - ".gitignore"
80
+ - ".rspec"
81
+ - ".ruby-gemset"
82
+ - ".ruby-version"
83
+ - ".travis.yml"
84
+ - CODE_OF_CONDUCT.md
85
+ - Gemfile
86
+ - LICENSE.txt
87
+ - README.md
88
+ - Rakefile
89
+ - bin/console
90
+ - bin/run_current
91
+ - bin/setup
92
+ - exe/fmpvc
93
+ - fmpvc.gemspec
94
+ - lib/fmpvc.rb
95
+ - lib/fmpvc/DDR.rb
96
+ - lib/fmpvc/FMPReport.rb
97
+ - lib/fmpvc/configuration.rb
98
+ - lib/fmpvc/version.rb
99
+ homepage: http://rubygems.org/gems/fmpvc
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.2.1
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Create a text version of the design elements of a FileMaker database.
123
+ test_files: []