-
Notifications
You must be signed in to change notification settings - Fork 454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adjust Xcodeproj::Workspace to keep the underlying REXML document and add basic <Group> support. #322
Adjust Xcodeproj::Workspace to keep the underlying REXML document and add basic <Group> support. #322
Changes from 3 commits
e6ab7cf
75de773
c07a2b1
9c6d59e
20e2f66
e751cc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,48 @@ | ||
require 'fileutils' | ||
require 'rexml/document' | ||
require 'xcodeproj/workspace/file_reference' | ||
require 'xcodeproj/workspace/group_reference' | ||
|
||
module Xcodeproj | ||
# Provides support for generating, reading and serializing Xcode Workspace | ||
# documents. | ||
# | ||
class Workspace | ||
# @return [Array<String>] the paths of the projects contained in the | ||
# @return [REXML::Document] the parsed XML model for the workspace contents | ||
attr_reader :document | ||
attr_reader :schemes | ||
|
||
# @return [Array<FileReference>] the paths of the projects contained in the | ||
# workspace. | ||
# | ||
attr_reader :file_references | ||
attr_reader :schemes | ||
def file_references | ||
return [] unless @document | ||
@document.get_elements('/Workspace//FileRef').map do |node| | ||
FileReference.from_node(node) | ||
end | ||
end | ||
|
||
# @param [Array] file_references @see file_references | ||
# @return [Array<GroupReference>] the groups contained in the workspace | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should still be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you saying the |
||
# | ||
def initialize(*file_references) | ||
@file_references = file_references.flatten | ||
def group_references | ||
return [] unless @document | ||
@document.get_elements('/Workspace//Group').map do |node| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should cache that ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea of this change is to work against the underlying xml to allow the document structure to remain intact when working with workspaces. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah gotcha, makes sense to ensure it's always in sync with the XML too. |
||
GroupReference.from_node(node) | ||
end | ||
end | ||
|
||
# @param [REXML::Document] document @see document | ||
# @param [Array<FileReference>] file_references additional projects to add | ||
# | ||
def initialize(document, *file_references) | ||
@schemes = {} | ||
if !document || document.is_a?(REXML::Document) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefer |
||
@document = document | ||
else | ||
@document = REXML::Document.new(root_xml('')) | ||
self << document | ||
end | ||
file_references.each { |ref| self << ref } | ||
end | ||
|
||
#-------------------------------------------------------------------------# | ||
|
@@ -31,9 +55,10 @@ def initialize(*file_references) | |
# @return [Workspace] the generated workspace. | ||
# | ||
def self.new_from_xcworkspace(path) | ||
from_s(File.read(File.join(path, 'contents.xcworkspacedata')), File.expand_path(File.dirname(path))) | ||
from_s(File.read(File.join(path, 'contents.xcworkspacedata')), | ||
File.expand_path(File.dirname(path))) | ||
rescue Errno::ENOENT | ||
new | ||
new(nil) | ||
end | ||
|
||
#-------------------------------------------------------------------------# | ||
|
@@ -48,10 +73,7 @@ def self.new_from_xcworkspace(path) | |
# | ||
def self.from_s(xml, workspace_path = '') | ||
document = REXML::Document.new(xml) | ||
file_references = document.get_elements('/Workspace/FileRef').map do |node| | ||
FileReference.from_node(node) | ||
end | ||
instance = new(file_references) | ||
instance = new(document) | ||
instance.load_schemes(workspace_path) | ||
instance | ||
end | ||
|
@@ -67,9 +89,36 @@ def self.from_s(xml, workspace_path = '') | |
# @return [void] | ||
# | ||
def <<(projpath) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that the parameter to this method can be either a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apologies for my ignorance -- is this proper syntax for documenting a parameter with two allowed types? The yard @overload documentation seems cumbersome for something like this...
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, YARD uses commas to separate the different possible types for a parameter. For a quick cheat sheet, see http://yardoc.org/types.html Since you're already in |
||
project_file_reference = Xcodeproj::Workspace::FileReference.new(projpath) | ||
@file_references << project_file_reference | ||
load_schemes_from_project File.expand_path(projpath) | ||
return unless @document && @document.respond_to?(:root) | ||
case | ||
when projpath.is_a?(String) | ||
project_file_reference = Xcodeproj::Workspace::FileReference.new(projpath) | ||
when projpath.is_a?(Xcodeproj::Workspace::FileReference) | ||
project_file_reference = projpath | ||
projpath = nil | ||
else | ||
raise ArgumentError, 'Input to the << operator must be a file path or FileReference' | ||
end | ||
@document.root.add_element(project_file_reference.to_node) | ||
load_schemes_from_project File.expand_path(projpath || project_file_reference.path) | ||
end | ||
|
||
#-------------------------------------------------------------------------# | ||
|
||
# Adds a new group container to the workspace | ||
# workspace. | ||
# | ||
# @param [String] name The name of the group | ||
# | ||
# @return [Xcodeproj::Workspace::GroupReference] | ||
# The added group reference | ||
# | ||
def add_group(name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing documentation about the optional block used by |
||
return nil unless @document | ||
group = Xcodeproj::Workspace::GroupReference.new(name) | ||
elem = @document.root.add_element(group.to_node) | ||
yield group, elem if block_given? | ||
elem | ||
end | ||
|
||
# Checks if the workspace contains the project with the given file | ||
|
@@ -81,14 +130,36 @@ def <<(projpath) | |
# @return [Boolean] whether the project is contained in the workspace. | ||
# | ||
def include?(file_reference) | ||
@file_references.include?(file_reference) | ||
file_references.include?(file_reference) | ||
end | ||
|
||
# @return [String] the XML representation of the workspace. | ||
# | ||
def to_s | ||
contents = file_references.map { |reference| file_reference_xml(reference) } | ||
root_xml(contents.join('')) | ||
contents = '' | ||
stack = [] | ||
@document.root.each_recursive do |elem| | ||
until stack.empty? | ||
last = stack.last | ||
if last == elem.parent | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer using |
||
break | ||
else | ||
contents += xcworkspace_element_end_xml(stack.length, stack.last) | ||
stack.pop | ||
end | ||
end | ||
|
||
stack << elem | ||
contents += xcworkspace_element_start_xml(stack.length, elem) | ||
end | ||
|
||
until stack.empty? | ||
contents += xcworkspace_element_end_xml(stack.length, stack.last) | ||
stack.pop | ||
end | ||
|
||
# contents = file_references.map { |reference| file_reference_xml(reference) } | ||
root_xml(contents) | ||
end | ||
|
||
# Saves the workspace at the given `xcworkspace` path. | ||
|
@@ -115,7 +186,7 @@ def save_as(path) | |
# @return [void] | ||
# | ||
def load_schemes(workspace_dir_path) | ||
@file_references.each do |file_reference| | ||
file_references.each do |file_reference| | ||
project_full_path = file_reference.absolute_path(workspace_dir_path) | ||
load_schemes_from_project(project_full_path) | ||
end | ||
|
@@ -146,21 +217,36 @@ def root_xml(contents) | |
<?xml version="1.0" encoding="UTF-8"?> | ||
<Workspace | ||
version = "1.0"> | ||
#{contents.strip} | ||
#{contents.rstrip} | ||
</Workspace> | ||
DOC | ||
end | ||
|
||
# @return [String] The XML representation of a file reference. | ||
# @return [String] The Xcode-specific XML formatting of an element start | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convention is to put |
||
# | ||
# @param [Integer] depth The depth of the element in the tree | ||
# @param [REXML::Document::Element] elem The XML element to format. | ||
# | ||
def xcworkspace_element_start_xml(depth, elem) | ||
attributes = case elem.name | ||
when 'Group' | ||
%w(location name) | ||
when 'FileRef' | ||
%w(location) | ||
end | ||
contents = "<#{elem.name}" | ||
indent = ' ' * depth | ||
attributes.each { | name| contents += "\n #{name} = \"#{elem.attribute(name).value}\"" } | ||
contents.split("\n").map { |line| "#{indent}#{line}" }.join("\n") + ">\n" | ||
end | ||
|
||
# @return [String] The Xcode-specific XML formatting of an element end | ||
# | ||
# @param [FileReference] file_reference The file reference. | ||
# @param [Integer] depth The depth of the element in the tree | ||
# @param [REXML::Document::Element] elem The XML element to format. | ||
# | ||
def file_reference_xml(file_reference) | ||
<<-DOC | ||
<FileRef | ||
location = "#{file_reference.type}:#{file_reference.path}"> | ||
</FileRef> | ||
DOC | ||
def xcworkspace_element_end_xml(depth, elem) | ||
"#{' ' * depth}</#{elem.name}>\n" | ||
end | ||
|
||
#-------------------------------------------------------------------------# | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
module Xcodeproj | ||
class Workspace | ||
# Describes a group reference of a Workspace. | ||
# | ||
class GroupReference | ||
# @return [String] the name of the group | ||
# | ||
attr_reader :name | ||
|
||
# @return [String] the type of reference to the project | ||
# | ||
# This can be of the following values: | ||
# - absolute | ||
# - group | ||
# - container (only supported value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we throw in case we encounter a non-supported value, or just silently fail? (\cc @segiddins) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably throw? But it's not super important |
||
# - developer | ||
# | ||
attr_reader :type | ||
|
||
# @param [#to_s] name @see name | ||
# @param [#to_s] type @see type | ||
# | ||
def initialize(name, type = 'container') | ||
@name = name.to_s | ||
@type = type.to_s | ||
end | ||
|
||
# @return [Bool] Whether a group reference is equal to another. | ||
# | ||
def ==(other) | ||
name == other.name && type == other.type | ||
end | ||
alias_method :eql?, :== | ||
|
||
# @return [Fixnum] A hash identical for equals objects. | ||
# | ||
def hash | ||
[name, type].hash | ||
end | ||
|
||
# Returns a group reference given XML representation. | ||
# | ||
# @param [REXML::Element] xml_node | ||
# the XML representation. | ||
# | ||
# @return [GroupReference] The new group reference instance. | ||
# | ||
def self.from_node(xml_node) | ||
type, _ = xml_node.attribute('location').value.split(':', 2) | ||
name = xml_node.attribute('name').value | ||
new(name, type) | ||
end | ||
|
||
# @return [REXML::Element] the XML representation of the group reference. | ||
# | ||
def to_node | ||
REXML::Element.new('Group').tap do |element| | ||
element.add_attribute('location', "#{type}:") | ||
element.add_attribute('name', "#{name}") | ||
end | ||
end | ||
end | ||
end | ||
end |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
require File.expand_path('../../spec_helper', __FILE__) | ||
|
||
module Xcodeproj | ||
describe Workspace do | ||
before do | ||
@group = Workspace::GroupReference.new('Group Name', 'container') | ||
end | ||
|
||
it 'properly implements equality comparison' do | ||
@group.should == @group.dup | ||
@group.should.eql @group.dup | ||
@group.hash.should == @group.dup.hash | ||
end | ||
|
||
it 'can be initialized by the XML representation' do | ||
node = REXML::Element.new('Group') | ||
node.attributes['location'] = 'container:' | ||
node.attributes['name'] = 'Group Name' | ||
result = Workspace::GroupReference.from_node(node) | ||
result.should == @group | ||
end | ||
|
||
it 'returns the XML representation' do | ||
result = @group.to_node | ||
result.class.should == REXML::Element | ||
result.to_s.should == "<Group location='container:' name='Group Name'/>" | ||
end | ||
|
||
it 'can be converted back and forth without loss of information' do | ||
result = Workspace::GroupReference.from_node(@group.to_node) | ||
result.should == @group | ||
end | ||
|
||
it 'escapes XML entities' do | ||
file = Workspace::GroupReference.new('"&\'><.xcodeproj', 'group') | ||
result = file.to_node | ||
result.class.should == REXML::Element | ||
result.to_s.should == "<Group location='group:' name='"&'><.xcodeproj'/>" | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this attr needs to be documented